From 80aba99673e40ef710363e63c9a97b047cfee707 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Fri, 24 Feb 2023 18:13:40 +0100 Subject: [PATCH 001/141] add license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..678845a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 LNbits + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 3644e0e2547fa4a5895b57784e033972a899e8c4 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 14:43:26 -0800 Subject: [PATCH 002/141] add to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0832bfb..515cf90 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,5 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n ![LNURLp](https://i.imgur.com/C8s1P0Q.jpg) - you can now open your LNURLp and copy the LNURL, get the shareable link or print it\ ![view lnurlp](https://i.imgur.com/4n41S7T.jpg) + +3. Optional - add Lightning Address \ No newline at end of file From 577a3932f5bac8314c7da9136e81e16fcb1c7908 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 15:51:42 -0800 Subject: [PATCH 003/141] initial crud, index form --- README.md | 4 ++- crud.py | 62 +++++++++++++++++++++++++++++++++++-- static/js/index.js | 6 +++- templates/lnurlp/index.html | 16 ++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 515cf90..ec603ec 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,6 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n - you can now open your LNURLp and copy the LNURL, get the shareable link or print it\ ![view lnurlp](https://i.imgur.com/4n41S7T.jpg) -3. Optional - add Lightning Address \ No newline at end of file +3. Optional - add Lightning Address + - attach a username to your lnurlp to create a lightning address + - the LN address format will be username@lnbits-domain-name diff --git a/crud.py b/crud.py index 4acb4a4..2362601 100644 --- a/crud.py +++ b/crud.py @@ -1,10 +1,12 @@ +import re from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash -from . import db +from . import db, maindb from .models import CreatePayLinkData, PayLink +from loguru import logger async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: link_id = urlsafe_short_hash()[:6] @@ -26,9 +28,11 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: success_url, comment_chars, currency, - fiat_base_multiplier + fiat_base_multiplier, + username + ) - VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( link_id, @@ -44,6 +48,7 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: data.comment_chars, data.currency, data.fiat_base_multiplier, + data.username, ), ) assert result @@ -53,6 +58,57 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: return link + +async def check_lnaddress_update(username: str, id: str) -> bool: + # check no duplicates for lnaddress when updating an username + row = await db.fetchall( + "SELECT username FROM lnurlp.pay_links WHERE username = ? AND id = ?", + (username, id), + ) + logger.info("number of rows from username search") + logger.info(len(row)) + if len(row) > 1: + assert False, "Lightning Address Already exists. Try a different One?" + return + else: + return True + + +async def check_lnaddress_exists(username: str) -> bool: + # check if lnaddress username exists in the database when creating a new entry + row = await db.fetchall( + "SELECT username FROM lnurlp.pay_links WHERE username = ?", (username,) + ) + logger.info("number of rows from lnaddress search") + if row: + assert False, "Lighting Address Already exists. Try a different One?" + return + else: + return True + + +async def check_lnaddress_format(username: str) -> bool: + # check username complies with lnaddress specification + if not re.match("^[a-z0-9-_.]{3,15}$", username): + assert False, "Only letters a-z0-9-_. allowed, min 3 and max 15 characters!" + return + return True + +async def get_wallet_key(wallet_id: str) -> str: + row = await maindb.fetchone("SELECT inkey FROM wallets WHERE id = ?", (wallet_id,)) + if row is not None: + return row[0] + else: + assert False, "Cannot locate wallet invoice key" + return + +async def get_address_data(username: str) -> Optional[PayLink]: + row = await db.fetchone( + "SELECT * FROM lnurl.pay_links WHERE username = ?", (username,) + ) + return PayLink.from_row(row) if row else None + + async def get_pay_link(link_id: str) -> Optional[PayLink]: row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,)) return PayLink.from_row(row) if row else None diff --git a/static/js/index.js b/static/js/index.js index c1372be..0075f28 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -9,6 +9,8 @@ var locationPath = [ window.location.pathname ].join('') +var domain = window.location.hostname + var mapPayLink = obj => { obj._data = _.clone(obj) obj.date = Quasar.utils.date.formatDate( @@ -26,6 +28,7 @@ new Vue({ mixins: [windowMixin], data() { return { + domain: domain, currencies: [], fiatRates: {}, checker: null, @@ -137,7 +140,8 @@ new Vue({ 'success_text', 'success_url', 'comment_chars', - 'currency' + 'currency', + 'username' ), (value, key) => (key === 'webhook_url' || diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index 3fbd344..f1ffacc 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -29,6 +29,7 @@ Description + Lightning Address Amount Currency @@ -148,6 +149,21 @@ label="Wallet *" > +
+
+ +
+
+ @{% raw %} {{domain}} {% endraw %} +
+ +
Date: Thu, 2 Mar 2023 16:03:57 -0800 Subject: [PATCH 004/141] remove circular, fix html --- crud.py | 16 ++++++++-------- templates/lnurlp/index.html | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crud.py b/crud.py index 2362601..31495ac 100644 --- a/crud.py +++ b/crud.py @@ -3,7 +3,7 @@ from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash -from . import db, maindb +from . import db #, maindb from .models import CreatePayLinkData, PayLink from loguru import logger @@ -94,13 +94,13 @@ async def check_lnaddress_format(username: str) -> bool: return return True -async def get_wallet_key(wallet_id: str) -> str: - row = await maindb.fetchone("SELECT inkey FROM wallets WHERE id = ?", (wallet_id,)) - if row is not None: - return row[0] - else: - assert False, "Cannot locate wallet invoice key" - return +# async def get_wallet_key(wallet_id: str) -> str: +# row = await maindb.fetchone("SELECT inkey FROM wallets WHERE id = ?", (wallet_id,)) +# if row is not None: +# return row[0] +# else: +# assert False, "Cannot locate wallet invoice key" +# return async def get_address_data(username: str) -> Optional[PayLink]: row = await db.fetchone( diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index f1ffacc..c902b23 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -29,9 +29,9 @@ Description - Lightning Address Amount Currency + Lightning Address @@ -149,6 +149,14 @@ label="Wallet *" > + +
- -
Date: Thu, 2 Mar 2023 16:45:41 -0800 Subject: [PATCH 005/141] update migrations, models, crud --- crud.py | 6 ------ migrations.py | 12 ++++++++---- models.py | 9 +++++++++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/crud.py b/crud.py index 31495ac..8667a09 100644 --- a/crud.py +++ b/crud.py @@ -95,12 +95,6 @@ async def check_lnaddress_format(username: str) -> bool: return True # async def get_wallet_key(wallet_id: str) -> str: -# row = await maindb.fetchone("SELECT inkey FROM wallets WHERE id = ?", (wallet_id,)) -# if row is not None: -# return row[0] -# else: -# assert False, "Cannot locate wallet invoice key" -# return async def get_address_data(username: str) -> Optional[PayLink]: row = await db.fetchone( diff --git a/migrations.py b/migrations.py index 1ec85eb..d3ec18a 100644 --- a/migrations.py +++ b/migrations.py @@ -10,7 +10,8 @@ async def m001_initial(db): description TEXT NOT NULL, amount {db.big_int} NOT NULL, served_meta INTEGER NOT NULL, - served_pr INTEGER NOT NULL + served_pr INTEGER NOT NULL, + username TEXT ); """ ) @@ -97,7 +98,8 @@ async def m006_redux(db): success_url TEXT, comment_chars INTEGER DEFAULT 0, webhook_headers TEXT, - webhook_body TEXT + webhook_body TEXT, + username TEXT ); """ ) @@ -122,9 +124,10 @@ async def m006_redux(db): max, fiat_base_multiplier, webhook_headers, - webhook_body + webhook_body, + username ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( row[0], @@ -142,6 +145,7 @@ async def m006_redux(db): row[12], row[13], row[14], + row[15], ), ) diff --git a/models.py b/models.py index de66d40..f8c6d98 100644 --- a/models.py +++ b/models.py @@ -23,6 +23,7 @@ class CreatePayLinkData(BaseModel): success_text: str = Query(None) success_url: str = Query(None) fiat_base_multiplier: int = Query(100, ge=1) + username: str = Query(None) class PayLink(BaseModel): @@ -41,6 +42,7 @@ class PayLink(BaseModel): comment_chars: int max: float fiat_base_multiplier: int + username: str @classmethod def from_row(cls, row: Row) -> "PayLink": @@ -73,3 +75,10 @@ class PayLink(BaseModel): return {"tag": "message", "message": self.success_text} else: return None + + async def lnurlpay_metadata(self, domain) -> LnurlPayMetadata: + text = f"Payment to {self.lnaddress}" + identifier = f"{self.lnaddress}@{domain}" + metadata = [["text/plain", text], ["text/identifier", identifier]] + + return LnurlPayMetadata(json.dumps(metadata)) From a44f70dcb6d7789f3fd3931cf749bd0eae96c93d Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:03:00 -0800 Subject: [PATCH 006/141] add methods to check dup address and format --- crud.py | 81 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/crud.py b/crud.py index 8667a09..aeefe2a 100644 --- a/crud.py +++ b/crud.py @@ -6,9 +6,44 @@ from lnbits.helpers import urlsafe_short_hash from . import db #, maindb from .models import CreatePayLinkData, PayLink -from loguru import logger +# from loguru import logger + +async def check_lnaddress_update(username: str, id: str) -> bool: + # check no duplicates for lnaddress when updating an username + row = await db.fetchall( + "SELECT username FROM lnurlp.pay_links WHERE username = ? AND id = ?", + (username, id), + ) + if len(row) > 1: + assert False, "Lightning Address Already exists. Try a different One?" + return + else: + return True + + +async def check_lnaddress_exists(username: str) -> bool: + # check if lnaddress username exists in the database when creating a new entry + row = await db.fetchall( + "SELECT username FROM lnurlp.pay_links WHERE username = ?", (username,) + ) + if row: + assert False, "Lighting Address Already exists. Try a different One?" + return + else: + return True + + +async def check_lnaddress_format(username: str) -> bool: + # check username complies with lnaddress specification + if not re.match("^[a-z0-9-_.]{3,15}$", username): + assert False, "Only letters a-z0-9-_. allowed, min 3 and max 15 characters!" + return + return True + async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: + await check_lnaddress_format(data.username) + await check_lnaddress_exists(data.username) link_id = urlsafe_short_hash()[:6] result = await db.execute( @@ -58,44 +93,6 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: return link - -async def check_lnaddress_update(username: str, id: str) -> bool: - # check no duplicates for lnaddress when updating an username - row = await db.fetchall( - "SELECT username FROM lnurlp.pay_links WHERE username = ? AND id = ?", - (username, id), - ) - logger.info("number of rows from username search") - logger.info(len(row)) - if len(row) > 1: - assert False, "Lightning Address Already exists. Try a different One?" - return - else: - return True - - -async def check_lnaddress_exists(username: str) -> bool: - # check if lnaddress username exists in the database when creating a new entry - row = await db.fetchall( - "SELECT username FROM lnurlp.pay_links WHERE username = ?", (username,) - ) - logger.info("number of rows from lnaddress search") - if row: - assert False, "Lighting Address Already exists. Try a different One?" - return - else: - return True - - -async def check_lnaddress_format(username: str) -> bool: - # check username complies with lnaddress specification - if not re.match("^[a-z0-9-_.]{3,15}$", username): - assert False, "Only letters a-z0-9-_. allowed, min 3 and max 15 characters!" - return - return True - -# async def get_wallet_key(wallet_id: str) -> str: - async def get_address_data(username: str) -> Optional[PayLink]: row = await db.fetchone( "SELECT * FROM lnurl.pay_links WHERE username = ?", (username,) @@ -124,6 +121,12 @@ async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]: async def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]: + for field in kwargs.items(): + if field[0] == "lnaddress": + value = field[1] + await check_lnaddress_format(value) + await check_lnaddress_update(value, str(link_id)) + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id) From ec091817c78091b6f7c102bb35bfd3528a656f2e Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:10:11 -0800 Subject: [PATCH 007/141] remove metadata method --- models.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/models.py b/models.py index f8c6d98..20c23a1 100644 --- a/models.py +++ b/models.py @@ -75,10 +75,3 @@ class PayLink(BaseModel): return {"tag": "message", "message": self.success_text} else: return None - - async def lnurlpay_metadata(self, domain) -> LnurlPayMetadata: - text = f"Payment to {self.lnaddress}" - identifier = f"{self.lnaddress}@{domain}" - metadata = [["text/plain", text], ["text/identifier", identifier]] - - return LnurlPayMetadata(json.dumps(metadata)) From 4a2d41964d14fa239fe66718b0b2214174c8346a Mon Sep 17 00:00:00 2001 From: Bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:47:29 -0800 Subject: [PATCH 008/141] Update manifest.json try to see if this can import by manifest --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 508a576..7dbe831 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "repos": [ { "id": "lnurlp", - "organisation": "lnbits", + "organisation": "bitkarrot", "repository": "lnurlp" } ] From 9fa895aa6d17f6d83d053eac98d62537bb07b573 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 20:34:40 -0800 Subject: [PATCH 009/141] fix username --- models.py | 2 +- templates/lnurlp/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models.py b/models.py index 20c23a1..0a4e341 100644 --- a/models.py +++ b/models.py @@ -23,7 +23,7 @@ class CreatePayLinkData(BaseModel): success_text: str = Query(None) success_url: str = Query(None) fiat_base_multiplier: int = Query(100, ge=1) - username: str = Query(None) + username: str class PayLink(BaseModel): diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index c902b23..b3142e0 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -162,7 +162,7 @@ From abf0305853e5e6fcb6256f48919825acf1eae1ea Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 20:48:21 -0800 Subject: [PATCH 010/141] fix username display --- templates/lnurlp/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index b3142e0..1384441 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -31,7 +31,7 @@ Description Amount Currency - Lightning Address + LN Username @@ -66,6 +66,7 @@ {{ props.row.min }} - {{ props.row.max }} {{ props.row.currency || 'sat' }} + {{ props.row.username }} Webhook to {{ props.row.webhook_url}} From 6367dee6c2ecd9e4e640e0c6d969960791bc12f6 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 21:12:42 -0800 Subject: [PATCH 011/141] add test lnaddress buttton --- templates/lnurlp/index.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index 1384441..22f22bc 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -57,6 +57,18 @@ :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" @click="openQrCodeDialog(props.row.id)" > + + + Check LN Address works via external call {{ props.row.description }} From f904c784621514499ed2d147d24d838c5f117804 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 21:19:25 -0800 Subject: [PATCH 012/141] update api docs --- templates/lnurlp/_api_docs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/lnurlp/_api_docs.html b/templates/lnurlp/_api_docs.html index abb37e9..9c07eba 100644 --- a/templates/lnurlp/_api_docs.html +++ b/templates/lnurlp/_api_docs.html @@ -62,7 +62,7 @@ {"description": <string> "amount": <integer> "max": <integer> "min": <integer> "comment_chars": - <integer>}
Returns 201 CREATED (application/json) From 1176fd032221134c3754c79254bf7ef3de27b24a Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 22:10:18 -0800 Subject: [PATCH 013/141] add redirect paths --- __init__.py | 11 +++++++++++ views_api.py | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/__init__.py b/__init__.py index aa13bb9..0d6bb20 100644 --- a/__init__.py +++ b/__init__.py @@ -17,6 +17,17 @@ lnurlp_static_files = [ "name": "lnurlp_static", } ] + +lnurlp_redirect_paths = [ + { + "from_path": "/.well-known/lnurlp", + "redirect_to_path": "/api/v1/well-known", + "header_filters": { + "accept": "application/json" + } + } +] + scheduled_tasks: List[asyncio.Task] = [] lnurlp_ext: APIRouter = APIRouter(prefix="/lnurlp", tags=["lnurlp"]) diff --git a/views_api.py b/views_api.py index b4af294..624406f 100644 --- a/views_api.py +++ b/views_api.py @@ -1,6 +1,7 @@ import json from asyncio.log import logger from http import HTTPStatus +from urllib.parse import urlparse from fastapi import Depends, Query, Request from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl @@ -19,6 +20,12 @@ from .crud import ( update_pay_link, ) from .models import CreatePayLinkData +from .lnurl import lnurl_response + +@lnurlp_ext.get("/api/v1/well-known/{username}") +async def lnaddress(username: str, request: Request): + domain = urlparse(str(request.url)).netloc + return await lnurl_response(username, domain, request) @lnurlp_ext.get("/api/v1/currencies") From 57c36e50aabd7a88e776bbba9dfadd132a1ed3c1 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 22:25:04 -0800 Subject: [PATCH 014/141] add lnurl callback methods --- lnurl.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/lnurl.py b/lnurl.py index 918a5bd..a1d2d58 100644 --- a/lnurl.py +++ b/lnurl.py @@ -1,6 +1,6 @@ from http import HTTPStatus -from fastapi import Request +from fastapi import Request, Query from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse from starlette.exceptions import HTTPException @@ -8,8 +8,68 @@ from lnbits.core.services import create_invoice from lnbits.utils.exchange_rates import get_fiat_rate_satoshis from . import lnurlp_ext -from .crud import increment_pay_link +from .crud import increment_pay_link, get_pay_link, get_address_data +from loguru import logger +from urllib.parse import urlparse +# for .well-known/lnurlp +async def lnurl_response(username: str, domain: str, request: Request): + address_data = await get_address_data(username) + + if not address_data: + return {"status": "ERROR", "reason": "Address not found."} + + resp = { + "tag": "payRequest", + "callback": request.url_for( + "lnurlp.api_lnurl_callback", link_id=address_data.id + ), + "metadata": await address_data.lnurlpay_metadata(domain=domain), + "minSendable": int(address_data.min * 1000), + "maxSendable": int(address_data.max * 1000), + } + + logger.debug("RESP", resp) + return resp + + +# for lnaddress callback +@lnurlp_ext.get( + "/api/v1/lnurl/cb/{link_id}", + status_code=HTTPStatus.OK, + name="lnurlp.api_lnurl_callback", +) +async def api_lnurl_callback(request: Request, link_id, amount: int = Query(...)): + address = await get_pay_link(link_id) + if not address: + return LnurlErrorResponse(reason=f'{"Address not found"}').dict() + + domain = urlparse(str(request.url)).netloc + assert domain + + unhashed_description = await address.lnurlpay_metadata(domain=domain) + unhashed_description = unhashed_description.encode() + payment_hash, payment_request = await create_invoice( + wallet_id=address.wallet, + amount=int(amount / 1000), + memo=address.description, + unhashed_description=unhashed_description, + extra={ + "tag": "lnurlp", + "link": address.id, + "extra": {"tag": f"Payment to {address.username}@{domain}"}, + }, + ) + + success_action = address.success_action(payment_hash) + if success_action: + resp = LnurlPayActionResponse( + pr=payment_request, success_action=success_action, routes=[] + ) + else: + resp = LnurlPayActionResponse(pr=payment_request, routes=[]) + + return resp.dict() @lnurlp_ext.get( "/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes (with long URL) From e3fafa5c20c1af1504a6a0d33ea48e7fa9b80633 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 22:30:08 -0800 Subject: [PATCH 015/141] adjust lnurl endpoints and calls --- lnurl.py | 1 + views_api.py | 1 + 2 files changed, 2 insertions(+) diff --git a/lnurl.py b/lnurl.py index a1d2d58..1ce2b22 100644 --- a/lnurl.py +++ b/lnurl.py @@ -12,6 +12,7 @@ from .crud import increment_pay_link, get_pay_link, get_address_data from loguru import logger from urllib.parse import urlparse + # for .well-known/lnurlp async def lnurl_response(username: str, domain: str, request: Request): address_data = await get_address_data(username) diff --git a/views_api.py b/views_api.py index 624406f..e35e535 100644 --- a/views_api.py +++ b/views_api.py @@ -24,6 +24,7 @@ from .lnurl import lnurl_response @lnurlp_ext.get("/api/v1/well-known/{username}") async def lnaddress(username: str, request: Request): + print("calling /api/v1/well-known") domain = urlparse(str(request.url)).netloc return await lnurl_response(username, domain, request) From a665714978ef23fb3b00cc2aada3ed2ddad9d27c Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 22:40:43 -0800 Subject: [PATCH 016/141] add lnurlp to application/json --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 0d6bb20..c83d46b 100644 --- a/__init__.py +++ b/__init__.py @@ -23,7 +23,7 @@ lnurlp_redirect_paths = [ "from_path": "/.well-known/lnurlp", "redirect_to_path": "/api/v1/well-known", "header_filters": { - "accept": "application/json" + "accept": "application/lnurlp+json" } } ] From afd1cece0b6c0333a99c505af6f276a290dfc52d Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 23:23:52 -0800 Subject: [PATCH 017/141] test redirects --- __init__.py | 7 ++----- views_api.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/__init__.py b/__init__.py index c83d46b..eba504b 100644 --- a/__init__.py +++ b/__init__.py @@ -20,11 +20,8 @@ lnurlp_static_files = [ lnurlp_redirect_paths = [ { - "from_path": "/.well-known/lnurlp", - "redirect_to_path": "/api/v1/well-known", - "header_filters": { - "accept": "application/lnurlp+json" - } + "from_path": "/.well-known/lnurlp/", + "redirect_to_path": "/api/v1/well-known/{username}", } ] diff --git a/views_api.py b/views_api.py index e35e535..bfc2e5d 100644 --- a/views_api.py +++ b/views_api.py @@ -23,10 +23,15 @@ from .models import CreatePayLinkData from .lnurl import lnurl_response @lnurlp_ext.get("/api/v1/well-known/{username}") -async def lnaddress(username: str, request: Request): - print("calling /api/v1/well-known") - domain = urlparse(str(request.url)).netloc - return await lnurl_response(username, domain, request) +async def lnaddress(username: str): + msg = "calling /api/v1/well-known" + username + return msg + +# @lnurlp_ext.get("/api/v1/well-known/{username}") +# async def lnaddress(username: str, request: Request): +# print("calling /api/v1/well-known") +# domain = urlparse(str(request.url)).netloc +# return await lnurl_response(username, domain, request) @lnurlp_ext.get("/api/v1/currencies") From 58737b58e7091f8eff0d8f04b16ab75d9d3a8aff Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 23:38:32 -0800 Subject: [PATCH 018/141] temporary hard link for testing --- __init__.py | 4 ++-- views_api.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/__init__.py b/__init__.py index eba504b..288fa54 100644 --- a/__init__.py +++ b/__init__.py @@ -20,8 +20,8 @@ lnurlp_static_files = [ lnurlp_redirect_paths = [ { - "from_path": "/.well-known/lnurlp/", - "redirect_to_path": "/api/v1/well-known/{username}", + "from_path": "/.well-known/lnurlp/two", + "redirect_to_path": "/api/v1/well-known/two", } ] diff --git a/views_api.py b/views_api.py index bfc2e5d..4f53a20 100644 --- a/views_api.py +++ b/views_api.py @@ -22,16 +22,16 @@ from .crud import ( from .models import CreatePayLinkData from .lnurl import lnurl_response -@lnurlp_ext.get("/api/v1/well-known/{username}") -async def lnaddress(username: str): - msg = "calling /api/v1/well-known" + username - return msg - # @lnurlp_ext.get("/api/v1/well-known/{username}") -# async def lnaddress(username: str, request: Request): -# print("calling /api/v1/well-known") -# domain = urlparse(str(request.url)).netloc -# return await lnurl_response(username, domain, request) +# async def lnaddress(username: str): +# msg = "calling /api/v1/well-known" + username +# return msg + +@lnurlp_ext.get("/api/v1/well-known/{username}") +async def lnaddress(username: str, request: Request): + print("calling /api/v1/well-known") + domain = urlparse(str(request.url)).netloc + return await lnurl_response(username, domain, request) @lnurlp_ext.get("/api/v1/currencies") From 017cb7353f998988380fe97246325e56809ca053 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Thu, 2 Mar 2023 23:49:00 -0800 Subject: [PATCH 019/141] fix metadata, crud --- crud.py | 2 +- models.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crud.py b/crud.py index aeefe2a..84b5e53 100644 --- a/crud.py +++ b/crud.py @@ -95,7 +95,7 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: async def get_address_data(username: str) -> Optional[PayLink]: row = await db.fetchone( - "SELECT * FROM lnurl.pay_links WHERE username = ?", (username,) + "SELECT * FROM lnurlp.pay_links WHERE username = ?", (username,) ) return PayLink.from_row(row) if row else None diff --git a/models.py b/models.py index 0a4e341..0bf70b1 100644 --- a/models.py +++ b/models.py @@ -56,10 +56,6 @@ class PayLink(BaseModel): url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id) return lnurl_encode(url) - @property - def lnurlpay_metadata(self) -> LnurlPayMetadata: - return LnurlPayMetadata(json.dumps([["text/plain", self.description]])) - def success_action(self, payment_hash: str) -> Optional[Dict]: if self.success_url: url: ParseResult = urlparse(self.success_url) @@ -75,3 +71,10 @@ class PayLink(BaseModel): return {"tag": "message", "message": self.success_text} else: return None + + async def lnurlpay_metadata(self, domain) -> LnurlPayMetadata: + text = f"Payment to {self.username}" + identifier = f"{self.username}@{domain}" + metadata = [["text/plain", text], ["text/identifier", identifier]] + + return LnurlPayMetadata(json.dumps(metadata)) \ No newline at end of file From 8082913eba76287101ac02db2bbca08bd17b0cc6 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Sun, 5 Mar 2023 21:24:54 -0800 Subject: [PATCH 020/141] fix redirect paths --- __init__.py | 4 ++-- views_api.py | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/__init__.py b/__init__.py index 288fa54..89734b0 100644 --- a/__init__.py +++ b/__init__.py @@ -20,8 +20,8 @@ lnurlp_static_files = [ lnurlp_redirect_paths = [ { - "from_path": "/.well-known/lnurlp/two", - "redirect_to_path": "/api/v1/well-known/two", + "from_path": "/.well-known/lnurlp/", + "redirect_to_path": "/api/v1/well-known/", } ] diff --git a/views_api.py b/views_api.py index 4f53a20..9d7ffd2 100644 --- a/views_api.py +++ b/views_api.py @@ -22,14 +22,9 @@ from .crud import ( from .models import CreatePayLinkData from .lnurl import lnurl_response -# @lnurlp_ext.get("/api/v1/well-known/{username}") -# async def lnaddress(username: str): -# msg = "calling /api/v1/well-known" + username -# return msg - @lnurlp_ext.get("/api/v1/well-known/{username}") async def lnaddress(username: str, request: Request): - print("calling /api/v1/well-known") + print("calling /api/v1/well-known/" + username) domain = urlparse(str(request.url)).netloc return await lnurl_response(username, domain, request) From 572ab62a027f0eabba7f9fe8bf441d495df352b9 Mon Sep 17 00:00:00 2001 From: HackMD <37423+hackmd-hub[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 00:51:28 +0000 Subject: [PATCH 021/141] last changed at Mar 6, 2023 4:49 PM, pushed by Bitkarrot --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ec603ec..8957dff 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,4 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n 3. Optional - add Lightning Address - attach a username to your lnurlp to create a lightning address - the LN address format will be username@lnbits-domain-name + - Find out more about the lightning address spec at lightningaddress.com From 89f9cda6f49fbe9966385690c567e42206ab8fde Mon Sep 17 00:00:00 2001 From: Bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Mon, 6 Mar 2023 23:24:13 -0800 Subject: [PATCH 022/141] remove trailing slashes for redirect --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 89734b0..4ef4c7d 100644 --- a/__init__.py +++ b/__init__.py @@ -20,8 +20,8 @@ lnurlp_static_files = [ lnurlp_redirect_paths = [ { - "from_path": "/.well-known/lnurlp/", - "redirect_to_path": "/api/v1/well-known/", + "from_path": "/.well-known/lnurlp", + "redirect_to_path": "/api/v1/well-known", } ] From 8ad4d5564b8fdac529c8b577b7d584760135cdfe Mon Sep 17 00:00:00 2001 From: Bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Mon, 6 Mar 2023 23:33:59 -0800 Subject: [PATCH 023/141] Update manifest.json put back org --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 7dbe831..508a576 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "repos": [ { "id": "lnurlp", - "organisation": "bitkarrot", + "organisation": "lnbits", "repository": "lnurlp" } ] From 0ae3751cdc13688576ce8f9e51ff88557673293f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 14 Mar 2023 11:40:01 +0100 Subject: [PATCH 024/141] username optional in model --- models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models.py b/models.py index 0bf70b1..a8156b7 100644 --- a/models.py +++ b/models.py @@ -23,7 +23,7 @@ class CreatePayLinkData(BaseModel): success_text: str = Query(None) success_url: str = Query(None) fiat_base_multiplier: int = Query(100, ge=1) - username: str + username: str = Query(None) class PayLink(BaseModel): @@ -33,6 +33,7 @@ class PayLink(BaseModel): min: float served_meta: int served_pr: int + username: Optional[str] webhook_url: Optional[str] webhook_headers: Optional[str] webhook_body: Optional[str] @@ -42,7 +43,6 @@ class PayLink(BaseModel): comment_chars: int max: float fiat_base_multiplier: int - username: str @classmethod def from_row(cls, row: Row) -> "PayLink": @@ -77,4 +77,4 @@ class PayLink(BaseModel): identifier = f"{self.username}@{domain}" metadata = [["text/plain", text], ["text/identifier", identifier]] - return LnurlPayMetadata(json.dumps(metadata)) \ No newline at end of file + return LnurlPayMetadata(json.dumps(metadata)) From 8fbaaeb31b67594a2663dda1d6f937856cbc5a39 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 14 Mar 2023 11:47:07 +0100 Subject: [PATCH 025/141] fix table --- templates/lnurlp/index.html | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index 22f22bc..e569d7c 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -26,7 +26,7 @@ > {% raw %} From f2a72a31f1e9e801df45f335235779c3fc9ceeee Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 14 Mar 2023 11:48:25 +0100 Subject: [PATCH 026/141] wording --- crud.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crud.py b/crud.py index 84b5e53..c8a96e4 100644 --- a/crud.py +++ b/crud.py @@ -3,11 +3,12 @@ from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash -from . import db #, maindb +from . import db # , maindb from .models import CreatePayLinkData, PayLink # from loguru import logger + async def check_lnaddress_update(username: str, id: str) -> bool: # check no duplicates for lnaddress when updating an username row = await db.fetchall( @@ -15,7 +16,7 @@ async def check_lnaddress_update(username: str, id: str) -> bool: (username, id), ) if len(row) > 1: - assert False, "Lightning Address Already exists. Try a different One?" + assert False, "Username already exists. Try a different one." return else: return True @@ -27,7 +28,7 @@ async def check_lnaddress_exists(username: str) -> bool: "SELECT username FROM lnurlp.pay_links WHERE username = ?", (username,) ) if row: - assert False, "Lighting Address Already exists. Try a different One?" + assert False, "Username already exists. Try a different one." return else: return True From 5109833b8f9b06f8e25fa2fa7c62f5d4e01619e1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 14 Mar 2023 11:52:43 +0100 Subject: [PATCH 027/141] table entry if no ln address is set --- templates/lnurlp/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index e569d7c..c40a0b1 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -31,7 +31,7 @@ Description Amount Currency - LN Username + LN address @@ -66,7 +66,7 @@ {{ props.row.min }} - {{ props.row.max }} {{ props.row.currency || 'sat' }} - {{ props.row.username }} + {{ props.row.username || 'None' }} Webhook to {{ props.row.webhook_url}} From bea8db1595945ba2e811c8a17675954c4e3963c9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 14 Mar 2023 15:05:52 +0100 Subject: [PATCH 028/141] lnaddress works --- lnurl.py | 129 +++++++++++++++++++++++--------------------------- migrations.py | 7 +++ models.py | 13 +++-- views_api.py | 10 ++-- 4 files changed, 81 insertions(+), 78 deletions(-) diff --git a/lnurl.py b/lnurl.py index 1ce2b22..7f31188 100644 --- a/lnurl.py +++ b/lnurl.py @@ -16,7 +16,9 @@ from urllib.parse import urlparse # for .well-known/lnurlp async def lnurl_response(username: str, domain: str, request: Request): address_data = await get_address_data(username) - + # for lnaddress + domain = urlparse(str(request.url)).netloc + link.domain = domain if not address_data: return {"status": "ERROR", "reason": "Address not found."} @@ -25,7 +27,7 @@ async def lnurl_response(username: str, domain: str, request: Request): "callback": request.url_for( "lnurlp.api_lnurl_callback", link_id=address_data.id ), - "metadata": await address_data.lnurlpay_metadata(domain=domain), + "metadata": await address_data.lnurlpay_metadata, "minSendable": int(address_data.min * 1000), "maxSendable": int(address_data.max * 1000), } @@ -34,75 +36,15 @@ async def lnurl_response(username: str, domain: str, request: Request): return resp -# for lnaddress callback @lnurlp_ext.get( - "/api/v1/lnurl/cb/{link_id}", + "/api/v1/lnurl/cb/lnaddr/{link_id}", status_code=HTTPStatus.OK, - name="lnurlp.api_lnurl_callback", + name="lnurlp.api_lnurl_lnaddr_callback", ) -async def api_lnurl_callback(request: Request, link_id, amount: int = Query(...)): - address = await get_pay_link(link_id) - if not address: - return LnurlErrorResponse(reason=f'{"Address not found"}').dict() - - domain = urlparse(str(request.url)).netloc - assert domain - - unhashed_description = await address.lnurlpay_metadata(domain=domain) - unhashed_description = unhashed_description.encode() - payment_hash, payment_request = await create_invoice( - wallet_id=address.wallet, - amount=int(amount / 1000), - memo=address.description, - unhashed_description=unhashed_description, - extra={ - "tag": "lnurlp", - "link": address.id, - "extra": {"tag": f"Payment to {address.username}@{domain}"}, - }, - ) - - success_action = address.success_action(payment_hash) - if success_action: - resp = LnurlPayActionResponse( - pr=payment_request, success_action=success_action, routes=[] - ) - else: - resp = LnurlPayActionResponse(pr=payment_request, routes=[]) - - return resp.dict() - -@lnurlp_ext.get( - "/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes (with long URL) - status_code=HTTPStatus.OK, - name="lnurlp.api_lnurl_response.deprecated", -) -@lnurlp_ext.get( - "/{link_id}", - status_code=HTTPStatus.OK, - name="lnurlp.api_lnurl_response", -) -async def api_lnurl_response(request: Request, link_id): - link = await increment_pay_link(link_id, served_meta=1) - if not link: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." - ) - - rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 - - resp = LnurlPayResponse( - callback=request.url_for("lnurlp.api_lnurl_callback", link_id=link.id), - min_sendable=round(link.min * rate) * 1000, - max_sendable=round(link.max * rate) * 1000, - metadata=link.lnurlpay_metadata, - ) - params = resp.dict() - - if link.comment_chars > 0: - params["commentAllowed"] = link.comment_chars - - return params +async def api_lnurl_lnaddr_callback( + request: Request, link_id, amount: int = Query(...) +): + return await api_lnurl_callback(request, link_id, amount, lnaddress=True) @lnurlp_ext.get( @@ -110,7 +52,9 @@ async def api_lnurl_response(request: Request, link_id): status_code=HTTPStatus.OK, name="lnurlp.api_lnurl_callback", ) -async def api_lnurl_callback(request: Request, link_id): +async def api_lnurl_callback( + request: Request, link_id, amount: int = Query(...), lnaddress=False +): link = await increment_pay_link(link_id, served_pr=1) if not link: raise HTTPException( @@ -126,7 +70,7 @@ async def api_lnurl_callback(request: Request, link_id): min = link.min * 1000 max = link.max * 1000 - amount_received = int(request.query_params.get("amount") or 0) + amount_received = amount if amount_received < min: return LnurlErrorResponse( reason=f"Amount {amount_received} is smaller than minimum {min}." @@ -143,6 +87,10 @@ async def api_lnurl_callback(request: Request, link_id): reason=f"Got a comment with {len(comment)} characters, but can only accept {link.comment_chars}" ).dict() + if lnaddress: + domain = urlparse(str(request.url)).netloc + link.domain = domain + payment_hash, payment_request = await create_invoice( wallet_id=link.wallet, amount=int(amount_received / 1000), @@ -165,3 +113,44 @@ async def api_lnurl_callback(request: Request, link_id): resp = LnurlPayActionResponse(pr=payment_request, routes=[]) return resp.dict() + + +@lnurlp_ext.get( + "/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes (with long URL) + status_code=HTTPStatus.OK, + name="lnurlp.api_lnurl_response.deprecated", +) +@lnurlp_ext.get( + "/{link_id}", + status_code=HTTPStatus.OK, + name="lnurlp.api_lnurl_response", +) +async def api_lnurl_response(request: Request, link_id, lnaddress=False): + link = await increment_pay_link(link_id, served_meta=1) + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." + ) + + rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 + + if lnaddress: + # for lnaddress + domain = urlparse(str(request.url)).netloc + link.domain = domain + callback = request.url_for("lnurlp.api_lnurl_lnaddr_callback", link_id=link.id) + else: + callback = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id) + + resp = LnurlPayResponse( + callback=callback, + min_sendable=round(link.min * rate) * 1000, + max_sendable=round(link.max * rate) * 1000, + metadata=link.lnurlpay_metadata, + ) + params = resp.dict() + + if link.comment_chars > 0: + params["commentAllowed"] = link.comment_chars + + return params diff --git a/migrations.py b/migrations.py index d3ec18a..cd2db9a 100644 --- a/migrations.py +++ b/migrations.py @@ -150,3 +150,10 @@ async def m006_redux(db): ) await db.execute("DROP TABLE lnurlp.pay_links_old") + + +async def m007_add_lnaddress_username(db): + """ + Add headers and body to webhooks + """ + await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN username TEXT;") diff --git a/models.py b/models.py index a8156b7..9611d41 100644 --- a/models.py +++ b/models.py @@ -34,6 +34,7 @@ class PayLink(BaseModel): served_meta: int served_pr: int username: Optional[str] + domain: Optional[str] webhook_url: Optional[str] webhook_headers: Optional[str] webhook_body: Optional[str] @@ -72,9 +73,13 @@ class PayLink(BaseModel): else: return None - async def lnurlpay_metadata(self, domain) -> LnurlPayMetadata: - text = f"Payment to {self.username}" - identifier = f"{self.username}@{domain}" - metadata = [["text/plain", text], ["text/identifier", identifier]] + @property + def lnurlpay_metadata(self) -> LnurlPayMetadata: + if self.domain and self.username: + text = f"Payment to {self.username}" + identifier = f"{self.username}@{self.domain}" + metadata = [["text/plain", text], ["text/identifier", identifier]] + else: + metadata = [["text/plain", self.description]] return LnurlPayMetadata(json.dumps(metadata)) diff --git a/views_api.py b/views_api.py index 9d7ffd2..97accd2 100644 --- a/views_api.py +++ b/views_api.py @@ -18,15 +18,17 @@ from .crud import ( get_pay_link, get_pay_links, update_pay_link, + get_address_data, ) from .models import CreatePayLinkData -from .lnurl import lnurl_response +from .lnurl import api_lnurl_response + @lnurlp_ext.get("/api/v1/well-known/{username}") async def lnaddress(username: str, request: Request): - print("calling /api/v1/well-known/" + username) - domain = urlparse(str(request.url)).netloc - return await lnurl_response(username, domain, request) + address_data = await get_address_data(username) + assert address_data, "User not found" + return await api_lnurl_response(request, address_data.id, lnaddress=True) @lnurlp_ext.get("/api/v1/currencies") From 07a39e6343b34b0b2866ea11bee57877b1ba9d5e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 14 Mar 2023 15:10:57 +0100 Subject: [PATCH 029/141] clean types --- lnurl.py | 31 ++++--------------------------- views_api.py | 2 +- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/lnurl.py b/lnurl.py index 7f31188..8b79678 100644 --- a/lnurl.py +++ b/lnurl.py @@ -13,29 +13,6 @@ from loguru import logger from urllib.parse import urlparse -# for .well-known/lnurlp -async def lnurl_response(username: str, domain: str, request: Request): - address_data = await get_address_data(username) - # for lnaddress - domain = urlparse(str(request.url)).netloc - link.domain = domain - if not address_data: - return {"status": "ERROR", "reason": "Address not found."} - - resp = { - "tag": "payRequest", - "callback": request.url_for( - "lnurlp.api_lnurl_callback", link_id=address_data.id - ), - "metadata": await address_data.lnurlpay_metadata, - "minSendable": int(address_data.min * 1000), - "maxSendable": int(address_data.max * 1000), - } - - logger.debug("RESP", resp) - return resp - - @lnurlp_ext.get( "/api/v1/lnurl/cb/lnaddr/{link_id}", status_code=HTTPStatus.OK, @@ -107,10 +84,10 @@ async def api_lnurl_callback( success_action = link.success_action(payment_hash) if success_action: resp = LnurlPayActionResponse( - pr=payment_request, success_action=success_action, routes=[] + pr=payment_request, success_action=success_action, routes=[] # type: ignore ) else: - resp = LnurlPayActionResponse(pr=payment_request, routes=[]) + resp = LnurlPayActionResponse(pr=payment_request, routes=[]) # type: ignore return resp.dict() @@ -144,8 +121,8 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): resp = LnurlPayResponse( callback=callback, - min_sendable=round(link.min * rate) * 1000, - max_sendable=round(link.max * rate) * 1000, + min_sendable=round(link.min * rate) * 1000, # type: ignore + max_sendable=round(link.max * rate) * 1000, # type: ignore metadata=link.lnurlpay_metadata, ) params = resp.dict() diff --git a/views_api.py b/views_api.py index 97accd2..7ffed6e 100644 --- a/views_api.py +++ b/views_api.py @@ -23,7 +23,7 @@ from .crud import ( from .models import CreatePayLinkData from .lnurl import api_lnurl_response - +# redirected from /.well-known/lnurlp @lnurlp_ext.get("/api/v1/well-known/{username}") async def lnaddress(username: str, request: Request): address_data = await get_address_data(username) From fcd5a307121ffa30e71770c61e73700992132e02 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:06:04 +0100 Subject: [PATCH 030/141] fix migrations --- migrations.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/migrations.py b/migrations.py index cd2db9a..5910699 100644 --- a/migrations.py +++ b/migrations.py @@ -10,8 +10,7 @@ async def m001_initial(db): description TEXT NOT NULL, amount {db.big_int} NOT NULL, served_meta INTEGER NOT NULL, - served_pr INTEGER NOT NULL, - username TEXT + served_pr INTEGER NOT NULL ); """ ) @@ -98,8 +97,7 @@ async def m006_redux(db): success_url TEXT, comment_chars INTEGER DEFAULT 0, webhook_headers TEXT, - webhook_body TEXT, - username TEXT + webhook_body TEXT ); """ ) @@ -124,8 +122,7 @@ async def m006_redux(db): max, fiat_base_multiplier, webhook_headers, - webhook_body, - username + webhook_body ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, From e46f1fb0271520860c3ec060973cdf28a24c2bae Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:07:00 +0100 Subject: [PATCH 031/141] fix migrations --- migrations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/migrations.py b/migrations.py index 5910699..879bec3 100644 --- a/migrations.py +++ b/migrations.py @@ -124,7 +124,7 @@ async def m006_redux(db): webhook_headers, webhook_body ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( row[0], @@ -142,7 +142,6 @@ async def m006_redux(db): row[12], row[13], row[14], - row[15], ), ) From 8c2f718c66d86a2c074d5d3f202a91b619ab4d1d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:14:25 +0100 Subject: [PATCH 032/141] add lnaddress to extra --- lnurl.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lnurl.py b/lnurl.py index 8b79678..d03faaa 100644 --- a/lnurl.py +++ b/lnurl.py @@ -65,20 +65,25 @@ async def api_lnurl_callback( ).dict() if lnaddress: - domain = urlparse(str(request.url)).netloc - link.domain = domain + # for lnaddress, we have to set this otherwise the metadata won't have the identifier + link.domain = urlparse(str(request.url)).netloc + + extra = { + "tag": "lnurlp", + "link": link.id, + "comment": comment, + "extra": request.query_params.get("amount"), + } + + if lnaddress and link.username and link.domain: + extra["lnaddress"] = f"{link.username}@{link.domain}" payment_hash, payment_request = await create_invoice( wallet_id=link.wallet, amount=int(amount_received / 1000), memo=link.description, unhashed_description=link.lnurlpay_metadata.encode(), - extra={ - "tag": "lnurlp", - "link": link.id, - "comment": comment, - "extra": request.query_params.get("amount"), - }, + extra=extra, ) success_action = link.success_action(payment_hash) @@ -112,9 +117,8 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 if lnaddress: - # for lnaddress - domain = urlparse(str(request.url)).netloc - link.domain = domain + # for lnaddress, we have to set this otherwise the metadata won't have the identifier + link.domain = urlparse(str(request.url)).netloc callback = request.url_for("lnurlp.api_lnurl_lnaddr_callback", link_id=link.id) else: callback = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id) From aadebddd82130846a1d2e04ea07f3a89219673c7 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:21:40 -0700 Subject: [PATCH 033/141] change LN address display label to username --- templates/lnurlp/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index c40a0b1..405ea3a 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -31,7 +31,7 @@ Description Amount Currency - LN address + Username From 8e5ed7d23d37eec78fde16c861d506f10e1cc343 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:26:06 +0100 Subject: [PATCH 034/141] show comment extra only when present --- lnurl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lnurl.py b/lnurl.py index d03faaa..0f94f54 100644 --- a/lnurl.py +++ b/lnurl.py @@ -71,10 +71,12 @@ async def api_lnurl_callback( extra = { "tag": "lnurlp", "link": link.id, - "comment": comment, "extra": request.query_params.get("amount"), } + if comment: + extra["comment"] = (comment,) + if lnaddress and link.username and link.domain: extra["lnaddress"] = f"{link.username}@{link.domain}" From 096190cfd2500750c6f532b100ea01f133f9347a Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:27:06 -0700 Subject: [PATCH 035/141] add label for lnaddress to qrcode html --- templates/lnurlp/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index 405ea3a..b5f5909 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -319,6 +319,7 @@ Dispatches webhook to: {{ qrCodeDialog.data.webhook }}
On success: {{ qrCodeDialog.data.success }}
+ Lightning Address: {{ qrCodeDialog.data.username}}

