From 04cfdc2bf3b15bc9d9bdc5b0137d0d729c0ccadf Mon Sep 17 00:00:00 2001 From: Taylor Helsper Date: Thu, 29 Sep 2022 23:06:45 -0500 Subject: [PATCH 01/29] Update index.html --- lnbits/core/templates/core/index.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index f769b44f..1319fa1f 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -171,6 +171,19 @@ +
+
+ + + +
+
+   +
+
From fb58f1ed5e04895582ac79e889a74e2d9a07dd57 Mon Sep 17 00:00:00 2001 From: Taylor Helsper Date: Thu, 29 Sep 2022 23:42:38 -0500 Subject: [PATCH 02/29] Update myNode images --- lnbits/static/images/mynode.png | Bin 3757 -> 13994 bytes lnbits/static/images/mynodel.png | Bin 14352 -> 9272 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/lnbits/static/images/mynode.png b/lnbits/static/images/mynode.png index cf25bc586797b750a64ddba848b2499a0698a5b2..390446b8ee906da9e01142b477a59506269ce5e7 100644 GIT binary patch literal 13994 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 delta 3684 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 diff --git a/lnbits/static/images/mynodel.png b/lnbits/static/images/mynodel.png index b8afb9ffcd0dbe9c3c07cbd1e007b1067d92caa9..344b54b6db53406258f7f1207b78f72b9fb1cbf3 100644 GIT binary patch delta 5190 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 Date: Fri, 30 Sep 2022 10:19:17 -0500 Subject: [PATCH 03/29] Fix formatting --- lnbits/core/templates/core/index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 1319fa1f..68a7b7ed 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -180,9 +180,7 @@ > -
-   -
+
 
