{"X-Api-Key": "{{ wallet.inkey }}"}{"out": false, "amount": <int>, "memo": <string>}{"out": false, "amount": <int>, "memo": <string>, "unit": <string>, "webhook": <url:string>}
curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
"amount": <int>, "memo": <string>, "webhook":
- <url:string>}' -H "X-Api-Key: {{ wallet.inkey }}" -H
+ <url:string>, "unit": <string>}' -H "X-Api-Key: {{ wallet.inkey }}" -H
"Content-type: application/json"
diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html
index 88dc496d..f363a841 100644
--- a/lnbits/core/templates/core/index.html
+++ b/lnbits/core/templates/core/index.html
@@ -11,7 +11,7 @@
color="primary"
@click="processing"
type="a"
- href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}"
+ href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
>
Press to claim bitcoin
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index 2b6ec5de..db435866 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -273,442 +273,469 @@
- - This is an LNURL-withdraw QR code for slurping everything from - this wallet. Do not share with anyone. -
- ++ This is an LNURL-withdraw QR code for slurping everything + from this wallet. Do not share with anyone. +
+ +
+ It is compatible with balanceCheck and
+ balanceNotify so your wallet may keep pulling
+ the funds continuously from here after the first withdraw.
+
+ This QR code contains your wallet URL with full access. You + can scan it from your phone to open your wallet from there. +
- It is compatible with balanceCheck and
- balanceNotify so your wallet may keep pulling the
- funds continuously from here after the first withdraw.
-
- This QR code contains your wallet URL with full access. You - can scan it from your phone to open your wallet from there. -
-- This whole wallet will be deleted, the funds will be - UNRECOVERABLE. -
-+ This whole wallet will be deleted, the funds will be + UNRECOVERABLE. +
+- {{receive.lnurl.domain}} is requesting an invoice: -
- {% endraw %} {% if LNBITS_DENOMINATION != 'sats' %} -
- Description: {{ parse.invoice.description }}
- Expire date: {{ parse.invoice.expireDate }}
- Hash: {{ parse.invoice.hash }}
-
- Authenticate with {{ parse.lnurlauth.domain }}?
+
+ {{receive.lnurl.domain}} is requesting an invoice:
- For every website and for every LNbits wallet, a new keypair will be
- deterministically generated so your identity can't be tied to your
- LNbits wallet or linked across websites. No other data will be shared
- with {{ parse.lnurlauth.domain }}.
- Your public key for {{ parse.lnurlauth.domain }} is:
- {{ parse.lnurlauth.pubkey }}
-
- {{ parse.lnurlpay.domain }} is requesting {{
- parse.lnurlpay.maxSendable | msatoshiFormat }} {{LNBITS_DENOMINATION}}
-
-
- and a {{parse.lnurlpay.commentAllowed}}-char comment
-
-
- {{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }} is
- requesting
- between {{ parse.lnurlpay.minSendable | msatoshiFormat }} and
- {{ parse.lnurlpay.maxSendable | msatoshiFormat }}
- {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
-
-
- and a {{parse.lnurlpay.commentAllowed}}-char comment
-
-
- {{ parse.lnurlpay.description }} -
-
-
+ Description: {{ parse.invoice.description }}
+ Expire date: {{ parse.invoice.expireDate }}
+ Hash: {{ parse.invoice.hash }}
+
+ Authenticate with {{ parse.lnurlauth.domain }}? +
++ For every website and for every LNbits wallet, a new keypair will be + deterministically generated so your identity can't be tied to your + LNbits wallet or linked across websites. No other data will be + shared with {{ parse.lnurlauth.domain }}. +
+Your public key for {{ parse.lnurlauth.domain }} is:
+
+ {{ parse.lnurlauth.pubkey }}
+
+ {{ parse.lnurlpay.domain }} is requesting {{
+ parse.lnurlpay.maxSendable | msatoshiFormat }}
+ {{LNBITS_DENOMINATION}}
+
+
+ and a {{parse.lnurlpay.commentAllowed}}-char comment
+
+
+ {{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }} is
+ requesting
+ between {{ parse.lnurlpay.minSendable | msatoshiFormat }} and
+ {{ parse.lnurlpay.maxSendable | msatoshiFormat }}
+ {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
+
+
+ and a {{parse.lnurlpay.commentAllowed}}-char comment
+
+
+ {{ parse.lnurlpay.description }} +
+
+
- Login functionality to be released in v0.2, for now, - make sure you bookmark this page for future access to your - wallet! -
-- This service is in BETA, and we hold no responsibility for people losing - access to funds. To encourage you to run your own LNbits installation, any - balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will - incur a charge of {{ service_fee }}% service fee per - week. -
-+ Login functionality to be released in v0.2, for now, + make sure you bookmark this page for future access to your + wallet! +
++ This service is in BETA, and we hold no responsibility for people losing + access to funds. {% if service_fee > 0 %} To encourage you to run your + own LNbits installation, any balance on {% raw %}{{ + disclaimerDialog.location.host }}{% endraw %} will incur a charge of + {{ service_fee }}% service fee per week. {% endif %} +
+
+ Connect your LNbits instance to a Discord Bot leveraging LNbits as a community based lightning node.
+
+ Created by, Chris Lennon
+
+ Based on User Manager, by Ben Arc
+
GET
+ /discordbot/api/v1/users
+ JSON list of users
+ curl -X GET {{ request.base_url }}discordbot/api/v1/users -H
+ "X-Api-Key: {{ user.wallets[0].inkey }}"
+
+ GET
+ /discordbot/api/v1/users/<user_id>
+ JSON list of users
+ curl -X GET {{ request.base_url
+ }}discordbot/api/v1/users/<user_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+ GET
+ /discordbot/api/v1/wallets/<user_id>
+ {"X-Api-Key": <string>}
+ JSON wallet data
+ curl -X GET {{ request.base_url
+ }}discordbot/api/v1/wallets/<user_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+ GET
+ /discordbot/api/v1/wallets<wallet_id>
+ {"X-Api-Key": <string>}
+ JSON a wallets transactions
+ curl -X GET {{ request.base_url
+ }}discordbot/api/v1/wallets<wallet_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+ POST
+ /discordbot/api/v1/users
+ {"X-Api-Key": <string>, "Content-type":
+ "application/json"}
+ {"admin_id": <string>, "user_name": <string>,
+ "wallet_name": <string>,"discord_id": <string>}
+ {"id": <string>, "name": <string>, "admin":
+ <string>, "discord_id": <string>}
+ curl -X POST {{ request.base_url }}discordbot/api/v1/users -d
+ '{"admin_id": "{{ user.id }}", "wallet_name": <string>,
+ "user_name": <string>, "discord_id": <string>}' -H "X-Api-Key: {{
+ user.wallets[0].inkey }}" -H "Content-type: application/json"
+
+ POST
+ /discordbot/api/v1/wallets
+ {"X-Api-Key": <string>, "Content-type":
+ "application/json"}
+ {"user_id": <string>, "wallet_name": <string>,
+ "admin_id": <string>}
+ {"id": <string>, "admin": <string>, "name":
+ <string>, "user": <string>, "adminkey": <string>,
+ "inkey": <string>}
+ curl -X POST {{ request.base_url }}discordbot/api/v1/wallets -d
+ '{"user_id": <string>, "wallet_name": <string>,
+ "admin_id": "{{ user.id }}"}' -H "X-Api-Key: {{ user.wallets[0].inkey
+ }}" -H "Content-type: application/json"
+
+ DELETE
+ /discordbot/api/v1/users/<user_id>
+ {"X-Api-Key": <string>}
+ curl -X DELETE {{ request.base_url
+ }}discordbot/api/v1/users/<user_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+ DELETE
+ /discordbot/api/v1/wallets/<wallet_id>
+ {"X-Api-Key": <string>}
+ curl -X DELETE {{ request.base_url
+ }}discordbot/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{
+ user.wallets[0].inkey }}"
+
+ POST
+ /discordbot/api/v1/extensions
+ {"X-Api-Key": <string>}
+ curl -X POST {{ request.base_url }}discordbot/api/v1/extensions -d
+ '{"userid": <string>, "extension": <string>, "active":
+ <integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H
+ "Content-type: application/json"
+
+ 
+
+
+curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"
diff --git a/lnbits/extensions/example/__init__.py b/lnbits/extensions/example/__init__.py
new file mode 100644
index 00000000..96cc6428
--- /dev/null
+++ b/lnbits/extensions/example/__init__.py
@@ -0,0 +1,16 @@
+from fastapi import APIRouter
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+
+db = Database("ext_example")
+
+example_ext: APIRouter = APIRouter(prefix="/example", tags=["example"])
+
+
+def example_renderer():
+ return template_renderer(["lnbits/extensions/example/templates"])
+
+
+from .views import * # noqa
+from .views_api import * # noqa
diff --git a/lnbits/extensions/example/example.config.json b/lnbits/extensions/example/example.config.json
new file mode 100644
index 00000000..b8eec193
--- /dev/null
+++ b/lnbits/extensions/example/example.config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Build your own!!",
+ "short_description": "Join us, make an extension",
+ "icon": "info",
+ "contributors": ["github_username"]
+}
diff --git a/lnbits/extensions/example/migrations.py b/lnbits/extensions/example/migrations.py
new file mode 100644
index 00000000..99d7c362
--- /dev/null
+++ b/lnbits/extensions/example/migrations.py
@@ -0,0 +1,10 @@
+# async def m001_initial(db):
+# await db.execute(
+# f"""
+# CREATE TABLE example.example (
+# id TEXT PRIMARY KEY,
+# wallet TEXT NOT NULL,
+# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
+# );
+# """
+# )
diff --git a/lnbits/extensions/example/models.py b/lnbits/extensions/example/models.py
new file mode 100644
index 00000000..bfeb7517
--- /dev/null
+++ b/lnbits/extensions/example/models.py
@@ -0,0 +1,5 @@
+# from pydantic import BaseModel
+
+# class Example(BaseModel):
+# id: str
+# wallet: str
diff --git a/lnbits/extensions/example/templates/example/index.html b/lnbits/extensions/example/templates/example/index.html
new file mode 100644
index 00000000..d732ef37
--- /dev/null
+++ b/lnbits/extensions/example/templates/example/index.html
@@ -0,0 +1,59 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
++ A magical "g" is always available, with info about the user, wallets and + extensions: +
+{% raw %}{{ g }}{% endraw %}
+ {{this.ticketDialog.data.content}}
+
- ID: {{ qrCodeDialog.data.id }}
+ ID: {{ qrCodeDialog.data.id }}
- Unique: {{ qrCodeDialog.data.is_unique }}
+
+ Unique: {{ qrCodeDialog.data.is_unique }}
(QR code will change after each withdrawal)
Max. withdrawable: {{
@@ -356,4 +259,4 @@
+ {%else%} {% if SITE_TITLE != 'LNbits' %} {{ SITE_TITLE }} {% else
+ %}
+ LNbits {% endif %} {%endif%} {% endblock %}
+
+ {%else%} {% if SITE_TITLE != 'LNbits' %} {{ SITE_TITLE }} {% else %}
+ LNbits {% endif %} {% endif %}
{% endblock %}
diff --git a/lnbits/utils/exchange_rates.py b/lnbits/utils/exchange_rates.py
index 53a5a80b..4de1da8a 100644
--- a/lnbits/utils/exchange_rates.py
+++ b/lnbits/utils/exchange_rates.py
@@ -71,6 +71,7 @@ currencies = {
"IMP": "Isle of Man Pound",
"INR": "Indian Rupee",
"IQD": "Iraqi Dinar",
+ "IRT": "Iranian Toman",
"ISK": "Icelandic Króna",
"JEP": "Jersey Pound",
"JMD": "Jamaican Dollar",
@@ -179,6 +180,12 @@ class Provider(NamedTuple):
exchange_rate_providers = {
+ "exir": Provider(
+ "Exir",
+ "exir.io",
+ "https://api.exir.io/v1/ticker?symbol={from}-{to}",
+ lambda data, replacements: data["last"],
+ ),
"bitfinex": Provider(
"Bitfinex",
"bitfinex.com",
diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py
index 4f0802c6..8a2ca1a5 100644
--- a/lnbits/wallets/__init__.py
+++ b/lnbits/wallets/__init__.py
@@ -1,12 +1,12 @@
# flake8: noqa
-from .void import VoidWallet
from .clightning import CLightningWallet
-from .lndgrpc import LndWallet
-from .lntxbot import LntxbotWallet
-from .opennode import OpenNodeWallet
-from .lnpay import LNPayWallet
+from .eclair import EclairWallet
+from .fake import FakeWallet
from .lnbits import LNbitsWallet
from .lndrest import LndRestWallet
+from .lnpay import LNPayWallet
+from .lntxbot import LntxbotWallet
+from .opennode import OpenNodeWallet
from .spark import SparkWallet
-from .fake import FakeWallet
+from .void import VoidWallet
diff --git a/lnbits/wallets/base.py b/lnbits/wallets/base.py
index 973c1808..39c68759 100644
--- a/lnbits/wallets/base.py
+++ b/lnbits/wallets/base.py
@@ -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
diff --git a/lnbits/wallets/clightning.py b/lnbits/wallets/clightning.py
index 639d0b38..f8c2b16c 100644
--- a/lnbits/wallets/clightning.py
+++ b/lnbits/wallets/clightning.py
@@ -18,6 +18,7 @@ from .base import (
Unsupported,
Wallet,
)
+from lnbits import bolt11 as lnbits_bolt11
def async_wrap(func):
@@ -31,8 +32,8 @@ def async_wrap(func):
return run
-def _pay_invoice(ln, bolt11):
- return ln.pay(bolt11)
+def _pay_invoice(ln, payload):
+ return ln.call("pay", payload)
def _paid_invoices_stream(ln, last_pay_index):
@@ -102,10 +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:
wrapped = async_wrap(_pay_invoice)
- r = await wrapped(self.ln, bolt11)
+ r = await wrapped(self.ln, payload)
except RpcError as exc:
return PaymentResponse(False, None, 0, None, str(exc))
diff --git a/lnbits/wallets/eclair.py b/lnbits/wallets/eclair.py
new file mode 100644
index 00000000..aa7ddc39
--- /dev/null
+++ b/lnbits/wallets/eclair.py
@@ -0,0 +1,200 @@
+import asyncio
+import base64
+import json
+import urllib.parse
+from os import getenv
+from typing import AsyncGenerator, Dict, Optional
+
+import httpx
+from websockets import connect
+from websockets.exceptions import (
+ ConnectionClosed,
+ ConnectionClosedError,
+ ConnectionClosedOK,
+)
+
+from .base import (
+ InvoiceResponse,
+ PaymentResponse,
+ PaymentStatus,
+ StatusResponse,
+ Wallet,
+)
+
+
+class EclairError(Exception):
+ pass
+
+
+class UnknownError(Exception):
+ pass
+
+
+class EclairWallet(Wallet):
+ def __init__(self):
+ url = getenv("ECLAIR_URL")
+ self.url = url[:-1] if url.endswith("/") else url
+
+ self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws"
+
+ passw = getenv("ECLAIR_PASS")
+ encodedAuth = base64.b64encode(f":{passw}".encode("utf-8"))
+ auth = str(encodedAuth, "utf-8")
+ self.auth = {"Authorization": f"Basic {auth}"}
+
+ async def status(self) -> StatusResponse:
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.url}/usablebalances", headers=self.auth, timeout=40
+ )
+ try:
+ data = r.json()
+ except:
+ return StatusResponse(
+ f"Failed to connect to {self.url}, got: '{r.text[:200]}...'", 0
+ )
+
+ if r.is_error:
+ return StatusResponse(data["error"], 0)
+
+ return StatusResponse(None, data[0]["canSend"] * 1000)
+
+ async def create_invoice(
+ self,
+ amount: int,
+ memo: Optional[str] = None,
+ description_hash: Optional[bytes] = None,
+ ) -> InvoiceResponse:
+
+ data: Dict = {"amountMsat": amount * 1000}
+ if description_hash:
+ data["description_hash"] = description_hash.hex()
+ else:
+ data["description"] = memo or ""
+
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.url}/createinvoice", headers=self.auth, data=data, timeout=40
+ )
+
+ if r.is_error:
+ try:
+ data = r.json()
+ error_message = data["error"]
+ except:
+ error_message = r.text
+ pass
+
+ return InvoiceResponse(False, None, None, error_message)
+
+ data = r.json()
+ return InvoiceResponse(True, data["paymentHash"], data["serialized"], None)
+
+ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.url}/payinvoice",
+ headers=self.auth,
+ data={"invoice": bolt11, "blocking": True},
+ timeout=40,
+ )
+
+ if "error" in r.json():
+ try:
+ data = r.json()
+ error_message = data["error"]
+ except:
+ error_message = r.text
+ pass
+ return PaymentResponse(False, None, 0, None, error_message)
+
+ data = r.json()
+
+ checking_id = data["paymentHash"]
+ preimage = data["paymentPreimage"]
+
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.url}/getsentinfo",
+ headers=self.auth,
+ data={"paymentHash": checking_id},
+ timeout=40,
+ )
+
+ if "error" in r.json():
+ try:
+ data = r.json()
+ error_message = data["error"]
+ except:
+ error_message = r.text
+ pass
+ return PaymentResponse(
+ True, checking_id, 0, preimage, error_message
+ ) ## ?? is this ok ??
+
+ data = r.json()
+ fees = [i["status"] for i in data]
+ fee_msat = sum([i["feesPaid"] for i in fees])
+
+ return PaymentResponse(True, checking_id, fee_msat, preimage, None)
+
+ async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ f"{self.url}/getreceivedinfo",
+ headers=self.auth,
+ data={"paymentHash": checking_id},
+ )
+ data = r.json()
+
+ if r.is_error or "error" in data:
+ return PaymentStatus(None)
+
+ if data["status"]["type"] != "received":
+ return PaymentStatus(False)
+
+ return PaymentStatus(True)
+
+ async def get_payment_status(self, checking_id: str) -> PaymentStatus:
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ url=f"{self.url}/getsentinfo",
+ headers=self.auth,
+ data={"paymentHash": checking_id},
+ )
+
+ data = r.json()[0]
+
+ if r.is_error:
+ return PaymentStatus(None)
+
+ if data["status"]["type"] != "sent":
+ return PaymentStatus(False)
+
+ return PaymentStatus(True)
+
+ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
+
+ try:
+ async with connect(
+ self.ws_url,
+ extra_headers=[("Authorization", self.auth["Authorization"])],
+ ) as ws:
+ while True:
+ message = await ws.recv()
+ message = json.loads(message)
+
+ if message and message["type"] == "payment-received":
+ yield message["paymentHash"]
+
+ except (
+ OSError,
+ ConnectionClosedOK,
+ ConnectionClosedError,
+ ConnectionClosed,
+ ) as ose:
+ print("OSE", ose)
+ pass
+
+ print("lost connection to eclair's websocket, retrying in 5 seconds")
+ await asyncio.sleep(5)
diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py
index 93e50abd..331d5285 100644
--- a/lnbits/wallets/fake.py
+++ b/lnbits/wallets/fake.py
@@ -36,7 +36,13 @@ class FakeWallet(Wallet):
"out": False,
"amount": amount,
"currency": "bc",
- "privkey": hashlib.pbkdf2_hmac('sha256', secret.encode("utf-8"), ("FakeWallet").encode("utf-8"), 2048, 32).hex(),
+ "privkey": hashlib.pbkdf2_hmac(
+ "sha256",
+ secret.encode("utf-8"),
+ ("FakeWallet").encode("utf-8"),
+ 2048,
+ 32,
+ ).hex(),
"memo": None,
"description_hash": None,
"description": "",
@@ -53,22 +59,29 @@ class FakeWallet(Wallet):
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:]
+ 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) -> PaymentResponse:
- invoice = decode(bolt11)
- if hasattr(invoice, 'checking_id') and invoice.checking_id[6:] == data["privkey"][:6]:
+ 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!")
-
+ 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)
diff --git a/lnbits/wallets/lnbits.py b/lnbits/wallets/lnbits.py
index 5e34a2d7..414e987b 100644
--- a/lnbits/wallets/lnbits.py
+++ b/lnbits/wallets/lnbits.py
@@ -80,7 +80,7 @@ class LNbitsWallet(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(
url=f"{self.endpoint}/api/v1/payments",
diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py
index 85c6dd09..f9a0496a 100644
--- a/lnbits/wallets/lndgrpc.py
+++ b/lnbits/wallets/lndgrpc.py
@@ -92,11 +92,12 @@ class LndWallet(Wallet):
or getenv("LND_GRPC_INVOICE_MACAROON")
or getenv("LND_INVOICE_MACAROON")
)
-
-
+
encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
if encrypted_macaroon:
- macaroon = AESCipher(description="macaroon decryption").decrypt(encrypted_macaroon)
+ macaroon = AESCipher(description="macaroon decryption").decrypt(
+ encrypted_macaroon
+ )
self.macaroon = load_macaroon(macaroon)
cert = open(self.cert_path, "rb").read()
@@ -143,10 +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 = await self.rpc.SendPayment(
- lnrpc.SendPaymentRequest(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)
diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py
index 1c2a86a0..a107f749 100644
--- a/lnbits/wallets/lndrest.py
+++ b/lnbits/wallets/lndrest.py
@@ -39,11 +39,13 @@ class LndRestWallet(Wallet):
encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED")
if encrypted_macaroon:
- macaroon = AESCipher(description="macaroon decryption").decrypt(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")
+ self.cert = getenv("LND_REST_CERT", True)
async def status(self) -> StatusResponse:
try:
@@ -97,15 +99,11 @@ 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
- invoice = lnbits_bolt11.decode(bolt11)
lnrpcFeeLimit = dict()
- if invoice.amount_msat > 1000_000:
- lnrpcFeeLimit["percent"] = "1" # in percent
- else:
- lnrpcFeeLimit["fixed"] = "10" # in sat
+ lnrpcFeeLimit["fixed_msat"] = "{}".format(fee_limit_msat)
r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions",
@@ -162,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"]:
diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py
index 98610a79..88e6a3a2 100644
--- a/lnbits/wallets/lnpay.py
+++ b/lnbits/wallets/lnpay.py
@@ -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",
diff --git a/lnbits/wallets/lntxbot.py b/lnbits/wallets/lntxbot.py
index 6d706959..bbd87a73 100644
--- a/lnbits/wallets/lntxbot.py
+++ b/lnbits/wallets/lntxbot.py
@@ -74,7 +74,7 @@ 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",
diff --git a/lnbits/wallets/macaroon/__init__.py b/lnbits/wallets/macaroon/__init__.py
index b7cadcfe..4967c100 100644
--- a/lnbits/wallets/macaroon/__init__.py
+++ b/lnbits/wallets/macaroon/__init__.py
@@ -1 +1 @@
-from .macaroon import load_macaroon, AESCipher
\ No newline at end of file
+from .macaroon import load_macaroon, AESCipher
diff --git a/lnbits/wallets/macaroon/macaroon.py b/lnbits/wallets/macaroon/macaroon.py
index dd6ff636..3548e9e2 100644
--- a/lnbits/wallets/macaroon/macaroon.py
+++ b/lnbits/wallets/macaroon/macaroon.py
@@ -5,10 +5,11 @@ from hashlib import md5
import getpass
BLOCK_SIZE = 16
-import getpass
+import getpass
+
def load_macaroon(macaroon: str) -> str:
- """Returns hex version of a macaroon encoded in base64 or the file path.
+ """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
@@ -29,6 +30,7 @@ def load_macaroon(macaroon: str) -> str:
pass
return macaroon
+
class AESCipher(object):
"""This class is compatible with crypto-js/aes.js
@@ -39,6 +41,7 @@ class AESCipher(object):
AES.decrypt(encrypted, password).toString(Utf8);
"""
+
def __init__(self, key=None, description=""):
self.key = key
self.description = description + " "
@@ -47,7 +50,6 @@ class AESCipher(object):
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]))]
@@ -70,8 +72,7 @@ class AESCipher(object):
return final_key[:output]
def decrypt(self, encrypted: str) -> str:
- """Decrypts a string using AES-256-CBC.
- """
+ """Decrypts a string using AES-256-CBC."""
passphrase = self.passphrase
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
@@ -92,7 +93,10 @@ class AESCipher(object):
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()
+ 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__":
diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py
index 965b6d66..0ac205e2 100644
--- a/lnbits/wallets/opennode.py
+++ b/lnbits/wallets/opennode.py
@@ -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",
diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py
index 93f37cce..7b9a23e2 100644
--- a/lnbits/wallets/spark.py
+++ b/lnbits/wallets/spark.py
@@ -107,9 +107,12 @@ 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)
+ r = await self.pay(
+ bolt11=bolt11,
+ maxfee=fee_limit_msat,
+ )
except (SparkError, UnknownError) as exc:
listpays = await self.listpays(bolt11)
if listpays:
@@ -129,7 +132,9 @@ class SparkWallet(Wallet):
if pay["status"] == "failed":
return PaymentResponse(False, None, 0, None, str(exc))
elif pay["status"] == "pending":
- return PaymentResponse(None, payment_hash, 0, None, None)
+ return PaymentResponse(
+ None, payment_hash, fee_limit_msat, None, None
+ )
elif pay["status"] == "complete":
r = pay
r["payment_preimage"] = pay["preimage"]
@@ -152,9 +157,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
diff --git a/lnbits/wallets/void.py b/lnbits/wallets/void.py
index 03b6db44..c5cc08b5 100644
--- a/lnbits/wallets/void.py
+++ b/lnbits/wallets/void.py
@@ -25,7 +25,7 @@ class VoidWallet(Wallet):
)
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:
diff --git a/tests/conftest.py b/tests/conftest.py
index 127233c1..27ba9137 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -19,9 +19,10 @@ def app():
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
+
@pytest.fixture
async def client(app):
- client = AsyncClient(app=app, base_url=f'http://{HOST}:{PORT}')
+ client = AsyncClient(app=app, base_url=f"http://{HOST}:{PORT}")
# yield and pass the client to the test
yield client
# close the async client after the test has finished
diff --git a/tests/core/views/test_generic.py b/tests/core/views/test_generic.py
index 4917cde4..5d3db0fb 100644
--- a/tests/core/views/test_generic.py
+++ b/tests/core/views/test_generic.py
@@ -1,6 +1,7 @@
import pytest
from tests.conftest import client
+
@pytest.mark.asyncio
async def test_core_views_generic(client):
response = await client.get("/")
diff --git a/tests/extensions/bleskomat/conftest.py b/tests/extensions/bleskomat/conftest.py
index 924998a7..265d3be0 100644
--- a/tests/extensions/bleskomat/conftest.py
+++ b/tests/extensions/bleskomat/conftest.py
@@ -4,16 +4,22 @@ import secrets
from lnbits.core.crud import create_account, create_wallet
from lnbits.extensions.bleskomat.crud import create_bleskomat, create_bleskomat_lnurl
from lnbits.extensions.bleskomat.models import CreateBleskomat
-from lnbits.extensions.bleskomat.helpers import generate_bleskomat_lnurl_secret, generate_bleskomat_lnurl_signature, prepare_lnurl_params, query_to_signing_payload
+from lnbits.extensions.bleskomat.helpers import (
+ generate_bleskomat_lnurl_secret,
+ generate_bleskomat_lnurl_signature,
+ prepare_lnurl_params,
+ query_to_signing_payload,
+)
from lnbits.extensions.bleskomat.exchange_rates import exchange_rate_providers
exchange_rate_providers["dummy"] = {
"name": "dummy",
"domain": None,
"api_url": None,
- "getter": lambda data, replacements: str(1e8),# 1 BTC = 100000000 sats
+ "getter": lambda data, replacements: str(1e8), # 1 BTC = 100000000 sats
}
+
@pytest.fixture
async def bleskomat():
user = await create_account()
@@ -22,11 +28,12 @@ async def bleskomat():
name="Test Bleskomat",
fiat_currency="EUR",
exchange_rate_provider="dummy",
- fee="0"
+ fee="0",
)
bleskomat = await create_bleskomat(data=data, wallet_id=wallet.id)
return bleskomat
+
@pytest.fixture
async def lnurl(bleskomat):
query = {
@@ -43,7 +50,7 @@ async def lnurl(bleskomat):
signature = generate_bleskomat_lnurl_signature(
payload=payload,
api_key_secret=bleskomat.api_key_secret,
- api_key_encoding=bleskomat.api_key_encoding
+ api_key_encoding=bleskomat.api_key_encoding,
)
secret = generate_bleskomat_lnurl_secret(bleskomat.api_key_id, signature)
params = json.JSONEncoder().encode(params)
diff --git a/tests/extensions/bleskomat/test_lnurl_api.py b/tests/extensions/bleskomat/test_lnurl_api.py
index 2ee59117..00358470 100644
--- a/tests/extensions/bleskomat/test_lnurl_api.py
+++ b/tests/extensions/bleskomat/test_lnurl_api.py
@@ -3,7 +3,10 @@ import secrets
from lnbits.core.crud import get_wallet
from lnbits.settings import HOST, PORT
from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl
-from lnbits.extensions.bleskomat.helpers import generate_bleskomat_lnurl_signature, query_to_signing_payload
+from lnbits.extensions.bleskomat.helpers import (
+ generate_bleskomat_lnurl_signature,
+ query_to_signing_payload,
+)
from tests.conftest import client
from tests.helpers import credit_wallet
from tests.extensions.bleskomat.conftest import bleskomat, lnurl
@@ -73,7 +76,9 @@ async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
}
payload = query_to_signing_payload(query)
signature = generate_bleskomat_lnurl_signature(
- payload=payload, api_key_secret=bleskomat.api_key_secret, api_key_encoding=bleskomat.api_key_encoding
+ payload=payload,
+ api_key_secret=bleskomat.api_key_secret,
+ api_key_encoding=bleskomat.api_key_encoding,
)
response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200
@@ -97,7 +102,9 @@ async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl):
response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.status_code == 200
assert response.json()["status"] == "ERROR"
- assert ("Insufficient balance" in response.json()["reason"]) or ("fee" in response.json()["reason"])
+ assert ("Insufficient balance" in response.json()["reason"]) or (
+ "fee" in response.json()["reason"]
+ )
wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 0
bleskomat_lnurl = await get_bleskomat_lnurl(secret)
@@ -123,4 +130,4 @@ async def test_bleskomat_lnurl_api_action_success(client, lnurl):
assert wallet.balance_msat == 50000
bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl.has_uses_remaining() == False
- WALLET.pay_invoice.assert_called_once_with(pr)
+ WALLET.pay_invoice.assert_called_once_with(pr, 2000)
diff --git a/tests/helpers.py b/tests/helpers.py
index 1687e25d..3774f6fc 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -2,6 +2,7 @@ import hashlib
import secrets
from lnbits.core.crud import create_payment
+
async def credit_wallet(wallet_id: str, amount: int):
preimage = secrets.token_hex(32)
m = hashlib.sha256()
@@ -14,6 +15,6 @@ async def credit_wallet(wallet_id: str, amount: int):
checking_id=payment_hash,
preimage=preimage,
memo="",
- amount=amount,# msat
- pending=False,# not pending, so it will increase the wallet's balance
+ amount=amount, # msat
+ pending=False, # not pending, so it will increase the wallet's balance
)
diff --git a/tests/mocks.py b/tests/mocks.py
index 5b20824c..a3b5308d 100644
--- a/tests/mocks.py
+++ b/tests/mocks.py
@@ -9,28 +9,42 @@ from lnbits.wallets.base import (
)
from lnbits.settings import WALLET
-WALLET.status = AsyncMock(return_value=StatusResponse(
- "",# no error
- 1000000,# msats
-))
-WALLET.create_invoice = AsyncMock(return_value=InvoiceResponse(
- True,# ok
- "6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd",# checking_id (i.e. payment_hash)
- "lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q",# payment_request
- "",# no error
-))
-def pay_invoice_side_effect(payment_request: str):
+WALLET.status = AsyncMock(
+ return_value=StatusResponse(
+ "", # no error
+ 1000000, # msats
+ )
+)
+WALLET.create_invoice = AsyncMock(
+ return_value=InvoiceResponse(
+ True, # ok
+ "6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd", # checking_id (i.e. payment_hash)
+ "lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q", # payment_request
+ "", # no error
+ )
+)
+
+
+def pay_invoice_side_effect(
+ payment_request: str, fee_limit_msat: int
+) -> PaymentResponse:
invoice = bolt11.decode(payment_request)
return PaymentResponse(
- True,# ok
- invoice.payment_hash,# checking_id (i.e. payment_hash)
- 0,# fee_msat
- "",# no error
+ True, # ok
+ invoice.payment_hash, # checking_id (i.e. payment_hash)
+ 0, # fee_msat
+ "", # no error
)
+
+
WALLET.pay_invoice = AsyncMock(side_effect=pay_invoice_side_effect)
-WALLET.get_invoice_status = AsyncMock(return_value=PaymentStatus(
- True,# paid
-))
-WALLET.get_payment_status = AsyncMock(return_value=PaymentStatus(
- True,# paid
-))
+WALLET.get_invoice_status = AsyncMock(
+ return_value=PaymentStatus(
+ True, # paid
+ )
+)
+WALLET.get_payment_status = AsyncMock(
+ return_value=PaymentStatus(
+ True, # paid
+ )
+)