{% endraw %}
From b672a7710cfa75cf35d2624179e067feea99b826 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:35:31 -0700 Subject: [PATCH 036/141] add ln address to qr code --- static/js/index.js | 3 ++- templates/lnurlp/index.html | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 0075f28..c44c4ca 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -93,7 +93,8 @@ new Vue({ : 'do nothing', lnurl: link.lnurl, pay_url: link.pay_url, - print_url: link.print_url + print_url: link.print_url, + username: link.username } this.qrCodeDialog.show = true }, diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index b5f5909..adbcdb7 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -319,7 +319,10 @@ Dispatches webhook to: {{ qrCodeDialog.data.webhook }}
On success: {{ qrCodeDialog.data.success }}
- Lightning Address: {{ qrCodeDialog.data.username}}
+ + Lightning Address: {{ qrCodeDialog.data.username}}@{{domain}} +
+

{% endraw %}
From 11a9d02f9386df18d2e41c34ac700f5bff5a4487 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:45:22 +0100 Subject: [PATCH 037/141] fix missing username --- crud.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/crud.py b/crud.py index c8a96e4..aeb78d0 100644 --- a/crud.py +++ b/crud.py @@ -22,14 +22,13 @@ async def check_lnaddress_update(username: str, id: str) -> bool: return True -async def check_lnaddress_exists(username: str) -> bool: +async def check_lnaddress_not_exists(username: str) -> bool: # check if lnaddress username exists in the database when creating a new entry row = await db.fetchall( "SELECT username FROM lnurlp.pay_links WHERE username = ?", (username,) ) if row: assert False, "Username already exists. Try a different one." - return else: return True @@ -43,8 +42,10 @@ async def check_lnaddress_format(username: str) -> bool: async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: - await check_lnaddress_format(data.username) - await check_lnaddress_exists(data.username) + if data.username: + await check_lnaddress_format(data.username) + await check_lnaddress_not_exists(data.username) + link_id = urlsafe_short_hash()[:6] result = await db.execute( @@ -121,12 +122,10 @@ async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]: return [PayLink.from_row(row) for row in rows] -async def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]: - for field in kwargs.items(): - if field[0] == "lnaddress": - value = field[1] - await check_lnaddress_format(value) - await check_lnaddress_update(value, str(link_id)) +async def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]: + if "lnaddress" in kwargs: + await check_lnaddress_format(kwargs["lnaddress"]) + await check_lnaddress_update(kwargs["lnaddress"], link_id) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( @@ -136,7 +135,7 @@ async def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]: return PayLink.from_row(row) if row else None -async def increment_pay_link(link_id: int, **kwargs) -> Optional[PayLink]: +async def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]: q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()]) await db.execute( f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id) @@ -145,5 +144,5 @@ async def increment_pay_link(link_id: int, **kwargs) -> Optional[PayLink]: return PayLink.from_row(row) if row else None -async def delete_pay_link(link_id: int) -> None: +async def delete_pay_link(link_id: str) -> None: await db.execute("DELETE FROM lnurlp.pay_links WHERE id = ?", (link_id,)) From 9b7d96ca3d5aa09cfb75d89c811266547283faf6 Mon Sep 17 00:00:00 2001 From: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:46:09 -0700 Subject: [PATCH 038/141] add spacer for lnaddress --- templates/lnurlp/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index adbcdb7..1ecd490 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -166,11 +166,11 @@ dense v-model.trim="formDialog.data.username" type="text" - label="Lightning Username" + label="Lightning Address" >
- @{% raw %} {{domain}} {% endraw %} +   @ {% raw %} {{domain}} {% endraw %}
From c51b849fe688e3653d565d4d2830d4ef4634f122 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 15 Mar 2023 12:47:24 +0100 Subject: [PATCH 039/141] fix hostname --- static/js/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index c44c4ca..8edc055 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -9,8 +9,6 @@ var locationPath = [ window.location.pathname ].join('') -var domain = window.location.hostname - var mapPayLink = obj => { obj._data = _.clone(obj) obj.date = Quasar.utils.date.formatDate( @@ -28,7 +26,7 @@ new Vue({ mixins: [windowMixin], data() { return { - domain: domain, + domain: window.location.host, currencies: [], fiatRates: {}, checker: null, From 04d1494c9059da81d34df450a66195c981988796 Mon Sep 17 00:00:00 2001 From: arbadacarba <63317640+arbadacarbaYK@users.noreply.github.com> Date: Thu, 16 Mar 2023 14:16:30 +0100 Subject: [PATCH 040/141] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved Darth´ explanations from general info on updating to LNURLp cause it was very specialized to it --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8957dff..f450a66 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LNURLp -## Create a static QR code people can use to pay over Lightning Network +## Create a static QR code or LNaddress people can use to pay over Lightning Network LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL-pay is a link that wallets use to fetch an invoice from a server on-demand. The link or QR code is fixed, but each time it is read by a compatible wallet a new invoice is issued by the service and sent to the wallet. @@ -30,3 +30,30 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n - attach a username to your lnurlp to create a lightning address - the LN address format will be username@lnbits-domain-name - Find out more about the lightning address spec at lightningaddress.com + +## Update your LNURL-pay extension + +Now that the extensions are taken out of core LNbits we can update each extension separately without the need to reload or restart LNbits as a whole. +This new version of the extension will give you the option to add a Lightning Address to each LNURLpay link. + +- Open your LNbits instance as super admin (not as a regular user. You will find the SuperUser-ID in your server logs on restart of LNbits. Use that to bookmark and manage LNbits from there in the future.) +Now lets install the new version of a given extension like extensively [described in this guide](https://github.com/lnbits/lnbits/blob/main/docs/guide/extension-install.md#install-new-extension). In short: +- Go to "Mange extensions", click on "ALL", search for e.g. LNURLp, click on "Manage" +- Open the details of the extension and click on version 0.2.1, click "Install". You´re done! + +[![lnurl-p-1.jpg](https://i.postimg.cc/fTwDWD17/lnurl-p-1.jpg)](https://postimg.cc/xqFWtDfq) + +- Open the LNURLp extension from the left panel +- If you already have had some LNURLp defined, you can now click on edit and add a LN Address to each. _Note that this will change your QR-Code!_ +- If you didn't create any LNURLp before nothing changed except the window for defining new ones + +[![lnurl-p-ln-address.jpg](https://i.postimg.cc/rsQQc1tr/lnurl-p-ln-address.jpg)](https://postimg.cc/tnnhNVkq) + +Now you can receive sats to your newly created LN address. You will find this info also in the transaction overview for each payment (click on the green arrow). + +[![lnurl-details.jpg](https://i.postimg.cc/zDwq1V2X/lnurl-details.jpg)](https://postimg.cc/3WwsXJHP) + + + + + From fa3cb87ba0c3cd7030ca893936e6656d092a7eee Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 17:39:03 +0100 Subject: [PATCH 041/141] add zaps --- lnurl.py | 10 +++++++++- tasks.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/lnurl.py b/lnurl.py index 0f94f54..324cf9e 100644 --- a/lnurl.py +++ b/lnurl.py @@ -77,6 +77,10 @@ async def api_lnurl_callback( if comment: extra["comment"] = (comment,) + nostr = request.query_params.get("nostr") + if nostr: + extra["nostr"] = nostr + if lnaddress and link.username and link.domain: extra["lnaddress"] = f"{link.username}@{link.domain}" @@ -84,7 +88,7 @@ async def api_lnurl_callback( wallet_id=link.wallet, amount=int(amount_received / 1000), memo=link.description, - unhashed_description=link.lnurlpay_metadata.encode(), + unhashed_description=nostr.encode() or link.lnurlpay_metadata.encode(), extra=extra, ) @@ -136,4 +140,8 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): if link.comment_chars > 0: params["commentAllowed"] = link.comment_chars + params["allowNostr"] = True + params[ + "nostrPubkey" + ] = "749b4d4dfc6b00a5e6c9a88d8a220c46c069ff8f027dcf312f040475e059554a" # private: de1af06647137d49b2277faa86f96effc94257a7b7efd6f5dcc52bea08a4746b return params diff --git a/tasks.py b/tasks.py index ea01e04..7e7f62b 100644 --- a/tasks.py +++ b/tasks.py @@ -8,8 +8,10 @@ from lnbits.core.crud import update_payment_extra from lnbits.core.models import Payment from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener - +from websocket import WebSocketApp +from lnbits.settings import settings from .crud import get_pay_link +from threading import Thread async def wait_for_paid_invoices(): @@ -63,6 +65,56 @@ async def on_invoice_paid(payment: Payment): payment.payment_hash, -1, False, "Unexpected Error", str(ex) ) + nostr = payment.extra.get("nostr") + if nostr: + from ..nostrclient.nostr.event import Event + from ..nostrclient.nostr.key import PrivateKey, PublicKey + + event_json = json.loads(nostr) + + def get_tag(event_json, tag): + res = [ + event_tag[1] for event_tag in event_json["tags"] if event_tag[0] == tag + ] + return res[0] if res else None + + private_key = PrivateKey( + bytes.fromhex( + "de1af06647137d49b2277faa86f96effc94257a7b7efd6f5dcc52bea08a4746b" + ) + ) + + p_tag = get_tag(event_json, "p") + tags = [] + for t in ["p", "e"]: + tag = get_tag(event_json, t) + if tag: + tags.append([t, tag]) + tags.append(["bolt11", payment.bolt11]) + tags.append(["description", json.dumps(event_json)]) + zap_receipt = Event( + public_key="749b4d4dfc6b00a5e6c9a88d8a220c46c069ff8f027dcf312f040475e059554a", + kind=9735, + tags=tags, + ) + private_key.sign_event(zap_receipt) + + print(f"NOSTR STUFF: {event_json}") + print(f"Receipt: {zap_receipt}") + + def send_event(class_obj): + ws.send(zap_receipt.to_message()) + # nonlocal wst + # wst.join(timeout=1) + + ws = WebSocketApp( + f"wss://localhost:{settings.port}/nostrclient/api/v1/relay", + on_open=send_event, + ) + wst = Thread(target=ws.run_forever) + wst.daemon = True + wst.start() + async def mark_webhook_sent( payment_hash: str, status: int, is_success: bool, reason_phrase="", text="" From 6fb0a47ad410f28b1254c1dc241bf5316e903b69 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 17:43:11 +0100 Subject: [PATCH 042/141] unbreak lnurlp --- lnurl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lnurl.py b/lnurl.py index 324cf9e..2d4ff9e 100644 --- a/lnurl.py +++ b/lnurl.py @@ -88,7 +88,9 @@ async def api_lnurl_callback( wallet_id=link.wallet, amount=int(amount_received / 1000), memo=link.description, - unhashed_description=nostr.encode() or link.lnurlpay_metadata.encode(), + unhashed_description=nostr.encode() + if nostr + else link.lnurlpay_metadata.encode(), extra=extra, ) From d8e742a45208477e512f93be2dfb1cf2e75ae892 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 17:48:49 +0100 Subject: [PATCH 043/141] ws not wss --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 7e7f62b..5496642 100644 --- a/tasks.py +++ b/tasks.py @@ -108,7 +108,7 @@ async def on_invoice_paid(payment: Payment): # wst.join(timeout=1) ws = WebSocketApp( - f"wss://localhost:{settings.port}/nostrclient/api/v1/relay", + f"ws://localhost:{settings.port}/nostrclient/api/v1/relay", on_open=send_event, ) wst = Thread(target=ws.run_forever) From bf22efdd32ab8916e11b7c143618e22878c6adb9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 18:27:37 +0100 Subject: [PATCH 044/141] fix stuff --- tasks.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tasks.py b/tasks.py index 5496642..a15ad5e 100644 --- a/tasks.py +++ b/tasks.py @@ -92,17 +92,10 @@ async def on_invoice_paid(payment: Payment): tags.append([t, tag]) tags.append(["bolt11", payment.bolt11]) tags.append(["description", json.dumps(event_json)]) - zap_receipt = Event( - public_key="749b4d4dfc6b00a5e6c9a88d8a220c46c069ff8f027dcf312f040475e059554a", - kind=9735, - tags=tags, - ) + zap_receipt = Event(kind=9735, tags=tags, content="asd") private_key.sign_event(zap_receipt) - print(f"NOSTR STUFF: {event_json}") - print(f"Receipt: {zap_receipt}") - - def send_event(class_obj): + def send_event(_): ws.send(zap_receipt.to_message()) # nonlocal wst # wst.join(timeout=1) From cdf137b484629d6df90e2383808fea36aec7d68a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 19:02:20 +0100 Subject: [PATCH 045/141] description --- lnurl.py | 3 ++- tasks.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lnurl.py b/lnurl.py index 2d4ff9e..f8679db 100644 --- a/lnurl.py +++ b/lnurl.py @@ -11,6 +11,7 @@ from . import lnurlp_ext from .crud import increment_pay_link, get_pay_link, get_address_data from loguru import logger from urllib.parse import urlparse +import json @lnurlp_ext.get( @@ -88,7 +89,7 @@ async def api_lnurl_callback( wallet_id=link.wallet, amount=int(amount_received / 1000), memo=link.description, - unhashed_description=nostr.encode() + unhashed_description=json.dumps(nostr).encode() if nostr else link.lnurlpay_metadata.encode(), extra=extra, diff --git a/tasks.py b/tasks.py index a15ad5e..307be12 100644 --- a/tasks.py +++ b/tasks.py @@ -91,14 +91,12 @@ async def on_invoice_paid(payment: Payment): if tag: tags.append([t, tag]) tags.append(["bolt11", payment.bolt11]) - tags.append(["description", json.dumps(event_json)]) + tags.append(["description", nostr]) zap_receipt = Event(kind=9735, tags=tags, content="asd") private_key.sign_event(zap_receipt) def send_event(_): ws.send(zap_receipt.to_message()) - # nonlocal wst - # wst.join(timeout=1) ws = WebSocketApp( f"ws://localhost:{settings.port}/nostrclient/api/v1/relay", From 2e8b6070dc84229d510bf2a30a9f9b3608de457f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 19:22:45 +0100 Subject: [PATCH 046/141] zaaaarpp --- lnurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnurl.py b/lnurl.py index f8679db..8c4a5e1 100644 --- a/lnurl.py +++ b/lnurl.py @@ -89,7 +89,7 @@ async def api_lnurl_callback( wallet_id=link.wallet, amount=int(amount_received / 1000), memo=link.description, - unhashed_description=json.dumps(nostr).encode() + unhashed_description=nostr.encode() if nostr else link.lnurlpay_metadata.encode(), extra=extra, From 0cbee4dc6cd830c4b2be1563300c16769b462a11 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 19:37:44 +0100 Subject: [PATCH 047/141] I hate you python --- lnurl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lnurl.py b/lnurl.py index 8c4a5e1..039e2a6 100644 --- a/lnurl.py +++ b/lnurl.py @@ -85,11 +85,12 @@ async def api_lnurl_callback( if lnaddress and link.username and link.domain: extra["lnaddress"] = f"{link.username}@{link.domain}" + nostr_description = json.dumps(nostr)[1:-1] # remove leading and trailing " payment_hash, payment_request = await create_invoice( wallet_id=link.wallet, amount=int(amount_received / 1000), memo=link.description, - unhashed_description=nostr.encode() + unhashed_description=nostr_description.encode() if nostr else link.lnurlpay_metadata.encode(), extra=extra, From 54ca4476ccf5e84c5b9526197aa37d096cf506ec Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 21 Mar 2023 19:50:32 +0100 Subject: [PATCH 048/141] debug printing --- lnurl.py | 5 +++++ tasks.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/lnurl.py b/lnurl.py index 039e2a6..7f08760 100644 --- a/lnurl.py +++ b/lnurl.py @@ -85,7 +85,12 @@ async def api_lnurl_callback( if lnaddress and link.username and link.domain: extra["lnaddress"] = f"{link.username}@{link.domain}" + print("HASHING THIS") nostr_description = json.dumps(nostr)[1:-1] # remove leading and trailing " + print(nostr_description) + import hashlib + + print(hashlib.sha256(nostr_description.encode()).hexdigest()) payment_hash, payment_request = await create_invoice( wallet_id=link.wallet, amount=int(amount_received / 1000), diff --git a/tasks.py b/tasks.py index 307be12..79385e9 100644 --- a/tasks.py +++ b/tasks.py @@ -98,6 +98,11 @@ async def on_invoice_paid(payment: Payment): def send_event(_): ws.send(zap_receipt.to_message()) + from lnbits.core import bolt11 + + print( + f"Invoice description hash: {bolt11.decode(payment.bolt11).description_hash}" + ) ws = WebSocketApp( f"ws://localhost:{settings.port}/nostrclient/api/v1/relay", on_open=send_event, From 50f9b505cd6880c41cf23367a5c1370a41a58447 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 22 Mar 2023 00:47:37 +0100 Subject: [PATCH 049/141] allowNostr vs alllowsNostr --- lnurl.py | 27 ++++++++++++++------------- tasks.py | 7 ++++++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lnurl.py b/lnurl.py index 7f08760..37c22f2 100644 --- a/lnurl.py +++ b/lnurl.py @@ -48,15 +48,15 @@ async def api_lnurl_callback( min = link.min * 1000 max = link.max * 1000 - amount_received = amount - if amount_received < min: + amount = amount + if amount < min: return LnurlErrorResponse( - reason=f"Amount {amount_received} is smaller than minimum {min}." + reason=f"Amount {amount} is smaller than minimum {min}." ).dict() - elif amount_received > max: + elif amount > max: return LnurlErrorResponse( - reason=f"Amount {amount_received} is greater than maximum {max}." + reason=f"Amount {amount} is greater than maximum {max}." ).dict() comment = request.query_params.get("comment") @@ -79,21 +79,22 @@ async def api_lnurl_callback( extra["comment"] = (comment,) nostr = request.query_params.get("nostr") + nostr_description = "" if nostr: extra["nostr"] = nostr + # print("HASHING THIS") + nostr_description = json.dumps(nostr)[1:-1] # remove leading and trailing " + # print(nostr_description) + import hashlib + + print(hashlib.sha256(nostr_description.encode()).hexdigest()) if lnaddress and link.username and link.domain: extra["lnaddress"] = f"{link.username}@{link.domain}" - print("HASHING THIS") - nostr_description = json.dumps(nostr)[1:-1] # remove leading and trailing " - print(nostr_description) - import hashlib - - print(hashlib.sha256(nostr_description.encode()).hexdigest()) payment_hash, payment_request = await create_invoice( wallet_id=link.wallet, - amount=int(amount_received / 1000), + amount=int(amount / 1000), memo=link.description, unhashed_description=nostr_description.encode() if nostr @@ -149,7 +150,7 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): if link.comment_chars > 0: params["commentAllowed"] = link.comment_chars - params["allowNostr"] = True + params["allowsNostr"] = True params[ "nostrPubkey" ] = "749b4d4dfc6b00a5e6c9a88d8a220c46c069ff8f027dcf312f040475e059554a" # private: de1af06647137d49b2277faa86f96effc94257a7b7efd6f5dcc52bea08a4746b diff --git a/tasks.py b/tasks.py index 79385e9..698658f 100644 --- a/tasks.py +++ b/tasks.py @@ -65,6 +65,7 @@ async def on_invoice_paid(payment: Payment): payment.payment_hash, -1, False, "Unexpected Error", str(ex) ) + # NIP-57 nostr = payment.extra.get("nostr") if nostr: from ..nostrclient.nostr.event import Event @@ -92,11 +93,15 @@ async def on_invoice_paid(payment: Payment): tags.append([t, tag]) tags.append(["bolt11", payment.bolt11]) tags.append(["description", nostr]) - zap_receipt = Event(kind=9735, tags=tags, content="asd") + zap_receipt = Event( + kind=9735, tags=tags, content=payment.extra.get("comment") or "" + ) private_key.sign_event(zap_receipt) def send_event(_): + # logger.debug(f"Sending zap: {zap_receipt.to_message()}") ws.send(zap_receipt.to_message()) + ws.close() from lnbits.core import bolt11 From b66588d7c9c1b6586e339da4a8633e718ae7c87b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 22 Mar 2023 01:18:17 +0100 Subject: [PATCH 050/141] escape --- lnurl.py | 2 +- tasks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lnurl.py b/lnurl.py index 37c22f2..13ec543 100644 --- a/lnurl.py +++ b/lnurl.py @@ -83,7 +83,7 @@ async def api_lnurl_callback( if nostr: extra["nostr"] = nostr # print("HASHING THIS") - nostr_description = json.dumps(nostr)[1:-1] # remove leading and trailing " + nostr_description = nostr # json.dumps(nostr)[1:-1] # json.dumps supposedly makes escaped "-s, remove leading and trailing " # print(nostr_description) import hashlib diff --git a/tasks.py b/tasks.py index 698658f..670f38a 100644 --- a/tasks.py +++ b/tasks.py @@ -99,7 +99,7 @@ async def on_invoice_paid(payment: Payment): private_key.sign_event(zap_receipt) def send_event(_): - # logger.debug(f"Sending zap: {zap_receipt.to_message()}") + logger.debug(f"Sending zap: {zap_receipt.to_message()}") ws.send(zap_receipt.to_message()) ws.close() From 022a5b79bb217e69f65898b93774a4f6486757f1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:52:47 +0100 Subject: [PATCH 051/141] refactor --- services.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 services.py diff --git a/services.py b/services.py new file mode 100644 index 0000000..641ffce --- /dev/null +++ b/services.py @@ -0,0 +1,9 @@ +import re + + +async def check_lnaddress_format(username: str) -> bool: + # check username complies with lnaddress specification + if not re.match("^[a-z0-9-_.]{3,15}$", username): + assert False, "Only letters a-z0-9-_. allowed, min 3 and max 15 characters!" + return + return True From 8c5a494489e7ea2f13cce234ee5b940c1de8fa63 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:53:02 +0100 Subject: [PATCH 052/141] refactor --- crud.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/crud.py b/crud.py index aeb78d0..d708e93 100644 --- a/crud.py +++ b/crud.py @@ -5,6 +5,7 @@ from lnbits.helpers import urlsafe_short_hash from . import db # , maindb from .models import CreatePayLinkData, PayLink +from .services import check_lnaddress_format # from loguru import logger @@ -15,9 +16,8 @@ async def check_lnaddress_update(username: str, id: str) -> bool: "SELECT username FROM lnurlp.pay_links WHERE username = ? AND id = ?", (username, id), ) - if len(row) > 1: - assert False, "Username already exists. Try a different one." - return + if row: + raise Exception("Username already exists. Try a different one.") else: return True @@ -28,19 +28,11 @@ async def check_lnaddress_not_exists(username: str) -> bool: "SELECT username FROM lnurlp.pay_links WHERE username = ?", (username,) ) if row: - assert False, "Username already exists. Try a different one." + raise Exception("Username already exists. Try a different one.") else: return True -async def check_lnaddress_format(username: str) -> bool: - # check username complies with lnaddress specification - if not re.match("^[a-z0-9-_.]{3,15}$", username): - assert False, "Only letters a-z0-9-_. allowed, min 3 and max 15 characters!" - return - return True - - async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: if data.username: await check_lnaddress_format(data.username) From f2615aa155348bebdb1dda519368eaece6f0a095 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:53:23 +0100 Subject: [PATCH 053/141] clean --- lnurl.py | 14 ++++---------- tasks.py | 5 ----- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/lnurl.py b/lnurl.py index 13ec543..3175a85 100644 --- a/lnurl.py +++ b/lnurl.py @@ -78,16 +78,10 @@ async def api_lnurl_callback( if comment: extra["comment"] = (comment,) + # nip 57 nostr = request.query_params.get("nostr") - nostr_description = "" if nostr: - extra["nostr"] = nostr - # print("HASHING THIS") - nostr_description = nostr # json.dumps(nostr)[1:-1] # json.dumps supposedly makes escaped "-s, remove leading and trailing " - # print(nostr_description) - import hashlib - - print(hashlib.sha256(nostr_description.encode()).hexdigest()) + extra["nostr"] = nostr # put it here for later publishing in tasks.py if lnaddress and link.username and link.domain: extra["lnaddress"] = f"{link.username}@{link.domain}" @@ -96,8 +90,8 @@ async def api_lnurl_callback( wallet_id=link.wallet, amount=int(amount / 1000), memo=link.description, - unhashed_description=nostr_description.encode() - if nostr + unhashed_description=nostr.encode() + if nostr # we take the zap request as the description instead of the LNURL metadata if present else link.lnurlpay_metadata.encode(), extra=extra, ) diff --git a/tasks.py b/tasks.py index 670f38a..0526d62 100644 --- a/tasks.py +++ b/tasks.py @@ -103,11 +103,6 @@ async def on_invoice_paid(payment: Payment): ws.send(zap_receipt.to_message()) ws.close() - from lnbits.core import bolt11 - - print( - f"Invoice description hash: {bolt11.decode(payment.bolt11).description_hash}" - ) ws = WebSocketApp( f"ws://localhost:{settings.port}/nostrclient/api/v1/relay", on_open=send_event, From c2e58fa1b438afcc9b4f9fa92b9ac7e666d2e7e6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 22 Mar 2023 17:49:43 +0100 Subject: [PATCH 054/141] generate new keys --- __init__.py | 20 ++++++++++++++++---- lnurl.py | 14 ++++++++++---- tasks.py | 21 ++++++++++----------- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/__init__.py b/__init__.py index 4ef4c7d..7a466ba 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,18 @@ from fastapi.staticfiles import StaticFiles from lnbits.db import Database from lnbits.helpers import template_renderer from lnbits.tasks import catch_everything_and_restart +from loguru import logger + +try: + from ..nostrclient.nostr.event import Event + from ..nostrclient.nostr.key import PrivateKey, PublicKey + + nostrclient_present = True + nostr_privatekey = PrivateKey() + nostr_publickey: PublicKey = nostr_privatekey.public_key + logger.debug(f"LNURLP Zaps Nostr pubkey: {nostr_publickey.hex()}") +except ImportError: + nostrclient_present = False db = Database("ext_lnurlp") @@ -19,10 +31,10 @@ lnurlp_static_files = [ ] lnurlp_redirect_paths = [ - { - "from_path": "/.well-known/lnurlp", - "redirect_to_path": "/api/v1/well-known", - } + { + "from_path": "/.well-known/lnurlp", + "redirect_to_path": "/api/v1/well-known", + } ] scheduled_tasks: List[asyncio.Task] = [] diff --git a/lnurl.py b/lnurl.py index 3175a85..00ca97d 100644 --- a/lnurl.py +++ b/lnurl.py @@ -12,6 +12,13 @@ from .crud import increment_pay_link, get_pay_link, get_address_data from loguru import logger from urllib.parse import urlparse import json +from . import nostrclient_present, nostr_publickey + +if nostrclient_present: + try: + from ..nostrclient.nostr.key import PrivateKey, PublicKey + except ImportError: + nostrclient_present = False @lnurlp_ext.get( @@ -144,8 +151,7 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): if link.comment_chars > 0: params["commentAllowed"] = link.comment_chars - params["allowsNostr"] = True - params[ - "nostrPubkey" - ] = "749b4d4dfc6b00a5e6c9a88d8a220c46c069ff8f027dcf312f040475e059554a" # private: de1af06647137d49b2277faa86f96effc94257a7b7efd6f5dcc52bea08a4746b + if nostrclient_present: + params["allowsNostr"] = True + params["nostrPubkey"] = nostr_publickey.hex() return params diff --git a/tasks.py b/tasks.py index 0526d62..e6abbeb 100644 --- a/tasks.py +++ b/tasks.py @@ -12,6 +12,14 @@ from websocket import WebSocketApp from lnbits.settings import settings from .crud import get_pay_link from threading import Thread +from . import nostrclient_present, nostr_privatekey + +if nostrclient_present: + try: + from ..nostrclient.nostr.event import Event + from ..nostrclient.nostr.key import PrivateKey, PublicKey + except ImportError: + nostrclient_present = False async def wait_for_paid_invoices(): @@ -67,9 +75,7 @@ async def on_invoice_paid(payment: Payment): # NIP-57 nostr = payment.extra.get("nostr") - if nostr: - from ..nostrclient.nostr.event import Event - from ..nostrclient.nostr.key import PrivateKey, PublicKey + if nostrclient_present and nostr: event_json = json.loads(nostr) @@ -79,13 +85,6 @@ async def on_invoice_paid(payment: Payment): ] return res[0] if res else None - private_key = PrivateKey( - bytes.fromhex( - "de1af06647137d49b2277faa86f96effc94257a7b7efd6f5dcc52bea08a4746b" - ) - ) - - p_tag = get_tag(event_json, "p") tags = [] for t in ["p", "e"]: tag = get_tag(event_json, t) @@ -96,7 +95,7 @@ async def on_invoice_paid(payment: Payment): zap_receipt = Event( kind=9735, tags=tags, content=payment.extra.get("comment") or "" ) - private_key.sign_event(zap_receipt) + nostr_privatekey.sign_event(zap_receipt) def send_event(_): logger.debug(f"Sending zap: {zap_receipt.to_message()}") From e119f5c4c52c3e00f6283d9db14503a06acd4ac2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 23 Mar 2023 23:32:27 +0100 Subject: [PATCH 055/141] send zaps to relays in zap request --- tasks.py | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/tasks.py b/tasks.py index e6abbeb..39af433 100644 --- a/tasks.py +++ b/tasks.py @@ -13,6 +13,7 @@ from lnbits.settings import settings from .crud import get_pay_link from threading import Thread from . import nostrclient_present, nostr_privatekey +from typing import List if nostrclient_present: try: @@ -81,7 +82,7 @@ async def on_invoice_paid(payment: Payment): def get_tag(event_json, tag): res = [ - event_tag[1] for event_tag in event_json["tags"] if event_tag[0] == tag + event_tag[1:] for event_tag in event_json["tags"] if event_tag[0] == tag ] return res[0] if res else None @@ -89,7 +90,7 @@ async def on_invoice_paid(payment: Payment): for t in ["p", "e"]: tag = get_tag(event_json, t) if tag: - tags.append([t, tag]) + tags.append([t, tag[0]]) tags.append(["bolt11", payment.bolt11]) tags.append(["description", nostr]) zap_receipt = Event( @@ -97,18 +98,41 @@ async def on_invoice_paid(payment: Payment): ) nostr_privatekey.sign_event(zap_receipt) - def send_event(_): - logger.debug(f"Sending zap: {zap_receipt.to_message()}") - ws.send(zap_receipt.to_message()) - ws.close() + def send_zap(relay): + def send_event(_): + logger.debug(f"Sending zap to {ws.url}") + ws.send(zap_receipt.to_message()) + ws.close() - ws = WebSocketApp( - f"ws://localhost:{settings.port}/nostrclient/api/v1/relay", - on_open=send_event, - ) - wst = Thread(target=ws.run_forever) - wst.daemon = True - wst.start() + ws = WebSocketApp(relay, on_open=send_event) + wst = Thread(target=ws.run_forever, name=f"LNURL zap {relay}") + wst.daemon = True + wst.start() + return ws, wst + + # list of all websockets + wss: List[WebSocketApp] = [] + # list of all threads for these websockets + wsts: List[Thread] = [] + + # send zap via nostrclient + ws, wst = send_zap(f"ws://localhost:{settings.port}/nostrclient/api/v1/relay") + wss += [ws] + wsts += [wst] + + # send zap receipt to relays in zap request + relays = get_tag(event_json, "relays") + if relays: + for i, r in enumerate(relays): + ws, wst = send_zap(r) + wss += [ws] + wsts += [wst] + + await asyncio.sleep(10) + for ws, wst in zip(wss, wsts): + logger.debug(f"Closing websocket {ws.url}") + ws.close() + wst.join() async def mark_webhook_sent( From f755a441080135e09641bde83fd5b8cfcf56b89f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 23 Mar 2023 23:46:16 +0100 Subject: [PATCH 056/141] sleep a bit --- tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks.py b/tasks.py index 39af433..715823b 100644 --- a/tasks.py +++ b/tasks.py @@ -14,6 +14,7 @@ from .crud import get_pay_link from threading import Thread from . import nostrclient_present, nostr_privatekey from typing import List +import time if nostrclient_present: try: @@ -102,6 +103,7 @@ async def on_invoice_paid(payment: Payment): def send_event(_): logger.debug(f"Sending zap to {ws.url}") ws.send(zap_receipt.to_message()) + time.sleep(2) ws.close() ws = WebSocketApp(relay, on_open=send_event) From 642cad9ed8972eb23b1f5bc6f80c933b22a9444d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 25 Mar 2023 03:08:54 +0100 Subject: [PATCH 057/141] add max relays --- tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 715823b..12fdfa4 100644 --- a/tasks.py +++ b/tasks.py @@ -125,7 +125,9 @@ async def on_invoice_paid(payment: Payment): # send zap receipt to relays in zap request relays = get_tag(event_json, "relays") if relays: - for i, r in enumerate(relays): + if len(relays) > 50: + relays = relays[:50] + for r in relays: ws, wst = send_zap(r) wss += [ws] wsts += [wst] From bf6659bbbdfa486211bbf5ffe883f4e2e74c6d1e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 4 Apr 2023 12:52:10 +0200 Subject: [PATCH 058/141] remove the need for nostrclient --- README.md | 1 + __init__.py | 15 ++++++--------- config.json | 8 ++------ lnurl.py | 13 ++++--------- manifest.json | 15 +++++++-------- tasks.py | 20 ++++++++------------ 6 files changed, 28 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 8957dff..fc683b8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ LNURL is a range of lightning-network standards that allow us to use lightning-n 2. Use the shareable link or view the LNURLp you just created\ ![LNURLp](https://i.imgur.com/C8s1P0Q.jpg) + - you can now open your LNURLp and copy the LNURL, get the shareable link or print it\ ![view lnurlp](https://i.imgur.com/4n41S7T.jpg) diff --git a/__init__.py b/__init__.py index 7a466ba..0c69f78 100644 --- a/__init__.py +++ b/__init__.py @@ -9,16 +9,13 @@ from lnbits.helpers import template_renderer from lnbits.tasks import catch_everything_and_restart from loguru import logger -try: - from ..nostrclient.nostr.event import Event - from ..nostrclient.nostr.key import PrivateKey, PublicKey - nostrclient_present = True - nostr_privatekey = PrivateKey() - nostr_publickey: PublicKey = nostr_privatekey.public_key - logger.debug(f"LNURLP Zaps Nostr pubkey: {nostr_publickey.hex()}") -except ImportError: - nostrclient_present = False +from .nostr.event import Event +from .nostr.key import PrivateKey, PublicKey + +nostr_privatekey = PrivateKey() +nostr_publickey: PublicKey = nostr_privatekey.public_key +logger.debug(f"LNURLP Zaps Nostr pubkey: {nostr_publickey.hex()}") db = Database("ext_lnurlp") diff --git a/config.json b/config.json index d3e046d..8e0ee88 100644 --- a/config.json +++ b/config.json @@ -1,10 +1,6 @@ { "name": "LNURLp", "short_description": "Make reusable LNURL pay links", - "tile": "/lnurlp/static/image/lnurl-pay.png", - "contributors": [ - "arcbtc", - "eillarra", - "fiatjaf" - ] + "tile": "/lnurlp/static/image/lnurl-pay.png", + "contributors": ["arcbtc", "eillarra", "fiatjaf", "callebtc"] } diff --git a/lnurl.py b/lnurl.py index 00ca97d..38fd911 100644 --- a/lnurl.py +++ b/lnurl.py @@ -12,13 +12,9 @@ from .crud import increment_pay_link, get_pay_link, get_address_data from loguru import logger from urllib.parse import urlparse import json -from . import nostrclient_present, nostr_publickey +from . import nostr_publickey -if nostrclient_present: - try: - from ..nostrclient.nostr.key import PrivateKey, PublicKey - except ImportError: - nostrclient_present = False +from ..nostrclient.nostr.key import PrivateKey, PublicKey @lnurlp_ext.get( @@ -151,7 +147,6 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): if link.comment_chars > 0: params["commentAllowed"] = link.comment_chars - if nostrclient_present: - params["allowsNostr"] = True - params["nostrPubkey"] = nostr_publickey.hex() + params["allowsNostr"] = True + params["nostrPubkey"] = nostr_publickey.hex() return params diff --git a/manifest.json b/manifest.json index 508a576..cc0c3d2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,9 @@ - { - "repos": [ - { - "id": "lnurlp", - "organisation": "lnbits", - "repository": "lnurlp" - } - ] + "repos": [ + { + "id": "lnurlp", + "organisation": "lnbits", + "repository": "lnurlp" + } + ] } diff --git a/tasks.py b/tasks.py index 12fdfa4..ca77b16 100644 --- a/tasks.py +++ b/tasks.py @@ -12,16 +12,12 @@ from websocket import WebSocketApp from lnbits.settings import settings from .crud import get_pay_link from threading import Thread -from . import nostrclient_present, nostr_privatekey +from . import nostr_privatekey from typing import List import time -if nostrclient_present: - try: - from ..nostrclient.nostr.event import Event - from ..nostrclient.nostr.key import PrivateKey, PublicKey - except ImportError: - nostrclient_present = False +from .nostr.event import Event +from .nostr.key import PrivateKey, PublicKey async def wait_for_paid_invoices(): @@ -77,7 +73,7 @@ async def on_invoice_paid(payment: Payment): # NIP-57 nostr = payment.extra.get("nostr") - if nostrclient_present and nostr: + if nostr: event_json = json.loads(nostr) @@ -117,10 +113,10 @@ async def on_invoice_paid(payment: Payment): # list of all threads for these websockets wsts: List[Thread] = [] - # send zap via nostrclient - ws, wst = send_zap(f"ws://localhost:{settings.port}/nostrclient/api/v1/relay") - wss += [ws] - wsts += [wst] + # # send zap via nostrclient + # ws, wst = send_zap(f"ws://localhost:{settings.port}/nostrclient/api/v1/relay") + # wss += [ws] + # wsts += [wst] # send zap receipt to relays in zap request relays = get_tag(event_json, "relays") From db607e463ea5f1b48c84a825571992150bc661a3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 4 Apr 2023 12:52:34 +0200 Subject: [PATCH 059/141] add event signing etc to extension directly --- nostr/bech32.py | 137 +++++++++++++++++++++++++++++++++++++++ nostr/event.py | 126 ++++++++++++++++++++++++++++++++++++ nostr/key.py | 147 ++++++++++++++++++++++++++++++++++++++++++ nostr/message_type.py | 15 +++++ 4 files changed, 425 insertions(+) create mode 100644 nostr/bech32.py create mode 100644 nostr/event.py create mode 100644 nostr/key.py create mode 100644 nostr/message_type.py diff --git a/nostr/bech32.py b/nostr/bech32.py new file mode 100644 index 0000000..b068de7 --- /dev/null +++ b/nostr/bech32.py @@ -0,0 +1,137 @@ +# Copyright (c) 2017, 2020 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" + + +from enum import Enum + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2bc830a3 + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data, spec) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + +def bech32_decode(bech): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + spec = bech32_verify_checksum(hrp, data) + if spec is None: + return (None, None, None) + return (hrp, data[:-6], spec) + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) + if decode(hrp, ret) == (None, None): + return None + return ret diff --git a/nostr/event.py b/nostr/event.py new file mode 100644 index 0000000..b903e0e --- /dev/null +++ b/nostr/event.py @@ -0,0 +1,126 @@ +import time +import json +from dataclasses import dataclass, field +from enum import IntEnum +from typing import List +from secp256k1 import PublicKey +from hashlib import sha256 + +from .message_type import ClientMessageType + + +class EventKind(IntEnum): + SET_METADATA = 0 + TEXT_NOTE = 1 + RECOMMEND_RELAY = 2 + CONTACTS = 3 + ENCRYPTED_DIRECT_MESSAGE = 4 + DELETE = 5 + + +@dataclass +class Event: + content: str = None + public_key: str = None + created_at: int = None + kind: int = EventKind.TEXT_NOTE + tags: List[List[str]] = field( + default_factory=list + ) # Dataclasses require special handling when the default value is a mutable type + signature: str = None + + def __post_init__(self): + if self.content is not None and not isinstance(self.content, str): + # DMs initialize content to None but all other kinds should pass in a str + raise TypeError("Argument 'content' must be of type str") + + if self.created_at is None: + self.created_at = int(time.time()) + + @staticmethod + def serialize( + public_key: str, created_at: int, kind: int, tags: List[List[str]], content: str + ) -> bytes: + data = [0, public_key, created_at, kind, tags, content] + data_str = json.dumps(data, separators=(",", ":"), ensure_ascii=False) + return data_str.encode() + + @staticmethod + def compute_id( + public_key: str, created_at: int, kind: int, tags: List[List[str]], content: str + ): + return sha256( + Event.serialize(public_key, created_at, kind, tags, content) + ).hexdigest() + + @property + def id(self) -> str: + # Always recompute the id to reflect the up-to-date state of the Event + return Event.compute_id( + self.public_key, self.created_at, self.kind, self.tags, self.content + ) + + def add_pubkey_ref(self, pubkey: str): + """Adds a reference to a pubkey as a 'p' tag""" + self.tags.append(["p", pubkey]) + + def add_event_ref(self, event_id: str): + """Adds a reference to an event_id as an 'e' tag""" + self.tags.append(["e", event_id]) + + def verify(self) -> bool: + pub_key = PublicKey( + bytes.fromhex("02" + self.public_key), True + ) # add 02 for schnorr (bip340) + return pub_key.schnorr_verify( + bytes.fromhex(self.id), bytes.fromhex(self.signature), None, raw=True + ) + + def to_message(self) -> str: + return json.dumps( + [ + ClientMessageType.EVENT, + { + "id": self.id, + "pubkey": self.public_key, + "created_at": self.created_at, + "kind": self.kind, + "tags": self.tags, + "content": self.content, + "sig": self.signature, + }, + ] + ) + + +@dataclass +class EncryptedDirectMessage(Event): + recipient_pubkey: str = None + cleartext_content: str = None + reference_event_id: str = None + + def __post_init__(self): + if self.content is not None: + self.cleartext_content = self.content + self.content = None + + if self.recipient_pubkey is None: + raise Exception("Must specify a recipient_pubkey.") + + self.kind = EventKind.ENCRYPTED_DIRECT_MESSAGE + super().__post_init__() + + # Must specify the DM recipient's pubkey in a 'p' tag + self.add_pubkey_ref(self.recipient_pubkey) + + # Optionally specify a reference event (DM) this is a reply to + if self.reference_event_id is not None: + self.add_event_ref(self.reference_event_id) + + @property + def id(self) -> str: + if self.content is None: + raise Exception( + "EncryptedDirectMessage `id` is undefined until its message is encrypted and stored in the `content` field" + ) + return super().id diff --git a/nostr/key.py b/nostr/key.py new file mode 100644 index 0000000..4adb0b9 --- /dev/null +++ b/nostr/key.py @@ -0,0 +1,147 @@ +import secrets +import base64 +import secp256k1 +from cffi import FFI +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from hashlib import sha256 + +from .event import EncryptedDirectMessage, Event, EventKind +from . import bech32 + + +class PublicKey: + def __init__(self, raw_bytes: bytes) -> None: + self.raw_bytes = raw_bytes + + def bech32(self) -> str: + converted_bits = bech32.convertbits(self.raw_bytes, 8, 5) + return bech32.bech32_encode("npub", converted_bits, bech32.Encoding.BECH32) + + def hex(self) -> str: + return self.raw_bytes.hex() + + def verify_signed_message_hash(self, hash: str, sig: str) -> bool: + pk = secp256k1.PublicKey(b"\x02" + self.raw_bytes, True) + return pk.schnorr_verify(bytes.fromhex(hash), bytes.fromhex(sig), None, True) + + @classmethod + def from_npub(cls, npub: str): + """Load a PublicKey from its bech32/npub form""" + hrp, data, spec = bech32.bech32_decode(npub) + raw_public_key = bech32.convertbits(data, 5, 8)[:-1] + return cls(bytes(raw_public_key)) + + +class PrivateKey: + def __init__(self, raw_secret: bytes = None) -> None: + if not raw_secret is None: + self.raw_secret = raw_secret + else: + self.raw_secret = secrets.token_bytes(32) + + sk = secp256k1.PrivateKey(self.raw_secret) + self.public_key = PublicKey(sk.pubkey.serialize()[1:]) + + @classmethod + def from_nsec(cls, nsec: str): + """Load a PrivateKey from its bech32/nsec form""" + hrp, data, spec = bech32.bech32_decode(nsec) + raw_secret = bech32.convertbits(data, 5, 8)[:-1] + return cls(bytes(raw_secret)) + + def bech32(self) -> str: + converted_bits = bech32.convertbits(self.raw_secret, 8, 5) + return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32) + + def hex(self) -> str: + return self.raw_secret.hex() + + def tweak_add(self, scalar: bytes) -> bytes: + sk = secp256k1.PrivateKey(self.raw_secret) + return sk.tweak_add(scalar) + + def compute_shared_secret(self, public_key_hex: str) -> bytes: + pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True) + return pk.ecdh(self.raw_secret, hashfn=copy_x) + + def encrypt_message(self, message: str, public_key_hex: str) -> str: + padder = padding.PKCS7(128).padder() + padded_data = padder.update(message.encode()) + padder.finalize() + + iv = secrets.token_bytes(16) + cipher = Cipher( + algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv) + ) + + encryptor = cipher.encryptor() + encrypted_message = encryptor.update(padded_data) + encryptor.finalize() + + return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}" + + def encrypt_dm(self, dm: EncryptedDirectMessage) -> None: + dm.content = self.encrypt_message( + message=dm.cleartext_content, public_key_hex=dm.recipient_pubkey + ) + + def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str: + encoded_data = encoded_message.split("?iv=") + encoded_content, encoded_iv = encoded_data[0], encoded_data[1] + + iv = base64.b64decode(encoded_iv) + cipher = Cipher( + algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv) + ) + encrypted_content = base64.b64decode(encoded_content) + + decryptor = cipher.decryptor() + decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize() + + unpadder = padding.PKCS7(128).unpadder() + unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize() + + return unpadded_data.decode() + + def sign_message_hash(self, hash: bytes) -> str: + sk = secp256k1.PrivateKey(self.raw_secret) + sig = sk.schnorr_sign(hash, None, raw=True) + return sig.hex() + + def sign_event(self, event: Event) -> None: + if event.kind == EventKind.ENCRYPTED_DIRECT_MESSAGE and event.content is None: + self.encrypt_dm(event) + if event.public_key is None: + event.public_key = self.public_key.hex() + event.signature = self.sign_message_hash(bytes.fromhex(event.id)) + + def __eq__(self, other): + return self.raw_secret == other.raw_secret + + +def mine_vanity_key(prefix: str = None, suffix: str = None) -> PrivateKey: + if prefix is None and suffix is None: + raise ValueError("Expected at least one of 'prefix' or 'suffix' arguments") + + while True: + sk = PrivateKey() + if ( + prefix is not None + and not sk.public_key.bech32()[5 : 5 + len(prefix)] == prefix + ): + continue + if suffix is not None and not sk.public_key.bech32()[-len(suffix) :] == suffix: + continue + break + + return sk + + +ffi = FFI() + + +@ffi.callback( + "int (unsigned char *, const unsigned char *, const unsigned char *, void *)" +) +def copy_x(output, x32, y32, data): + ffi.memmove(output, x32, 32) + return 1 diff --git a/nostr/message_type.py b/nostr/message_type.py new file mode 100644 index 0000000..3f5206b --- /dev/null +++ b/nostr/message_type.py @@ -0,0 +1,15 @@ +class ClientMessageType: + EVENT = "EVENT" + REQUEST = "REQ" + CLOSE = "CLOSE" + +class RelayMessageType: + EVENT = "EVENT" + NOTICE = "NOTICE" + END_OF_STORED_EVENTS = "EOSE" + + @staticmethod + def is_valid(type: str) -> bool: + if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS: + return True + return False \ No newline at end of file From 47f89afd4ea594b172883a751dd55aa91c229d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 24 Apr 2023 11:22:11 +0200 Subject: [PATCH 060/141] add fastapi 0.95 combatibility for url_for --- .gitignore | 1 + lnurl.py | 4 ++-- models.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/lnurl.py b/lnurl.py index 0f94f54..471c66b 100644 --- a/lnurl.py +++ b/lnurl.py @@ -121,9 +121,9 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): if lnaddress: # for lnaddress, we have to set this otherwise the metadata won't have the identifier link.domain = urlparse(str(request.url)).netloc - callback = request.url_for("lnurlp.api_lnurl_lnaddr_callback", link_id=link.id) + callback = str(request.url_for("lnurlp.api_lnurl_lnaddr_callback", link_id=link.id)) else: - callback = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id) + callback = str(request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)) resp = LnurlPayResponse( callback=callback, diff --git a/models.py b/models.py index 9611d41..1b51960 100644 --- a/models.py +++ b/models.py @@ -55,7 +55,7 @@ class PayLink(BaseModel): def lnurl(self, req: Request) -> str: url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id) - return lnurl_encode(url) + return lnurl_encode(str(url)) def success_action(self, payment_hash: str) -> Optional[Dict]: if self.success_url: From 6dbb0dfd9a28599aa5b55274d164eb3e290c868a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 24 Apr 2023 10:40:29 +0100 Subject: [PATCH 061/141] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f450a66..cee51a1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# LNURLp +# LNURLp - [LNbits](https://github.com/lnbits/lnbits) extension +For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions) ## Create a static QR code or LNaddress people can use to pay over Lightning Network From 64da75d60508a772abdb6dd067ec272a0e3418ee Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Apr 2023 17:12:35 +0200 Subject: [PATCH 062/141] configuration for nostr --- crud.py | 6 +- lnurl.py | 5 +- migrations.py | 9 +- models.py | 2 + static/js/index.js | 7 +- tasks.py | 7 +- templates/lnurlp/index.html | 229 +++++++++++++++++++++++------------- 7 files changed, 170 insertions(+), 95 deletions(-) diff --git a/crud.py b/crud.py index d708e93..4104b37 100644 --- a/crud.py +++ b/crud.py @@ -58,10 +58,11 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: comment_chars, currency, fiat_base_multiplier, - username + username, + zaps ) - VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( link_id, @@ -78,6 +79,7 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: data.currency, data.fiat_base_multiplier, data.username, + data.zaps, ), ) assert result diff --git a/lnurl.py b/lnurl.py index 38fd911..139a9f9 100644 --- a/lnurl.py +++ b/lnurl.py @@ -147,6 +147,7 @@ async def api_lnurl_response(request: Request, link_id, lnaddress=False): if link.comment_chars > 0: params["commentAllowed"] = link.comment_chars - params["allowsNostr"] = True - params["nostrPubkey"] = nostr_publickey.hex() + if link.zaps: + params["allowsNostr"] = True + params["nostrPubkey"] = nostr_publickey.hex() return params diff --git a/migrations.py b/migrations.py index 879bec3..705b55f 100644 --- a/migrations.py +++ b/migrations.py @@ -150,6 +150,13 @@ async def m006_redux(db): async def m007_add_lnaddress_username(db): """ - Add headers and body to webhooks + Add Lightning address to pay links """ await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN username TEXT;") + + +async def m008_add_zap_enabled_column(db): + """ + Add Nostr zaps to pay links + """ + await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN zaps BOOLEAN;") diff --git a/models.py b/models.py index 9611d41..1973ee9 100644 --- a/models.py +++ b/models.py @@ -24,6 +24,7 @@ class CreatePayLinkData(BaseModel): success_url: str = Query(None) fiat_base_multiplier: int = Query(100, ge=1) username: str = Query(None) + zaps: bool = Query(False) class PayLink(BaseModel): @@ -34,6 +35,7 @@ class PayLink(BaseModel): served_meta: int served_pr: int username: Optional[str] + zaps: Optional[bool] domain: Optional[str] webhook_url: Optional[str] webhook_headers: Optional[str] diff --git a/static/js/index.js b/static/js/index.js index 8edc055..e9951f1 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -40,7 +40,9 @@ new Vue({ formDialog: { show: false, fixedAmount: true, - data: {} + data: { + zaps:false + } }, qrCodeDialog: { show: false, @@ -140,7 +142,8 @@ new Vue({ 'success_url', 'comment_chars', 'currency', - 'username' + 'username', + 'zaps' ), (value, key) => (key === 'webhook_url' || diff --git a/tasks.py b/tasks.py index ca77b16..af736d5 100644 --- a/tasks.py +++ b/tasks.py @@ -72,9 +72,9 @@ async def on_invoice_paid(payment: Payment): ) # NIP-57 + # load the zap request nostr = payment.extra.get("nostr") - if nostr: - + if pay_link and pay_link.zaps and nostr: event_json = json.loads(nostr) def get_tag(event_json, tag): @@ -114,7 +114,7 @@ async def on_invoice_paid(payment: Payment): wsts: List[Thread] = [] # # send zap via nostrclient - # ws, wst = send_zap(f"ws://localhost:{settings.port}/nostrclient/api/v1/relay") + # ws, wst = send_zap(f"wss://localhost:{settings.port}/nostrclient/api/v1/relay") # wss += [ws] # wsts += [wst] @@ -138,7 +138,6 @@ async def on_invoice_paid(payment: Payment): async def mark_webhook_sent( payment_hash: str, status: int, is_success: bool, reason_phrase="", text="" ) -> None: - await update_payment_extra( payment_hash, { diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index 1ecd490..4b8202e 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -26,7 +26,7 @@ > {% raw %} @@ -125,7 +132,7 @@
- {{SITE_TITLE}} LNURL-pay extension + {{ SITE_TITLE }} LNURL-pay extension
@@ -152,29 +159,30 @@ > - + filled + dense + v-model.trim="formDialog.data.description" + type="text" + label="Item description *" + > +
- +
-
-   @ {% raw %} {{domain}} {% endraw %} +
+ +   @ {% raw %} {{ domain }} {% endraw %} +
-
-
+
- - - - - - - - + + +
LNURL
+
+
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + +
+
+
+ +
Nostr
+
+
+ +
+
+
+
+
ID: {{ qrCodeDialog.data.id }}
Amount: {{ qrCodeDialog.data.amount }}
{{ qrCodeDialog.data.currency }} price: {{ - fiatRates[qrCodeDialog.data.currency] ? - fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}
{{ qrCodeDialog.data.currency }} price: + {{ + fiatRates[qrCodeDialog.data.currency] + ? fiatRates[qrCodeDialog.data.currency] + ' sat' + : 'Loading...' + }}
- Accepts comments: {{ qrCodeDialog.data.comments }}
+ Accepts comments: {{ qrCodeDialog.data.comments + }}
Dispatches webhook to: {{ qrCodeDialog.data.webhook }}
On success: {{ qrCodeDialog.data.success }}
- Lightning Address: {{ qrCodeDialog.data.username}}@{{domain}} -
+ Lightning Address: + {{ qrCodeDialog.data.username }}@{{ domain }} +

{% endraw %} From b2f970fd669116827bdf417192bf1c23c7c1931e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Apr 2023 13:35:09 +0200 Subject: [PATCH 063/141] remove wrong nostrclient include --- lnurl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lnurl.py b/lnurl.py index 139a9f9..b709036 100644 --- a/lnurl.py +++ b/lnurl.py @@ -14,8 +14,6 @@ from urllib.parse import urlparse import json from . import nostr_publickey -from ..nostrclient.nostr.key import PrivateKey, PublicKey - @lnurlp_ext.get( "/api/v1/lnurl/cb/lnaddr/{link_id}", From d7c05074075bfba621138d6637fb241f2845a297 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 27 Apr 2023 11:56:41 +0200 Subject: [PATCH 064/141] user custom private key --- __init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 0c69f78..4ed6d80 100644 --- a/__init__.py +++ b/__init__.py @@ -12,8 +12,21 @@ from loguru import logger from .nostr.event import Event from .nostr.key import PrivateKey, PublicKey +from environs import Env -nostr_privatekey = PrivateKey() + +def generate_keys(private_key: str = ""): + if private_key.startswith("nsec"): + return PrivateKey.from_nsec(private_key) + elif private_key: + return PrivateKey(bytes.fromhex(private_key)) + else: + return PrivateKey() # generate random key + + +env = Env() +env.read_env() +nostr_privatekey = generate_keys(env.str("LNURLP_ZAP_NOSTR_PRIVATEKEY", default="")) nostr_publickey: PublicKey = nostr_privatekey.public_key logger.debug(f"LNURLP Zaps Nostr pubkey: {nostr_publickey.hex()}") From 095c7933810e77556c88aa44743e8040a7bf2c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Wed, 21 Jun 2023 09:42:14 +0200 Subject: [PATCH 065/141] fix openapi issue with exclusiveMaximum (#14) * fix openapi issue with exclusiveMaximum * dont change businesslogic --- models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models.py b/models.py index d3f8cd3..dd472a6 100644 --- a/models.py +++ b/models.py @@ -16,7 +16,7 @@ class CreatePayLinkData(BaseModel): min: float = Query(1, ge=0.01) max: float = Query(1, ge=0.01) currency: str = Query(None) - comment_chars: int = Query(0, ge=0, lt=800) + comment_chars: int = Query(0, ge=0, le=799) webhook_url: str = Query(None) webhook_headers: str = Query(None) webhook_body: str = Query(None) From 3730d51dce8d35d740f6bc53b31d84925b47816c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Wed, 28 Jun 2023 15:02:09 +0200 Subject: [PATCH 066/141] add github release workflow --- .github/workflows/release.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3e1da5a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,19 @@ +name: release github version +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + prerelease: false From a46058134f59775e613958822140c23ac2de1b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Sat, 23 Sep 2023 22:47:13 +0200 Subject: [PATCH 067/141] [FEAT] add better release workflow (#21) --- .github/workflows/release.yml | 60 +++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e1da5a..7ec9b48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,59 @@ -name: release github version on: push: tags: - - "[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+" + jobs: - build: + + release: runs-on: ubuntu-latest steps: - - name: Create GitHub Release - id: create_release - uses: actions/create-release@v1 + - uses: actions/checkout@v3 + - name: Create github release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref_name }} + run: | + gh release create "$tag" --generate-notes + + pullrequest: + needs: [release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - draft: false - prerelease: false + token: ${{ secrets.EXT_GITHUB }} + repository: lnbits/lnbits-extensions + path: './lnbits-extensions' + + - name: setup git user + run: | + git config --global user.name "alan" + git config --global user.email "alan@lnbits.com" + + - name: Create pull request in extensions repo + env: + GH_TOKEN: ${{ secrets.EXT_GITHUB }} + repo_name: "${{ github.event.repository.name }}" + tag: "${{ github.ref_name }}" + branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}" + title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}" + body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}" + archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip" + run: | + cd lnbits-extensions + git checkout -b $branch + + # if there is another open PR + git pull origin $branch || echo "branch does not exist" + + sh util.sh update_extension $repo_name $tag + + git add -A + git commit -am "$title" + git push origin $branch + + # check if pr exists before creating it + gh config set pager cat + check=$(gh pr list -H $branch | wc -l) + test $check -ne 0 || gh pr create --title "$title" --body "$body" --repo lnbits/lnbits-extensions From ab6b53668ec5fbb82ae1b6a260db2a98e77a5823 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sun, 24 Sep 2023 19:18:36 +0100 Subject: [PATCH 068/141] Fix webhook (#12) * refactor html * don't exclude values on update --- static/js/index.js | 24 +-- templates/lnurlp/index.html | 377 ++++++++++-------------------------- 2 files changed, 102 insertions(+), 299 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index e9951f1..4c09507 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -116,7 +116,6 @@ new Vue({ if (this.formDialog.fixedAmount) data.max = data.min if (data.currency === 'satoshis') data.currency = null if (isNaN(parseInt(data.comment_chars))) data.comment_chars = 0 - if (data.id) { this.updatePayLink(wallet, data) } else { @@ -131,33 +130,12 @@ new Vue({ } }, updatePayLink(wallet, data) { - let values = _.omit( - _.pick( - data, - 'description', - 'min', - 'max', - 'webhook_url', - 'success_text', - 'success_url', - 'comment_chars', - 'currency', - 'username', - 'zaps' - ), - (value, key) => - (key === 'webhook_url' || - key === 'success_text' || - key === 'success_url') && - (value === null || value === '') - ) - LNbits.api .request( 'PUT', '/lnurlp/api/v1/links/' + data.id, wallet.adminkey, - values + data ) .then(response => { this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id) diff --git a/templates/lnurlp/index.html b/templates/lnurlp/index.html index 4b8202e..38f93c6 100644 --- a/templates/lnurlp/index.html +++ b/templates/lnurlp/index.html @@ -4,9 +4,7 @@
- New pay link + New pay link @@ -17,13 +15,7 @@
Pay links
- + {% raw %}