From 3aa4d09103ac94b1e3703086f1528da1020f53e3 Mon Sep 17 00:00:00 2001 From: Indra <86242283+VajraOfIndra@users.noreply.github.com> Date: Sun, 2 Oct 2022 16:45:16 +0100 Subject: [PATCH 04/29] Adding missing theme options in conf file Missing bitcoin theme in comment. Missing flamingo theme in list of values for LNBITS_THEME_OPTIONS. --- .env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 93b82325..7d6de35f 100644 --- a/.env.example +++ b/.env.example @@ -37,8 +37,8 @@ LNBITS_RESERVE_FEE_PERCENT=1.0 LNBITS_SITE_TITLE="LNbits" LNBITS_SITE_TAGLINE="free and open-source lightning wallet" LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'" -# Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic -LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador" +# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic +LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador" # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet @@ -91,4 +91,4 @@ LNBITS_DENOMINATION=sats # EclairWallet ECLAIR_URL=http://127.0.0.1:8283 -ECLAIR_PASS=eclairpw \ No newline at end of file +ECLAIR_PASS=eclairpw From b6755abc8e9911c34b96d3039140b2b64ff5634f Mon Sep 17 00:00:00 2001 From: HODLmeTight <35168804+TrezorHannes@users.noreply.github.com> Date: Wed, 5 Oct 2022 09:16:54 +0200 Subject: [PATCH 05/29] updated poetry install notes - referenced option for python3.10 and newer - poetry install with --only main, instead of --no-dev (deprecated) --- docs/guide/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 87679ed5..5e9fdb28 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -26,8 +26,8 @@ sudo apt install python3.9 python3.9-distutils curl -sSL https://install.python-poetry.org | python3 - export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal -poetry env use python3.9 -poetry install --no-dev +poetry env use python3.9 # you can exchange with python3.10 or newer versions. Identify your version with python3 --version and specify here +poetry install --only main poetry run python build.py mkdir data From 79319faa2146da17cd22467d9dd4762c42540f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 6 Oct 2022 10:17:21 +0200 Subject: [PATCH 06/29] make workflows only run on pull_request --- .github/workflows/codeql.yml | 2 -- .github/workflows/formatting.yml | 2 -- .github/workflows/mypy.yml | 2 +- .github/workflows/regtest.yml | 2 +- .github/workflows/tests.yml | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 876c8b8a..bbe95983 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,8 +1,6 @@ name: codeql on: - push: - branches: [main, ] pull_request: branches: [main] schedule: diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index e3d0fd35..21c7fb38 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,8 +1,6 @@ name: formatting on: - push: - branches: [ main ] pull_request: branches: [ main ] diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index d80da678..96b574d2 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -1,6 +1,6 @@ name: mypy -on: [push, pull_request] +on: [pull_request] jobs: check: diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index 2d7aae6b..f0adb3ac 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -1,6 +1,6 @@ name: regtest -on: [push, pull_request] +on: [pull_request] jobs: LndRestWallet: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d368fbb..c7b6e44b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: tests -on: [push, pull_request] +on: [pull_request] jobs: venv-sqlite: From 63e4f7d59c0505654d47b8793738211966b9b776 Mon Sep 17 00:00:00 2001 From: HODLmeTight <35168804+TrezorHannes@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:25:14 +0200 Subject: [PATCH 07/29] changed comment places and small adjustments - #comments in own lines - cleaned up some ominous settings - added --debug option for running the server --- docs/guide/installation.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 5e9fdb28..37104fde 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -18,21 +18,25 @@ If you have problems installing LNbits using these instructions, please have a l git clone https://github.com/lnbits/lnbits-legend.git cd lnbits-legend/ -# for making sure python 3.9 is installed, skip if installed +# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version sudo apt update sudo apt install software-properties-common sudo add-apt-repository ppa:deadsnakes/ppa sudo apt install python3.9 python3.9-distutils curl -sSL https://install.python-poetry.org | python3 - -export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal -poetry env use python3.9 # you can exchange with python3.10 or newer versions. Identify your version with python3 --version and specify here +# Once the above poetry install is completed, use the installation path printed to terminal and replace in the following command +export PATH="/home/user/.local/bin:$PATH" +# Next command, you can exchange with python3.10 or newer versions. +# Identify your version with python3 --version and specify in the next line +# command is only needed when your default python is not ^3.9 or ^3.10 +poetry env use python3.9 poetry install --only main -poetry run python build.py mkdir data cp .env.example .env -nano .env # set funding source +# set funding source amongst other options +nano .env ``` #### Running the server @@ -40,6 +44,7 @@ nano .env # set funding source ```sh poetry run lnbits # To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0' +# add --debug for slightly more verbose output ``` ## Option 2: Nix From 36b60ad318fffa87d8ec55753538525531d76dec Mon Sep 17 00:00:00 2001 From: HODLmeTight <35168804+TrezorHannes@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:50:33 +0200 Subject: [PATCH 08/29] added debug option for .env --- docs/guide/installation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 37104fde..6b95f93b 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -44,7 +44,8 @@ nano .env ```sh poetry run lnbits # To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0' -# add --debug for slightly more verbose output +# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output +# Note that you have to add the line DEBUG=true in your .env file, too. ``` ## Option 2: Nix From 27bae6a0c6cce731256095465905c92c1547c1e2 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 14:16:21 +0100 Subject: [PATCH 09/29] adds atm check, so withdraw isnt cancelled on false call by wallets --- lnbits/extensions/lnurldevice/lnurl.py | 29 +++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index df0cd4b8..ec7f3b48 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -184,22 +184,27 @@ async def lnurl_callback( raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found." ) - if pr: - if lnurldevicepayment.id != k1: - return {"status": "ERROR", "reason": "Bad K1"} - if lnurldevicepayment.payhash != "payment_hash": - return {"status": "ERROR", "reason": f"Payment already claimed"} + if device.device == "atm": + if not pr: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="No payment request" + ) + else: + if lnurldevicepayment.id != k1: + return {"status": "ERROR", "reason": "Bad K1"} + if lnurldevicepayment.payhash != "payment_hash": + return {"status": "ERROR", "reason": f"Payment already claimed"} lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload ) - await pay_invoice( - wallet_id=device.wallet, - payment_request=pr, - max_sat=lnurldevicepayment.sats / 1000, - extra={"tag": "withdraw"}, - ) - return {"status": "OK"} + await pay_invoice( + wallet_id=device.wallet, + payment_request=pr, + max_sat=lnurldevicepayment.sats / 1000, + extra={"tag": "withdraw"}, + ) + return {"status": "OK"} payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, From 8e8bf08ea587aa9013a966a807600812fee3c780 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:21:09 +0100 Subject: [PATCH 10/29] fix issue with splitting to multiple wallets-queue --- lnbits/extensions/splitpayments/tasks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py index b7cf1750..cfc6c226 100644 --- a/lnbits/extensions/splitpayments/tasks.py +++ b/lnbits/extensions/splitpayments/tasks.py @@ -28,6 +28,10 @@ async def on_invoice_paid(payment: Payment) -> None: # now we make some special internal transfers (from no one to the receiver) targets = await get_targets(payment.wallet_id) + + if not targets: + return + transfers = [ (target.wallet, int(target.percent * payment.amount / 100)) for target in targets @@ -41,9 +45,6 @@ async def on_invoice_paid(payment: Payment) -> None: ) return - if not targets: - return - # mark the original payment with one extra key, "splitted" # (this prevents us from doing this process again and it's informative) # and reduce it by the amount we're going to send to the producer @@ -76,5 +77,5 @@ async def on_invoice_paid(payment: Payment) -> None: ) # manually send this for now - await internal_invoice_queue.put(internal_checking_id) + await internal_invoice_queue.put(internal_checking_id) return From 11ec82e33962462880d08a9dc2ab6e828762edbd Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:21:53 +0100 Subject: [PATCH 11/29] floor amount when not whole sat --- lnbits/extensions/scrub/tasks.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py index 320d34da..62adc5e5 100644 --- a/lnbits/extensions/scrub/tasks.py +++ b/lnbits/extensions/scrub/tasks.py @@ -1,6 +1,7 @@ import asyncio import json from http import HTTPStatus +from math import floor from urllib.parse import urlparse import httpx @@ -26,7 +27,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: # (avoid loops) - if "scrubed" == payment.extra.get("tag"): + if payment.extra.get("tag") == "scrubed": # already scrubbed return @@ -42,12 +43,13 @@ async def on_invoice_paid(payment: Payment) -> None: # I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267 domain = urlparse(data["callback"]).netloc - + rounded_amount = floor(payment.amount / 1000) * 1000 + async with httpx.AsyncClient() as client: try: r = await client.get( data["callback"], - params={"amount": payment.amount}, + params={"amount": rounded_amount}, timeout=40, ) if r.is_error: @@ -66,7 +68,8 @@ async def on_invoice_paid(payment: Payment) -> None: ) invoice = bolt11.decode(params["pr"]) - if invoice.amount_msat != payment.amount: + + if invoice.amount_msat != rounded_amount: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=f"{domain} returned an invalid invoice. Expected {payment.amount} msat, got {invoice.amount_msat}.", From 7316ef7dbb67a9327597e2e97aa7ef54f7a08ac0 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:33:20 +0100 Subject: [PATCH 12/29] add disclaimer about whole (integer) sats --- lnbits/extensions/scrub/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lnbits/extensions/scrub/README.md b/lnbits/extensions/scrub/README.md index 680c5e6d..3b8d0b2d 100644 --- a/lnbits/extensions/scrub/README.md +++ b/lnbits/extensions/scrub/README.md @@ -4,6 +4,8 @@ SCRUB is a small but handy extension that allows a user to take advantage of all the functionalities inside **LNbits** and upon a payment received to your LNbits wallet, automatically forward it to your desired wallet via LNURL or LNAddress! +Only whole values, integers, are Scrubbed, amounts will be rounded down (example: 6.3 will be 6)! The decimals, if existing, will be kept in your wallet! + [**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets) ## Usage From 92a756b9bb92078bb937f4a22c9b35b033f44fed Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:34:37 +0100 Subject: [PATCH 13/29] make format --- lnbits/extensions/scrub/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py index 62adc5e5..852f3860 100644 --- a/lnbits/extensions/scrub/tasks.py +++ b/lnbits/extensions/scrub/tasks.py @@ -44,7 +44,7 @@ async def on_invoice_paid(payment: Payment) -> None: # I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267 domain = urlparse(data["callback"]).netloc rounded_amount = floor(payment.amount / 1000) * 1000 - + async with httpx.AsyncClient() as client: try: r = await client.get( @@ -68,7 +68,7 @@ async def on_invoice_paid(payment: Payment) -> None: ) invoice = bolt11.decode(params["pr"]) - + if invoice.amount_msat != rounded_amount: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, From 3222ae198d4f4bda826f1a63f861d5e90d0df4ac Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 17:10:15 +0100 Subject: [PATCH 14/29] Adding bitcoinswitch to lnurldevices --- lnbits/extensions/lnurldevice/__init__.py | 7 +++- lnbits/extensions/lnurldevice/lnurl.py | 40 ++++++++++++++++-- lnbits/extensions/lnurldevice/migrations.py | 6 +++ lnbits/extensions/lnurldevice/models.py | 2 + lnbits/extensions/lnurldevice/tasks.py | 39 ++++++++++++++++++ lnbits/extensions/lnurldevice/views.py | 45 +++++++++++++++++++++ 6 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 lnbits/extensions/lnurldevice/tasks.py diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index 54849c95..dc4456b4 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from lnbits.db import Database from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_lnurldevice") @@ -11,7 +12,11 @@ lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice def lnurldevice_renderer(): return template_renderer(["lnbits/extensions/lnurldevice/templates"]) - +from .tasks import wait_for_paid_invoices from .lnurl import * # noqa from .views import * # noqa from .views_api import * # noqa + +def lnurldevice_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index ec7f3b48..a2bc0dd4 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -102,7 +102,22 @@ async def lnurl_v1_params( if device.device == "atm": if paymentcheck: return {"status": "ERROR", "reason": f"Payment already claimed"} - + if device.device == "switch": + lnurldevicepayment = await create_lnurldevicepayment( + deviceid=device.id, + sats=device.profit, + ) + if not lnurldevicepayment: + return {"status": "ERROR", "reason": "Could not create payment."} + return { + "tag": "payRequest", + "callback": request.url_for( + "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id + ), + "minSendable": device.profit * 1000, + "maxSendable": device.profit * 1000, + "metadata": await device.lnurlpay_metadata(), + } if len(p) % 4 > 0: p += "=" * (4 - (len(p) % 4)) @@ -205,6 +220,27 @@ async def lnurl_callback( extra={"tag": "withdraw"}, ) return {"status": "OK"} + if device.device == "switch": + payment_hash, payment_request = await create_invoice( + wallet_id=device.wallet, + amount=lnurldevicepayment.sats / 1000, + memo=device.title, + unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), + extra={"tag": "Switch", "id": device.paymentid, "time": device.amount}, + ) + lnurldevicepayment = await update_lnurldevicepayment( + lnurldevicepayment_id=paymentid, payhash=payment_hash + ) + + return { + "pr": payment_request, + "successAction": { + "tag": "url", + "description": "Check the attached link", + "url": request.url_for("lnurldevice.displaypin", paymentid=paymentid), + }, + "routes": [], + } payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, @@ -226,5 +262,3 @@ async def lnurl_callback( }, "routes": [], } - - return resp.dict() diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index c7899282..65913b02 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -79,3 +79,9 @@ async def m002_redux(db): ) except: return + +async def m003_redux(db): + """ + Add 'meta' for storing various metadata about the wallet + """ + await db.execute("ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;") \ No newline at end of file diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index fef0aec1..cf617141 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -17,6 +17,7 @@ class createLnurldevice(BaseModel): currency: str device: str profit: float + amount: int class lnurldevices(BaseModel): @@ -27,6 +28,7 @@ class lnurldevices(BaseModel): currency: str device: str profit: float + amount: int timestamp: str def from_row(cls, row: Row) -> "lnurldevices": diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py new file mode 100644 index 00000000..2337c0ad --- /dev/null +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -0,0 +1,39 @@ +import asyncio +import json +from http import HTTPStatus +from urllib.parse import urlparse + +import httpx +from fastapi import HTTPException + +from lnbits import bolt11 +from lnbits.core.models import Payment +from lnbits.core.services import pay_invoice +from lnbits.helpers import get_current_extension_name +from lnbits.tasks import register_invoice_listener + +from .crud import get_lnurldevice +from .views import updater + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue, get_current_extension_name()) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + # (avoid loops) + if "switch" == payment.extra.get("tag"): + lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id")) + if not lnurldevicepayment: + return + if lnurldevicepayment.payhash == "used": + return + lnurldevicepayment = await update_lnurldevicepayment( + lnurldevicepayment_id=paymentid, payhash="used" + ) + return await updater(lnurldevicepayment.deviceid) + return \ No newline at end of file diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 3389e17c..4a28a5c1 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -51,3 +51,48 @@ async def displaypin(request: Request, paymentid: str = Query(None)): "lnurldevice/error.html", {"request": request, "pin": "filler", "not_paid": True}, ) + + +##################WEBSOCKET ROUTES######################## + + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket, lnurldevice_id: str): + await websocket.accept() + websocket.id = lnurldevice_id + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, lnurldevice_id: str): + for connection in self.active_connections: + if connection.id == lnurldevice_id: + await connection.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@lnurldevice_ext.websocket("/ws/{lnurldevice_id}", name="lnurldevice.lnurldevice_by_id") +async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str): + await manager.connect(websocket, lnurldevice_id) + try: + while True: + data = await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(websocket) + + +async def updater(lnurldevice_id): + lnurldevice = await get_lnurldevice(lnurldevice_id) + if not lnurldevice: + return + await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) \ No newline at end of file From 05c8f02cb7cc714457e885c2c9002f6cc97c3074 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 17:41:06 +0100 Subject: [PATCH 15/29] Added code sample for websocket stuff --- docs/devs/websockets.md | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/devs/websockets.md diff --git a/docs/devs/websockets.md b/docs/devs/websockets.md new file mode 100644 index 00000000..34cf940d --- /dev/null +++ b/docs/devs/websockets.md @@ -0,0 +1,87 @@ +--- +layout: default +parent: For developers +title: Websockets +nav_order: 2 +--- + + +Websockets +================= + +`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from teh copilot extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension): + + +```sh +from fastapi import Request, WebSocket, WebSocketDisconnect + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket, extension_id: str): + await websocket.accept() + websocket.id = extension_id + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, extension_id: str): + for connection in self.active_connections: + if connection.id == extension_id: + await connection.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@extension_ext.websocket("/ws/{extension_id}", name="extension.websocket_by_id") +async def websocket_endpoint(websocket: WebSocket, extension_id: str): + await manager.connect(websocket, extension_id) + try: + while True: + data = await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(websocket) + + +async def updater(extension_id, data): + extension = await get_extension(extension_id) + if not extension: + return + await manager.send_personal_message(f"{data}", extension_id) +``` + +Example vue-js function for listening to the websocket: + +``` +initWs: async function () { + if (location.protocol !== 'http:') { + localUrl = + 'wss://' + + document.domain + + ':' + + location.port + + '/extension/ws/' + + self.extension.id + } else { + localUrl = + 'ws://' + + document.domain + + ':' + + location.port + + '/extension/ws/' + + self.extension.id + } + this.ws = new WebSocket(localUrl) + this.ws.addEventListener('message', async ({data}) => { + const res = JSON.parse(data.toString()) + console.log(res) + }) +}, +``` \ No newline at end of file From 6d55445a1bb941ed2b13b290cb4d8994ef0a52ba Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:44:04 +0100 Subject: [PATCH 16/29] Update websockets.md --- docs/devs/websockets.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/devs/websockets.md b/docs/devs/websockets.md index 34cf940d..0638e4f2 100644 --- a/docs/devs/websockets.md +++ b/docs/devs/websockets.md @@ -9,7 +9,7 @@ nav_order: 2 Websockets ================= -`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from teh copilot extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension): +`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from the `copilot` extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension): ```sh @@ -84,4 +84,4 @@ initWs: async function () { console.log(res) }) }, -``` \ No newline at end of file +``` From b46c06012d1e4e280eb9a8d3459276489a935b62 Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:51:36 +0100 Subject: [PATCH 17/29] Revert "make gh workflows only run on pull_request" --- .github/workflows/codeql.yml | 2 ++ .github/workflows/formatting.yml | 2 ++ .github/workflows/mypy.yml | 2 +- .github/workflows/regtest.yml | 2 +- .github/workflows/tests.yml | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bbe95983..876c8b8a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,6 +1,8 @@ name: codeql on: + push: + branches: [main, ] pull_request: branches: [main] schedule: diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 21c7fb38..e3d0fd35 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,6 +1,8 @@ name: formatting on: + push: + branches: [ main ] pull_request: branches: [ main ] diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 96b574d2..d80da678 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -1,6 +1,6 @@ name: mypy -on: [pull_request] +on: [push, pull_request] jobs: check: diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index f0adb3ac..2d7aae6b 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -1,6 +1,6 @@ name: regtest -on: [pull_request] +on: [push, pull_request] jobs: LndRestWallet: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7b6e44b..5d368fbb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: tests -on: [pull_request] +on: [push, pull_request] jobs: venv-sqlite: From ff98b87eca0ab2309ccc1b1be33969aaeb96b035 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 22:19:30 +0100 Subject: [PATCH 18/29] Added a switch --- lnbits/extensions/lnurldevice/__init__.py | 1 + lnbits/extensions/lnurldevice/models.py | 2 +- .../templates/lnurldevice/_api_docs.html | 8 +-- .../templates/lnurldevice/index.html | 66 ++++++++++++++++++- lnbits/extensions/lnurldevice/views.py | 32 ++++++++- 5 files changed, 102 insertions(+), 7 deletions(-) diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index dc4456b4..5452e356 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -1,3 +1,4 @@ +import asyncio from fastapi import APIRouter from lnbits.db import Database diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index cf617141..0714c656 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -36,7 +36,7 @@ class lnurldevices(BaseModel): def lnurl(self, req: Request) -> Lnurl: url = req.url_for( - "lnurldevice.lnurl_response", device_id=self.id, _external=True + "lnurldevice.lnurl_v1_params", device_id=self.id ) return lnurl_encode(url) diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html index 7f9afa27..7497714e 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html @@ -1,10 +1,10 @@

- Register LNURLDevice devices to receive payments in your LNbits wallet.
- Build your own here - https://github.com/arcbtc/bitcoinpos + Such as the LNPoS + https://lnbits.github.io/lnpos
Created by, Ben Arc + LNURLDevice Settings + + + bitcoinSwitch embeddable QRCode + +

LNURLDevice device string
+
{% raw + %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{% endraw + %} Click to copy URL + + Click to copy URL - +
@@ -191,6 +221,7 @@ label="Type of device" > +
+ + +
{ diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 4a28a5c1..a64de84b 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -1,4 +1,6 @@ from http import HTTPStatus +import pyqrcode +from io import BytesIO from fastapi import Request from fastapi.param_functions import Query @@ -14,6 +16,9 @@ from lnbits.decorators import check_user_exists from . import lnurldevice_ext, lnurldevice_renderer from .crud import get_lnurldevice, get_lnurldevicepayment +from fastapi import Request, WebSocket, WebSocketDisconnect + +from starlette.responses import HTMLResponse, StreamingResponse templates = Jinja2Templates(directory="templates") @@ -52,6 +57,30 @@ async def displaypin(request: Request, paymentid: str = Query(None)): {"request": request, "pin": "filler", "not_paid": True}, ) +@lnurldevice_ext.get("/img/{lnurldevice_id}", response_class=StreamingResponse) +async def img(request: Request, lnurldevice_id): + lnurldevice = await get_lnurldevice(lnurldevice_id) + if not lnurldevice: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." + ) + qr = pyqrcode.create(lnurldevice.lnurl(request)) + stream = BytesIO() + qr.svg(stream, scale=3) + stream.seek(0) + + async def _generator(stream: BytesIO): + yield stream.getvalue() + + return StreamingResponse( + _generator(stream), + headers={ + "Content-Type": "image/svg+xml", + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, + ) ##################WEBSOCKET ROUTES######################## @@ -95,4 +124,5 @@ async def updater(lnurldevice_id): lnurldevice = await get_lnurldevice(lnurldevice_id) if not lnurldevice: return - await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) \ No newline at end of file + await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) + From 3bf38884ed8cb30ef9b5c428fdaea1acbedf1020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Fri, 7 Oct 2022 08:30:07 +0200 Subject: [PATCH 19/29] log successful connection to backend with logger.success() --- lnbits/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/app.py b/lnbits/app.py index 51482538..8b9cf798 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -126,7 +126,7 @@ def check_funding_source(app: FastAPI) -> None: logger.info("Retrying connection to backend in 5 seconds...") await asyncio.sleep(5) signal.signal(signal.SIGINT, original_sigint_handler) - logger.info( + logger.success( f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat." ) From 52dc528a8a7557da685da624e22058e737e49a16 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 11:29:06 +0100 Subject: [PATCH 20/29] Added qrdialogue --- .../templates/lnurldevice/index.html | 61 +++++++++++++++---- lnbits/extensions/lnurldevice/views.py | 19 +----- lnbits/extensions/lnurldevice/views_api.py | 13 +++- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index 528e5afe..b55c83f7 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -94,18 +94,17 @@ - bitcoinSwitch embeddable QRCode - + v-if="props.row.device == 'switch'" + :disable="protocol == 'http:'" + flat + unelevated + dense + size="xs" + icon="visibility" + :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" + @click="openQrCodeDialog(props.row.id)" + > LNURLs only work over HTTPS view LNURL + + + + + + {% raw %} + +

+ ID: {{ qrCodeDialog.data.id }}
+ +

+ {% endraw %} +
+ Copy LNURL + Close +
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }} @@ -306,6 +333,7 @@ mixins: [windowMixin], data: function () { return { + protocol: window.location.protocol, location: window.location.hostname, wslocation: window.location.hostname, filter: '', @@ -404,6 +432,14 @@ } }, methods: { + openQrCodeDialog: function (lnurldevice_id) { + var lnurldevice = _.findWhere(this.lnurldeviceLinks, {id: lnurldevice_id}) + console.log(lnurldevice) + this.qrCodeDialog.data = _.clone(lnurldevice) + this.qrCodeDialog.data.url = + window.location.protocol + '//' + window.location.host + this.qrCodeDialog.show = true + }, cancellnurldevice: function (data) { var self = this self.formDialoglnurldevice.show = false @@ -460,6 +496,7 @@ .then(function (response) { if (response.data) { self.lnurldeviceLinks = response.data.map(maplnurldevice) + console.log(response.data) } }) .catch(function (error) { diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index a64de84b..26f581d3 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -64,23 +64,8 @@ async def img(request: Request, lnurldevice_id): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." ) - qr = pyqrcode.create(lnurldevice.lnurl(request)) - stream = BytesIO() - qr.svg(stream, scale=3) - stream.seek(0) - - async def _generator(stream: BytesIO): - yield stream.getvalue() - - return StreamingResponse( - _generator(stream), - headers={ - "Content-Type": "image/svg+xml", - "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - "Expires": "0", - }, - ) + return lnurldevice.lnurl(request) + ##################WEBSOCKET ROUTES######################## diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py index d152d210..4a4c2b90 100644 --- a/lnbits/extensions/lnurldevice/views_api.py +++ b/lnbits/extensions/lnurldevice/views_api.py @@ -45,14 +45,21 @@ async def api_lnurldevice_create_or_update( @lnurldevice_ext.get("/api/v1/lnurlpos") -async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_lnurldevices_retrieve(req: Request, wallet: WalletTypeInfo = Depends(get_key_type)): wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids try: return [ - {**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids) + {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + for lnurldevice in await get_lnurldevices(wallet_ids) ] except: - return "" + try: + return [ + {**lnurldevice.dict()} + for lnurldevice in await get_lnurldevices(wallet_ids) + ] + except: + return "" @lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}") From 7711387d6472171539493c2f830869edef1e49fd Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 15:23:57 +0300 Subject: [PATCH 21/29] fix: use bigint for amounts in postgres (#1030) --- lnbits/db.py | 6 ++++++ lnbits/extensions/boltcards/migrations.py | 8 ++++---- lnbits/extensions/boltz/migrations.py | 12 ++++++------ lnbits/extensions/invoices/migrations.py | 2 +- lnbits/extensions/lnurldevice/migrations.py | 2 +- lnbits/extensions/lnurlpayout/migrations.py | 4 ++-- lnbits/extensions/streamalerts/migrations.py | 2 +- lnbits/extensions/tipjar/migrations.py | 4 ++-- 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/lnbits/db.py b/lnbits/db.py index 66981784..f52b0391 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -52,6 +52,12 @@ class Compat: return "" return "" + @property + def big_int(self) -> str: + if self.type in {POSTGRES}: + return "BIGINT" + return "INT" + class Connection(Compat): def __init__(self, conn: AsyncConnection, txn, typ, name, schema): diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py index 08126013..9609e0c3 100644 --- a/lnbits/extensions/boltcards/migrations.py +++ b/lnbits/extensions/boltcards/migrations.py @@ -29,7 +29,7 @@ async def m001_initial(db): ) await db.execute( - """ + f""" CREATE TABLE boltcards.hits ( id TEXT PRIMARY KEY UNIQUE, card_id TEXT NOT NULL, @@ -38,7 +38,7 @@ async def m001_initial(db): useragent TEXT, old_ctr INT NOT NULL DEFAULT 0, new_ctr INT NOT NULL DEFAULT 0, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ @@ -47,11 +47,11 @@ async def m001_initial(db): ) await db.execute( - """ + f""" CREATE TABLE boltcards.refunds ( id TEXT PRIMARY KEY UNIQUE, hit_id TEXT NOT NULL, - refund_amount INT NOT NULL, + refund_amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ diff --git a/lnbits/extensions/boltz/migrations.py b/lnbits/extensions/boltz/migrations.py index e4026dd0..925322ec 100644 --- a/lnbits/extensions/boltz/migrations.py +++ b/lnbits/extensions/boltz/migrations.py @@ -1,16 +1,16 @@ async def m001_initial(db): await db.execute( - """ + f""" CREATE TABLE boltz.submarineswap ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, payment_hash TEXT NOT NULL, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, status TEXT NOT NULL, boltz_id TEXT NOT NULL, refund_address TEXT NOT NULL, refund_privkey TEXT NOT NULL, - expected_amount INT NOT NULL, + expected_amount {db.big_int} NOT NULL, timeout_block_height INT NOT NULL, address TEXT NOT NULL, bip21 TEXT NOT NULL, @@ -22,12 +22,12 @@ async def m001_initial(db): """ ) await db.execute( - """ + f""" CREATE TABLE boltz.reverse_submarineswap ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, onchain_address TEXT NOT NULL, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, instant_settlement BOOLEAN NOT NULL, status TEXT NOT NULL, boltz_id TEXT NOT NULL, @@ -37,7 +37,7 @@ async def m001_initial(db): claim_privkey TEXT NOT NULL, lockup_address TEXT NOT NULL, invoice TEXT NOT NULL, - onchain_amount INT NOT NULL, + onchain_amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ diff --git a/lnbits/extensions/invoices/migrations.py b/lnbits/extensions/invoices/migrations.py index c47a954a..74a0fdba 100644 --- a/lnbits/extensions/invoices/migrations.py +++ b/lnbits/extensions/invoices/migrations.py @@ -45,7 +45,7 @@ async def m001_initial_invoices(db): id TEXT PRIMARY KEY, invoice_id TEXT NOT NULL, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index c7899282..da62cb7e 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -29,7 +29,7 @@ async def m001_initial(db): payhash TEXT, payload TEXT NOT NULL, pin INT, - sats INT, + sats {db.big_int}, timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ diff --git a/lnbits/extensions/lnurlpayout/migrations.py b/lnbits/extensions/lnurlpayout/migrations.py index 6af04791..7a45e495 100644 --- a/lnbits/extensions/lnurlpayout/migrations.py +++ b/lnbits/extensions/lnurlpayout/migrations.py @@ -3,14 +3,14 @@ async def m001_initial(db): Initial lnurlpayouts table. """ await db.execute( - """ + f""" CREATE TABLE lnurlpayout.lnurlpayouts ( id TEXT PRIMARY KEY, title TEXT NOT NULL, wallet TEXT NOT NULL, admin_key TEXT NOT NULL, lnurlpay TEXT NOT NULL, - threshold INT NOT NULL + threshold {db.big_int} NOT NULL ); """ ) diff --git a/lnbits/extensions/streamalerts/migrations.py b/lnbits/extensions/streamalerts/migrations.py index 1b0cea37..7d50e8f1 100644 --- a/lnbits/extensions/streamalerts/migrations.py +++ b/lnbits/extensions/streamalerts/migrations.py @@ -25,7 +25,7 @@ async def m001_initial(db): name TEXT NOT NULL, message TEXT NOT NULL, cur_code TEXT NOT NULL, - sats INT NOT NULL, + sats {db.big_int} NOT NULL, amount FLOAT NOT NULL, service INTEGER NOT NULL, posted BOOLEAN NOT NULL, diff --git a/lnbits/extensions/tipjar/migrations.py b/lnbits/extensions/tipjar/migrations.py index 6b58fbca..d8f6da3f 100644 --- a/lnbits/extensions/tipjar/migrations.py +++ b/lnbits/extensions/tipjar/migrations.py @@ -19,8 +19,8 @@ async def m001_initial(db): wallet TEXT NOT NULL, name TEXT NOT NULL, message TEXT NOT NULL, - sats INT NOT NULL, - tipjar INT NOT NULL, + sats {db.big_int} NOT NULL, + tipjar {db.big_int} NOT NULL, FOREIGN KEY(tipjar) REFERENCES {db.references_schema}TipJars(id) ); """ From 8d4337679f93161966310ccf56d30eb4fd576317 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 13:46:40 +0100 Subject: [PATCH 22/29] Websocket/listener working --- lnbits/extensions/lnurldevice/crud.py | 6 +++-- lnbits/extensions/lnurldevice/lnurl.py | 26 +++++++++++-------- lnbits/extensions/lnurldevice/tasks.py | 8 +++--- .../templates/lnurldevice/index.html | 3 ++- lnbits/extensions/lnurldevice/views_api.py | 9 ++++--- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index 45166521..4c25e4cb 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -22,9 +22,10 @@ async def create_lnurldevice( wallet, currency, device, - profit + profit, + amount ) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( lnurldevice_id, @@ -34,6 +35,7 @@ async def create_lnurldevice( data.currency, data.device, data.profit, + data.amount, ), ) return await get_lnurldevice(lnurldevice_id) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index a2bc0dd4..3823005e 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -103,9 +103,19 @@ async def lnurl_v1_params( if paymentcheck: return {"status": "ERROR", "reason": f"Payment already claimed"} if device.device == "switch": + + price_msat = ( + await fiat_amount_as_satoshis(float(device.profit), device.currency) + if device.currency != "sat" + else amount_in_cent + ) * 1000 + lnurldevicepayment = await create_lnurldevicepayment( deviceid=device.id, - sats=device.profit, + payload="bla", + sats=price_msat, + pin=1, + payhash="bla", ) if not lnurldevicepayment: return {"status": "ERROR", "reason": "Could not create payment."} @@ -114,8 +124,8 @@ async def lnurl_v1_params( "callback": request.url_for( "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id ), - "minSendable": device.profit * 1000, - "maxSendable": device.profit * 1000, + "minSendable": price_msat, + "maxSendable": price_msat, "metadata": await device.lnurlpay_metadata(), } if len(p) % 4 > 0: @@ -224,21 +234,15 @@ async def lnurl_callback( payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, amount=lnurldevicepayment.sats / 1000, - memo=device.title, + memo=device.title + "-" + lnurldevicepayment.id, unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), - extra={"tag": "Switch", "id": device.paymentid, "time": device.amount}, + extra={"tag": "Switch", "id": paymentid, "time": device.amount}, ) lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment_id=paymentid, payhash=payment_hash ) - return { "pr": payment_request, - "successAction": { - "tag": "url", - "description": "Check the attached link", - "url": request.url_for("lnurldevice.displaypin", paymentid=paymentid), - }, "routes": [], } diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index 2337c0ad..4bf0d3b9 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -12,8 +12,9 @@ from lnbits.core.services import pay_invoice from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -from .crud import get_lnurldevice +from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment from .views import updater +from loguru import logger async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() @@ -23,17 +24,16 @@ async def wait_for_paid_invoices(): payment = await invoice_queue.get() await on_invoice_paid(payment) - async def on_invoice_paid(payment: Payment) -> None: # (avoid loops) - if "switch" == payment.extra.get("tag"): + if "Switch" == payment.extra.get("tag"): lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id")) if not lnurldevicepayment: return if lnurldevicepayment.payhash == "used": return lnurldevicepayment = await update_lnurldevicepayment( - lnurldevicepayment_id=paymentid, payhash="used" + lnurldevicepayment_id=payment.extra.get("id"), payhash="used" ) return await updater(lnurldevicepayment.deviceid) return \ No newline at end of file diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index b55c83f7..ccb0a6e9 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -240,6 +240,7 @@ fill-mask="0" reverse-fill-mask :step="'0.01'" + value="0.00" > Date: Fri, 7 Oct 2022 23:18:57 +0100 Subject: [PATCH 23/29] Black --- lnbits/extensions/lnurldevice/__init__.py | 2 ++ lnbits/extensions/lnurldevice/lnurl.py | 2 +- lnbits/extensions/lnurldevice/migrations.py | 5 ++++- lnbits/extensions/lnurldevice/models.py | 4 +--- lnbits/extensions/lnurldevice/tasks.py | 10 ++++++---- lnbits/extensions/lnurldevice/views.py | 4 ++-- lnbits/extensions/lnurldevice/views_api.py | 12 +++++++----- 7 files changed, 23 insertions(+), 16 deletions(-) diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index 5452e356..d987e6cc 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -13,11 +13,13 @@ lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice def lnurldevice_renderer(): return template_renderer(["lnbits/extensions/lnurldevice/templates"]) + from .tasks import wait_for_paid_invoices from .lnurl import * # noqa from .views import * # noqa from .views_api import * # noqa + def lnurldevice_start(): loop = asyncio.get_event_loop() loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index 3823005e..79892b78 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -122,7 +122,7 @@ async def lnurl_v1_params( return { "tag": "payRequest", "callback": request.url_for( - "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id + "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id ), "minSendable": price_msat, "maxSendable": price_msat, diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index 65913b02..fa56b3d1 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -80,8 +80,11 @@ async def m002_redux(db): except: return + async def m003_redux(db): """ Add 'meta' for storing various metadata about the wallet """ - await db.execute("ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;") \ No newline at end of file + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;" + ) diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index 0714c656..01bcc2ba 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -35,9 +35,7 @@ class lnurldevices(BaseModel): return cls(**dict(row)) def lnurl(self, req: Request) -> Lnurl: - url = req.url_for( - "lnurldevice.lnurl_v1_params", device_id=self.id - ) + url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) return lnurl_encode(url) async def lnurlpay_metadata(self) -> LnurlPayMetadata: diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index 4bf0d3b9..2f048138 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -16,6 +16,7 @@ from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepay from .views import updater from loguru import logger + async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() register_invoice_listener(invoice_queue, get_current_extension_name()) @@ -23,7 +24,8 @@ async def wait_for_paid_invoices(): while True: payment = await invoice_queue.get() await on_invoice_paid(payment) - + + async def on_invoice_paid(payment: Payment) -> None: # (avoid loops) if "Switch" == payment.extra.get("tag"): @@ -33,7 +35,7 @@ async def on_invoice_paid(payment: Payment) -> None: if lnurldevicepayment.payhash == "used": return lnurldevicepayment = await update_lnurldevicepayment( - lnurldevicepayment_id=payment.extra.get("id"), payhash="used" - ) + lnurldevicepayment_id=payment.extra.get("id"), payhash="used" + ) return await updater(lnurldevicepayment.deviceid) - return \ No newline at end of file + return diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 26f581d3..752d2c81 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -57,6 +57,7 @@ async def displaypin(request: Request, paymentid: str = Query(None)): {"request": request, "pin": "filler", "not_paid": True}, ) + @lnurldevice_ext.get("/img/{lnurldevice_id}", response_class=StreamingResponse) async def img(request: Request, lnurldevice_id): lnurldevice = await get_lnurldevice(lnurldevice_id) @@ -65,7 +66,7 @@ async def img(request: Request, lnurldevice_id): status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." ) return lnurldevice.lnurl(request) - + ##################WEBSOCKET ROUTES######################## @@ -110,4 +111,3 @@ async def updater(lnurldevice_id): if not lnurldevice: return await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) - diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py index 65625cbb..c034f66e 100644 --- a/lnbits/extensions/lnurldevice/views_api.py +++ b/lnbits/extensions/lnurldevice/views_api.py @@ -46,18 +46,20 @@ async def api_lnurldevice_create_or_update( @lnurldevice_ext.get("/api/v1/lnurlpos") -async def api_lnurldevices_retrieve(req: Request, wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_lnurldevices_retrieve( + req: Request, wallet: WalletTypeInfo = Depends(get_key_type) +): wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids try: return [ - {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} - for lnurldevice in await get_lnurldevices(wallet_ids) + {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + for lnurldevice in await get_lnurldevices(wallet_ids) ] except: try: return [ - {**lnurldevice.dict()} - for lnurldevice in await get_lnurldevices(wallet_ids) + {**lnurldevice.dict()} + for lnurldevice in await get_lnurldevices(wallet_ids) ] except: return "" From d4a5fe27768a8d05417e7ed8d6340b2420291a5f Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 23:48:42 +0100 Subject: [PATCH 24/29] isort --- lnbits/extensions/lnurldevice/tasks.py | 4 ++-- lnbits/extensions/lnurldevice/views.py | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index 2f048138..db8d87f0 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -12,9 +12,9 @@ from lnbits.core.services import pay_invoice from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment +from .crud import (get_lnurldevice, get_lnurldevicepayment, + update_lnurldevicepayment) from .views import updater -from loguru import logger async def wait_for_paid_invoices(): diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 752d2c81..5c6eba24 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -1,13 +1,13 @@ from http import HTTPStatus -import pyqrcode from io import BytesIO -from fastapi import Request +import pyqrcode +from fastapi import Request, WebSocket, WebSocketDisconnect from fastapi.param_functions import Query from fastapi.params import Depends from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse +from starlette.responses import HTMLResponse, StreamingResponse from lnbits.core.crud import update_payment_status from lnbits.core.models import User @@ -16,9 +16,6 @@ from lnbits.decorators import check_user_exists from . import lnurldevice_ext, lnurldevice_renderer from .crud import get_lnurldevice, get_lnurldevicepayment -from fastapi import Request, WebSocket, WebSocketDisconnect - -from starlette.responses import HTMLResponse, StreamingResponse templates = Jinja2Templates(directory="templates") From e3bd38fc6b11cbc9c1382fbac5fdfc005a0142db Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 23:51:28 +0100 Subject: [PATCH 25/29] back --- lnbits/extensions/lnurldevice/tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index db8d87f0..c8f3db04 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -12,8 +12,7 @@ from lnbits.core.services import pay_invoice from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -from .crud import (get_lnurldevice, get_lnurldevicepayment, - update_lnurldevicepayment) +from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment from .views import updater From 34effe3ae5690ad3b68d0a1af34ccc2d9d9b6e21 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 23:57:28 +0100 Subject: [PATCH 26/29] isort --- lnbits/extensions/lnurldevice/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index d987e6cc..c27c9e17 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -2,8 +2,8 @@ import asyncio from fastapi import APIRouter from lnbits.db import Database -from lnbits.helpers import template_renderer from lnbits.tasks import catch_everything_and_restart +from lnbits.helpers import template_renderer db = Database("ext_lnurldevice") From fbc20b3579169b44227cc8ba8e20cc78be1b65f5 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 8 Oct 2022 01:28:46 +0100 Subject: [PATCH 27/29] added credits --- .../templates/lnurldevice/_api_docs.html | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html index 7497714e..b239e981 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html @@ -2,12 +2,21 @@

For LNURL based Points of Sale, ATMs, and relay devices
- Such as the LNPoS + Use with:
+ LNPoS https://lnbits.github.io/lnpos
+ bitcoinSwitch + https://github.com/lnbits/bitcoinSwitch
+ FOSSA + https://github.com/lnbits/fossa
- Created by, Ben ArcBen Arc, BC, Vlad Stan

From 3bf3c91007ae4f589f06176a405061b92878e5ff Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:12:06 +0200 Subject: [PATCH 28/29] Add backend: ln.tips (LightningTipBot) wallet (#921) * add ln.tips * add wallet * revert lnbits * make format * .env.example * listener fix * reconnect fixed * make format * make format --- .env.example | 7 +- lnbits/extensions/lnurldevice/__init__.py | 5 +- .../templates/lnurldevice/_api_docs.html | 22 +-- .../templates/lnurldevice/index.html | 142 ++++++++------- lnbits/wallets/__init__.py | 2 + lnbits/wallets/lntips.py | 170 ++++++++++++++++++ 6 files changed, 263 insertions(+), 85 deletions(-) create mode 100644 lnbits/wallets/lntips.py diff --git a/.env.example b/.env.example index 7d6de35f..987c6ca6 100644 --- a/.env.example +++ b/.env.example @@ -41,7 +41,7 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador" # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" -# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet +# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet, LnTipsWallet # LndRestWallet, CoreLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet # VoidWallet is just a fallback that works without any actual Lightning capabilities, @@ -92,3 +92,8 @@ LNBITS_DENOMINATION=sats # EclairWallet ECLAIR_URL=http://127.0.0.1:8283 ECLAIR_PASS=eclairpw + +# LnTipsWallet +# Enter /api in LightningTipBot to get your key +LNTIPS_API_KEY=LNTIPS_ADMIN_KEY +LNTIPS_API_ENDPOINT=https://ln.tips diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index c27c9e17..d2010c44 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -1,9 +1,10 @@ import asyncio + from fastapi import APIRouter from lnbits.db import Database -from lnbits.tasks import catch_everything_and_restart from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_lnurldevice") @@ -14,8 +15,8 @@ def lnurldevice_renderer(): return template_renderer(["lnbits/extensions/lnurldevice/templates"]) -from .tasks import wait_for_paid_invoices from .lnurl import * # noqa +from .tasks import wait_for_paid_invoices from .views import * # noqa from .views_api import * # noqa diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html index b239e981..f93d44d8 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html @@ -3,20 +3,22 @@

For LNURL based Points of Sale, ATMs, and relay devices
Use with:
- LNPoS - https://lnbits.github.io/lnpos + https://lnbits.github.io/lnpos
- bitcoinSwitch - https://github.com/lnbits/bitcoinSwitch + https://github.com/lnbits/bitcoinSwitch
- FOSSA - https://github.com/lnbits/fossa + https://github.com/lnbits/fossa
- Created by, Ben Arc, BC, Vlad StanBen Arc, + BC, + Vlad Stan

diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index ccb0a6e9..028dd94b 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -94,17 +94,19 @@ LNURLs only work over HTTPS view LNURL + v-if="props.row.device == 'switch'" + :disable="protocol == 'http:'" + flat + unelevated + dense + size="xs" + icon="visibility" + :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" + @click="openQrCodeDialog(props.row.id)" + > + LNURLs only work over HTTPS view LNURL
LNURLDevice device string
- {% raw - %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{% endraw - %} Click to copy URL - - {% raw %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{% + endraw %} Click to copy URL + + {% raw - %}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}}, - {{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw - %} Click to copy URL - -
+ >{% raw + %}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}}, + {{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw + %} Click to copy URL + +
@@ -229,29 +230,28 @@ label="Profit margin (% added to invoices/deducted from faucets)" >
- - -
+ + +

ID: {{ qrCodeDialog.data.id }}
-

{% endraw %}
@@ -434,13 +433,15 @@ }, methods: { openQrCodeDialog: function (lnurldevice_id) { - var lnurldevice = _.findWhere(this.lnurldeviceLinks, {id: lnurldevice_id}) - console.log(lnurldevice) - this.qrCodeDialog.data = _.clone(lnurldevice) - this.qrCodeDialog.data.url = - window.location.protocol + '//' + window.location.host - this.qrCodeDialog.show = true - }, + var lnurldevice = _.findWhere(this.lnurldeviceLinks, { + id: lnurldevice_id + }) + console.log(lnurldevice) + this.qrCodeDialog.data = _.clone(lnurldevice) + this.qrCodeDialog.data.url = + window.location.protocol + '//' + window.location.host + this.qrCodeDialog.show = true + }, cancellnurldevice: function (data) { var self = this self.formDialoglnurldevice.show = false @@ -617,10 +618,7 @@ '//', window.location.host ].join('') - self.wslocation = [ - 'ws://', - window.location.host - ].join('') + self.wslocation = ['ws://', window.location.host].join('') LNbits.api .request('GET', '/api/v1/currencies') .then(response => { diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 41949652..fa533566 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa + from .cliche import ClicheWallet from .cln import CoreLightningWallet # legacy .env support from .cln import CoreLightningWallet as CLightningWallet @@ -9,6 +10,7 @@ from .lnbits import LNbitsWallet from .lndgrpc import LndWallet from .lndrest import LndRestWallet from .lnpay import LNPayWallet +from .lntips import LnTipsWallet from .lntxbot import LntxbotWallet from .opennode import OpenNodeWallet from .spark import SparkWallet diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py new file mode 100644 index 00000000..54220c85 --- /dev/null +++ b/lnbits/wallets/lntips.py @@ -0,0 +1,170 @@ +import asyncio +import hashlib +import json +import time +from os import getenv +from typing import AsyncGenerator, Dict, Optional + +import httpx +from loguru import logger + +from .base import ( + InvoiceResponse, + PaymentResponse, + PaymentStatus, + StatusResponse, + Wallet, +) + + +class LnTipsWallet(Wallet): + def __init__(self): + endpoint = getenv("LNTIPS_API_ENDPOINT") + self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint + + key = ( + getenv("LNTIPS_API_KEY") + or getenv("LNTIPS_ADMIN_KEY") + or getenv("LNTIPS_INVOICE_KEY") + ) + self.auth = {"Authorization": f"Basic {key}"} + + async def status(self) -> StatusResponse: + async with httpx.AsyncClient() as client: + r = await client.get( + f"{self.endpoint}/api/v1/balance", headers=self.auth, timeout=40 + ) + try: + data = r.json() + except: + return StatusResponse( + f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0 + ) + + if data.get("error"): + return StatusResponse(data["error"], 0) + + return StatusResponse(None, data["balance"] * 1000) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, + ) -> InvoiceResponse: + data: Dict = {"amount": amount} + if description_hash: + data["description_hash"] = description_hash.hex() + elif unhashed_description: + data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() + else: + data["memo"] = memo or "" + + async with httpx.AsyncClient() as client: + r = await client.post( + f"{self.endpoint}/api/v1/createinvoice", + headers=self.auth, + json=data, + timeout=40, + ) + + if r.is_error: + try: + data = r.json() + error_message = data["message"] + except: + error_message = r.text + pass + + return InvoiceResponse(False, None, None, error_message) + + data = r.json() + return InvoiceResponse( + True, data["payment_hash"], data["payment_request"], 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.endpoint}/api/v1/payinvoice", + headers=self.auth, + json={"pay_req": bolt11}, + timeout=None, + ) + if r.is_error: + return PaymentResponse(False, None, 0, None, r.text) + + 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()["details"] + checking_id = data["payment_hash"] + fee_msat = -data["fee"] + preimage = data["preimage"] + 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.endpoint}/api/v1/invoicestatus/{checking_id}", + headers=self.auth, + ) + + if r.is_error or len(r.text) == 0: + return PaymentStatus(None) + + data = r.json() + return PaymentStatus(data["paid"]) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + async with httpx.AsyncClient() as client: + r = await client.post( + url=f"{self.endpoint}/api/v1/paymentstatus/{checking_id}", + headers=self.auth, + ) + + if r.is_error: + return PaymentStatus(None) + data = r.json() + + paid_to_status = {False: None, True: True} + return PaymentStatus(paid_to_status[data.get("paid")]) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + last_connected = None + while True: + url = f"{self.endpoint}/api/v1/invoicestream" + try: + async with httpx.AsyncClient(timeout=None, headers=self.auth) as client: + last_connected = time.time() + async with client.stream("GET", url) as r: + async for line in r.aiter_lines(): + try: + prefix = "data: " + if not line.startswith(prefix): + continue + data = line[len(prefix) :] # sse parsing + inv = json.loads(data) + if not inv.get("payment_hash"): + continue + except: + continue + yield inv["payment_hash"] + except Exception as e: + pass + + # do not sleep if the connection was active for more than 10s + # since the backend is expected to drop the connection after 90s + if last_connected is None or time.time() - last_connected < 10: + logger.error( + f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying in 5 seconds" + ) + await asyncio.sleep(5) From d1302e4868f0fe12cf6bdf7d25e82045981077c2 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Tue, 11 Oct 2022 08:52:39 +0200 Subject: [PATCH 29/29] show progress (#987) --- lnbits/core/crud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index cbed6292..bb1ca0c1 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -333,7 +333,7 @@ async def delete_expired_invoices( """ ) logger.debug(f"Checking expiry of {len(rows)} invoices") - for (payment_request,) in rows: + for i, (payment_request,) in enumerate(rows): try: invoice = bolt11.decode(payment_request) except: @@ -343,7 +343,7 @@ async def delete_expired_invoices( if expiration_date > datetime.datetime.utcnow(): continue logger.debug( - f"Deleting expired invoice: {invoice.payment_hash} (expired: {expiration_date})" + f"Deleting expired invoice {i}/{len(rows)}: {invoice.payment_hash} (expired: {expiration_date})" ) await (conn or db).execute( """