diff --git a/README.md b/README.md
index 3bc169dd..0f81b99f 100644
--- a/README.md
+++ b/README.md
@@ -7,29 +7,29 @@ LNbits

-# LNbits v0.9 BETA, free and open-source lightning-network wallet/accounts system
+# LNbits v0.9 BETA, free and open-source Lightning wallet accounts system
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
-(LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me)
+LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me
Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits server!
-LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
+LNbits is a Python server that sits on top of any funding source. It can be used as:
-* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
-* Extendable platform for exploring lightning-network functionality via LNbits extension framework
+* Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
+* Extendable platform for exploring Lightning network functionality via the LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
-LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
+LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, lntxbot, LightningTipBot, and with more being added regularly.
See [docs.lnbits.org](https://docs.lnbits.org) for more detailed documentation.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
-LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits.
+LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as funding sources for LNbits.
## Running LNbits
@@ -58,16 +58,15 @@ Example use would be an ATM, which utilises LNURL, if the user scans the QR with

-## LNbits as an insta-wallet
+## LNbits as an instant wallet
-Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon).
-"Go to this website", has a lot less friction than "Download this app".
+Wallets can be easily generated and given out to people at events. "Go to this website", has a lot less friction than "Download this app".

## Tip us
-If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
+If you like this project [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
[docs]: https://docs.lnbits.org/
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index 9f8b26da..2bbdfb11 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -206,6 +206,10 @@ poetry add setuptools wheel
./venv/bin/pip install setuptools wheel
```
+#### Poetry
+
+If your Poetry version is older than 1.2, for `poetry install`, ignore the `--only main` flag.
+
### Optional: PostgreSQL database
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
diff --git a/lnbits/extensions/deezy/README.md b/lnbits/extensions/deezy/README.md
new file mode 100644
index 00000000..c8c0678a
--- /dev/null
+++ b/lnbits/extensions/deezy/README.md
@@ -0,0 +1,11 @@
+# Deezy: Home for Lightning Liquidity
+Swap lightning bitcoin for on-chain bitcoin to get inbound liquidity. Or get an on-chain deposit address for your lightning address.
+* [Website](https://deezy.io)
+* [Lightning Node](https://amboss.space/node/024bfaf0cabe7f874fd33ebf7c6f4e5385971fc504ef3f492432e9e3ec77e1b5cf)
+* [Documentation](https://docs.deezy.io)
+* [Discord](https://discord.gg/nEBbrUAvPy)
+
+# Usage
+This extension lets you swap lightning btc for on-chain btc and vice versa.
+* Swap Lightning -> BTC to get inbound liquidity
+* Swap BTC -> Lightning to generate an on-chain deposit address for your lightning address
\ No newline at end of file
diff --git a/lnbits/extensions/deezy/__init__.py b/lnbits/extensions/deezy/__init__.py
new file mode 100644
index 00000000..05d1c9a7
--- /dev/null
+++ b/lnbits/extensions/deezy/__init__.py
@@ -0,0 +1,25 @@
+from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+
+db = Database("ext_deezy")
+
+deezy_ext: APIRouter = APIRouter(prefix="/deezy", tags=["deezy"])
+
+deezy_static_files = [
+ {
+ "path": "/deezy/static",
+ "app": StaticFiles(directory="lnbits/extensions/deezy/static"),
+ "name": "deezy_static",
+ }
+]
+
+
+def deezy_renderer():
+ return template_renderer(["lnbits/extensions/deezy/templates"])
+
+
+from .views import * # noqa
+from .views_api import * # noqa
diff --git a/lnbits/extensions/deezy/config.json b/lnbits/extensions/deezy/config.json
new file mode 100644
index 00000000..4f945a79
--- /dev/null
+++ b/lnbits/extensions/deezy/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Deezy",
+ "short_description": "LN to onchain, onchain to LN swaps",
+ "tile": "/deezy/static/deezy.png",
+ "contributors": ["Uthpala"]
+}
diff --git a/lnbits/extensions/deezy/crud.py b/lnbits/extensions/deezy/crud.py
new file mode 100644
index 00000000..75549349
--- /dev/null
+++ b/lnbits/extensions/deezy/crud.py
@@ -0,0 +1,115 @@
+from http import HTTPStatus
+from typing import List, Optional
+
+from . import db
+from .models import BtcToLnSwap, LnToBtcSwap, Token, UpdateLnToBtcSwap
+
+
+async def get_ln_to_btc() -> List[LnToBtcSwap]:
+
+ rows = await db.fetchall(
+ f"SELECT * FROM deezy.ln_to_btc_swap ORDER BY created_at DESC",
+ )
+
+ return [LnToBtcSwap(**row) for row in rows]
+
+
+async def get_btc_to_ln() -> List[BtcToLnSwap]:
+
+ rows = await db.fetchall(
+ f"SELECT * FROM deezy.btc_to_ln_swap ORDER BY created_at DESC",
+ )
+
+ return [BtcToLnSwap(**row) for row in rows]
+
+
+async def get_token() -> Optional[Token]:
+
+ row = await db.fetchone(
+ f"SELECT * FROM deezy.token ORDER BY created_at DESC",
+ )
+
+ return Token(**row) if row else None
+
+
+async def save_token(
+ data: Token,
+) -> Token:
+
+ await db.execute(
+ """
+ INSERT INTO deezy.token (
+ deezy_token
+ )
+ VALUES (?)
+ """,
+ (data.deezy_token,),
+ )
+ return data
+
+
+async def save_ln_to_btc(
+ data: LnToBtcSwap,
+) -> LnToBtcSwap:
+
+ return await db.execute(
+ """
+ INSERT INTO deezy.ln_to_btc_swap (
+ amount_sats,
+ on_chain_address,
+ on_chain_sats_per_vbyte,
+ bolt11_invoice,
+ fee_sats,
+ txid,
+ tx_hex
+ )
+ VALUES (?,?,?,?,?,?,?)
+ """,
+ (
+ data.amount_sats,
+ data.on_chain_address,
+ data.on_chain_sats_per_vbyte,
+ data.bolt11_invoice,
+ data.fee_sats,
+ data.txid,
+ data.tx_hex,
+ ),
+ )
+
+
+async def update_ln_to_btc(data: UpdateLnToBtcSwap) -> str:
+ await db.execute(
+ """
+ UPDATE deezy.ln_to_btc_swap
+ SET txid = ?, tx_hex = ?
+ WHERE bolt11_invoice = ?
+ """,
+ (data.txid, data.tx_hex, data.bolt11_invoice),
+ )
+
+ return data.txid
+
+
+async def save_btc_to_ln(
+ data: BtcToLnSwap,
+) -> BtcToLnSwap:
+
+ return await db.execute(
+ """
+ INSERT INTO deezy.btc_to_ln_swap (
+ ln_address,
+ on_chain_address,
+ secret_access_key,
+ commitment,
+ signature
+ )
+ VALUES (?,?,?,?,?)
+ """,
+ (
+ data.ln_address,
+ data.on_chain_address,
+ data.secret_access_key,
+ data.commitment,
+ data.signature,
+ ),
+ )
diff --git a/lnbits/extensions/deezy/migrations.py b/lnbits/extensions/deezy/migrations.py
new file mode 100644
index 00000000..67455d6b
--- /dev/null
+++ b/lnbits/extensions/deezy/migrations.py
@@ -0,0 +1,37 @@
+async def m001_initial(db):
+ await db.execute(
+ f"""
+ CREATE TABLE deezy.ln_to_btc_swap (
+ id TEXT PRIMARY KEY,
+ amount_sats {db.big_int} NOT NULL,
+ on_chain_address TEXT NOT NULL,
+ on_chain_sats_per_vbyte INT NOT NULL,
+ bolt11_invoice TEXT NOT NULL,
+ fee_sats {db.big_int} NOT NULL,
+ txid TEXT NULL,
+ tx_hex TEXT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ );
+ """
+ )
+ await db.execute(
+ f"""
+ CREATE TABLE deezy.btc_to_ln_swap (
+ id TEXT PRIMARY KEY,
+ ln_address TEXT NOT NULL,
+ on_chain_address TEXT NOT NULL,
+ secret_access_key TEXT NOT NULL,
+ commitment TEXT NOT NULL,
+ signature TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ );
+ """
+ )
+ await db.execute(
+ f"""
+ CREATE TABLE deezy.token (
+ deezy_token TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ );
+ """
+ )
diff --git a/lnbits/extensions/deezy/models.py b/lnbits/extensions/deezy/models.py
new file mode 100644
index 00000000..e69db355
--- /dev/null
+++ b/lnbits/extensions/deezy/models.py
@@ -0,0 +1,34 @@
+from typing import Optional
+
+from pydantic.main import BaseModel
+from sqlalchemy.engine import base # type: ignore
+
+
+class Token(BaseModel):
+ deezy_token: str
+
+
+class LnToBtcSwap(BaseModel):
+ amount_sats: int
+ on_chain_address: str
+ on_chain_sats_per_vbyte: int
+ bolt11_invoice: str
+ fee_sats: int
+ txid: str = ""
+ tx_hex: str = ""
+ created_at: str = ""
+
+
+class UpdateLnToBtcSwap(BaseModel):
+ txid: str
+ tx_hex: str
+ bolt11_invoice: str
+
+
+class BtcToLnSwap(BaseModel):
+ ln_address: str
+ on_chain_address: str
+ secret_access_key: str
+ commitment: str
+ signature: str
+ created_at: str = ""
diff --git a/lnbits/extensions/deezy/static/deezy.png b/lnbits/extensions/deezy/static/deezy.png
new file mode 100644
index 00000000..cb526705
Binary files /dev/null and b/lnbits/extensions/deezy/static/deezy.png differ
diff --git a/lnbits/extensions/deezy/templates/deezy/_api_docs.html b/lnbits/extensions/deezy/templates/deezy/_api_docs.html
new file mode 100644
index 00000000..4a4e9e30
--- /dev/null
+++ b/lnbits/extensions/deezy/templates/deezy/_api_docs.html
@@ -0,0 +1,253 @@
+
+
+
+
+
+ Deezy.io: Do onchain to offchain and vice-versa swaps
+
+
+ Link :
+
+ https://deezy.io/
+
+
+
+ API DOCS
+
+
+ Created by,
+ Uthpala
+
+
+
+
+
+
+
+
+
+
+ Get the current info about the swap service for converting LN btc to
+ on-chain BTC.
+
+
+ GET (mainnet)
+ https://api.deezy.io/v1/swap/info
+
+
+
+ GET (testnet)
+ https://api-testnet.deezy.io/v1/swap/info
+
+ Response
+
+ {
+ "liquidity_fee_ppm": 2000,
+ "on_chain_bytes_estimate": 300,
+ "max_swap_amount_sats": 100000000,
+ "min_swap_amount_sats": 100000,
+ "available": true
+ }
+
+
+
+
+
+
+
+
+ Initiate a new swap to send lightning btc in exchange for on-chain
+ btc
+
+
+ POST (mainnet)
+ https://api.deezy.io/v1/swap
+
+
+
+ POST (testnet)
+ https://api-testnet.deezy.io/v1/swap
+
+ Payload
+
+ {
+ "amount_sats": 500000,
+ "on_chain_address": "tb1qrcdhlm0m...",
+ "on_chain_sats_per_vbyte": 2
+ }
+
+ Response
+
+ {
+ "bolt11_invoice": "lntb603u1p3vmxj7p...",
+ "fee_sats": 600
+ }
+
+
+
+
+
+
+
+
+ Lookup the on-chain transaction information for an existing swap
+
+
+ GET (mainnet)
+ https://api.deezy.io/v1/swap/lookup
+
+
+
+ GET (testnet)
+ https://api-testnet.deezy.io/v1/swap/lookup
+
+ Query Parameter
+
+ "bolt11_invoice": "lntb603u1p3vmxj7pp54...",
+
+ Response
+
+ {
+ "on_chain_txid": "string",
+ "tx_hex": "string"
+ }
+
+
+
+
+
+
+
+
+
+
+ Generate an on-chain deposit address for your lnurl or lightning
+ address.
+
+
+ POST (mainnet)
+ https://api.deezy.io/v1/source
+
+
+
+ POST (testnet)
+ https://api-testnet.deezy.io/v1/source
+
+ Payload
+
+ {
+ "lnurl_or_lnaddress": "LNURL1DP68GURN8GHJ...",
+ "secret_access_key": "b3c6056d2845867fa7..",
+ "webhook_url": "https://your.website.com/dee.."
+ }
+
+ Response
+
+ {
+ "address": "bc1qkceyc5...",
+ "secret_access_key": "b3c6056d28458...",
+ "commitment": "for any satoshis sent to bc1..",
+ "signature": "d69j6aj1ssz5egmsr..",
+ "webhook_url": "https://your.website.com/deez.."
+ }
+
+
+
+
+
+
+
+
+ Lookup (BTC to LN) swaps
+
+
+ GET (mainnet)
+ https://api.deezy.io/v1/source/lookup
+
+
+
+ GET (testnet)
+ https://api-testnet.deezy.io/v1/source/lookup
+
+ Response
+
+ {
+ "swaps": [
+ {
+ "lnurl_or_lnaddress": "string",
+ "deposit_address": "string",
+ "utxo_key": "string",
+ "deposit_amount_sats": 0,
+ "target_payout_amount_sats": 0,
+ "paid_amount_sats": 0,
+ "deezy_fee_sats": 0,
+ "status": "string"
+ }
+ ],
+ "total_sent_sats": 0,
+ "total_received_sats": 0,
+ "total_pending_payout_sats": 0,
+ "total_deezy_fees_sats": 0
+ }
+
+
+
+
+
+
diff --git a/lnbits/extensions/deezy/templates/deezy/index.html b/lnbits/extensions/deezy/templates/deezy/index.html
new file mode 100644
index 00000000..858d3255
--- /dev/null
+++ b/lnbits/extensions/deezy/templates/deezy/index.html
@@ -0,0 +1,588 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+
+
+
+ Deezy
+
+ An access token is required to use the swap service. Email
+ support@deezy.io or contact @dannydeezy on telegram to get one.
+
+
+
+ Deezy token
+ Add or Update token
+
+
+
+
+
+
+
+
+
+
+
+
+ Send lightning btc and receive on-chain btc
+
+
+
+
+ Send on-chain btc and receive via lightning
+
+
+
+
+
+
LIGHTNING BTC -> BTC
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+
Pay invoice to complete swap
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
BTC -> LIGHTNING BTC
+
+
+
+ Cancel
+
+
+
+
+
+
Onchain Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% raw %}
+
+
+
+ Success Bitcoin is on its way
+
+
+
+ Onchain tx id {{ swapLnToBtc.onchainTxId }}
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+ {{SITE_TITLE}} Boltz extension
+
+
+
+ {% include "deezy/_api_docs.html" %}
+
+
+
+
+
+
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+{% endblock %}
diff --git a/lnbits/extensions/deezy/views.py b/lnbits/extensions/deezy/views.py
new file mode 100644
index 00000000..131c03b2
--- /dev/null
+++ b/lnbits/extensions/deezy/views.py
@@ -0,0 +1,21 @@
+from fastapi import FastAPI, Request
+from fastapi.params import Depends
+from fastapi.templating import Jinja2Templates
+from starlette.responses import HTMLResponse
+
+from lnbits.core.models import User
+from lnbits.decorators import check_user_exists
+
+from . import deezy_ext, deezy_renderer
+
+templates = Jinja2Templates(directory="templates")
+
+
+@deezy_ext.get("/", response_class=HTMLResponse)
+async def index(
+ request: Request,
+ user: User = Depends(check_user_exists), # type: ignore
+):
+ return deezy_renderer().TemplateResponse(
+ "deezy/index.html", {"request": request, "user": user.dict()}
+ )
diff --git a/lnbits/extensions/deezy/views_api.py b/lnbits/extensions/deezy/views_api.py
new file mode 100644
index 00000000..1006edeb
--- /dev/null
+++ b/lnbits/extensions/deezy/views_api.py
@@ -0,0 +1,65 @@
+# views_api.py is for you API endpoints that could be hit by another service
+
+# add your dependencies here
+
+# import httpx
+# (use httpx just like requests, except instead of response.ok there's only the
+# response.is_error that is its inverse)
+
+from . import deezy_ext
+from .crud import (
+ get_btc_to_ln,
+ get_ln_to_btc,
+ get_token,
+ save_btc_to_ln,
+ save_ln_to_btc,
+ save_token,
+ update_ln_to_btc,
+)
+from .models import BtcToLnSwap, LnToBtcSwap, Token, UpdateLnToBtcSwap
+
+
+@deezy_ext.get("/api/v1/token")
+async def api_deezy_get_token():
+ rows = await get_token()
+ return rows
+
+
+@deezy_ext.get("/api/v1/ln-to-btc")
+async def api_deezy_get_ln_to_btc():
+ rows = await get_ln_to_btc()
+ return rows
+
+
+@deezy_ext.get("/api/v1/btc-to-ln")
+async def api_deezy_get_btc_to_ln():
+ rows = await get_btc_to_ln()
+ return rows
+
+
+@deezy_ext.post("/api/v1/store-token")
+async def api_deezy_save_toke(data: Token):
+ await save_token(data)
+
+ return data.deezy_token
+
+
+@deezy_ext.post("/api/v1/store-ln-to-btc")
+async def api_deezy_save_ln_to_btc(data: LnToBtcSwap):
+ response = await save_ln_to_btc(data)
+
+ return response
+
+
+@deezy_ext.post("/api/v1/update-ln-to-btc")
+async def api_deezy_update_ln_to_btc(data: UpdateLnToBtcSwap):
+ response = await update_ln_to_btc(data)
+
+ return response
+
+
+@deezy_ext.post("/api/v1/store-btc-to-ln")
+async def api_deezy_save_btc_to_ln(data: BtcToLnSwap):
+ response = await save_btc_to_ln(data)
+
+ return response
diff --git a/lnbits/extensions/example/templates/example/index.html b/lnbits/extensions/example/templates/example/index.html
index 64a4d3dd..03f66b5f 100644
--- a/lnbits/extensions/example/templates/example/index.html
+++ b/lnbits/extensions/example/templates/example/index.html
@@ -67,7 +67,7 @@
>
-
+
@@ -79,200 +79,189 @@
Frameworks
-
-
-
-
FASTAPI
-
QUASAR
-
VUE-JS
-
-
+
+
+
FASTAPI
+
QUASAR
+
VUE-JS
+
+
-
-
-
-
-
- LNbits API is built using
- FastAPI, a high-performance, easy to code API framework.
- FastAPI auto-generates swagger UI docs for testing
- endpoints /docs
-
+
+
+
+
+
+ LNbits API is built using
+ FastAPI, a high-performance, easy to code API framework.
+ FastAPI auto-generates swagger UI docs for testing endpoints
+ /docs
+
-
- TIP: Although it is possible for
- extensions to use other extensions API endpoints (such as
- with the Satspay and Onchain extension), ideally an
- extension should only use LNbits
- core
- endpoints.
+
+ TIP: Although it is possible for extensions
+ to use other extensions API endpoints (such as with the
+ Satspay and Onchain extension), ideally an extension should
+ only use LNbits
+ core
+ endpoints.
- views.py is used
- for setting application routes:
-
- views_api.py is
- used for setting application API endpoints:
-
-
+ views.py is used for
+ setting application routes:
+ 
+ views_api.py is used
+ for setting application API endpoints:
+
+
-
-
-
- LNbits uses
- Quasar Framework
- for frontend deisgn elements. Quasar Framework is an
- open-source Vue.js based framework for building apps.
-
-
-
- TIP: Look through
- /template files
- in other extensions for examples of Quasar elements being
- used.
-
-
- In the below example we make a dialogue popup box (box can
- be triggered
- here): Exmple of a tooltip!
-
- 
- Useful links:
- Style (typography, spacing, etc)
+
+
+ LNbits uses
+ Quasar Framework
- Genral components (cards, buttons, popup dialogs,
- etc)
- Layouts (rows/columns, flexbox)
-
+ for frontend deisgn elements. Quasar Framework is an
+ open-source Vue.js based framework for building apps.
+
-
+ TIP: Look through
+ /template files in
+ other extensions for examples of Quasar elements being used.
+
+
+ In the below example we make a dialogue popup box (box can
+ be triggered
+ here): Exmple of a tooltip!
+
+ 
+ Useful links:
+ Style (typography, spacing, etc)
-
-
-
- LNbits uses
- Vue
- components for best-in-class high-performance and
- responsive performance.
-
+ Genral components (cards, buttons, popup dialogs,
+ etc)
+ Layouts (rows/columns, flexbox)
+
-
- Typical example of Vue components in a frontend script:
-
- 
+
+
+
+
+ LNbits uses
+ Vue
+ components for best-in-class high-performance and responsive
+ performance.
+
-
- In a page body, models can be called.
Content can be
- conditionally rendered using Vue's
- v-if:
-
-
-
-
-
-
+
Typical example of Vue components in a frontend script:
+

+
+
+ In a page body, models can be called.
Content can be
+ conditionally rendered using Vue's
+ v-if:
+
+

+
+
+
Useful Tools
-
-
-
- MAGICAL G
- EXCHANGE RATES
-
-
+
+
+ MAGICAL G
+ EXCHANGE RATES
+
+
-
-
-
- Magical G
-
- A magical "g" (ie
- this.g.user.wallets[0].inkey) is always available, with info about the user, wallets
- and extensions:
-
- {% raw %}{{ g }}{% endraw %}
-
-
- Exchange rates
-
- LNbits includes a handy
- exchange rate function, that streams rates from 6 different sources.
-
- Exchange rate API:
- 
- Exchange rate functions, included using
+
+
+
+ Magical G
+
+ A magical "g" (ie
from lnbits.utils.exchange_rates import
- fiat_amount_as_satoshis:
-
-
-
-
-
+ >this.g.user.wallets[0].inkey) is always available, with info about the user, wallets
+ and extensions:
+
+
{% raw %}{{ g }}{% endraw %}
+
+
+ Exchange rates
+
+ LNbits includes a handy
+ exchange rate function, that streams rates from 6 different sources.
+
+ Exchange rate API:
+ 
+ Exchange rate functions, included using
+ from lnbits.utils.exchange_rates import
+ fiat_amount_as_satoshis:
+
+
+
+
-
- File Structure
+
+ Good Practice
Coming soon...
diff --git a/lnbits/extensions/livestream/lnurl.py b/lnbits/extensions/livestream/lnurl.py
index 63523f33..e3e1b1be 100644
--- a/lnbits/extensions/livestream/lnurl.py
+++ b/lnbits/extensions/livestream/lnurl.py
@@ -1,12 +1,9 @@
-import hashlib
import math
from http import HTTPStatus
-from os import name
-from fastapi.exceptions import HTTPException
-from fastapi.params import Query
+from fastapi import HTTPException, Query, Request
from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse
-from starlette.requests import Request # type: ignore
+from lnurl.models import ClearnetUrl, LightningInvoice, MilliSatoshi
from lnbits.core.services import create_invoice
@@ -29,9 +26,12 @@ async def lnurl_livestream(ls_id, request: Request):
)
resp = LnurlPayResponse(
- callback=request.url_for("livestream.lnurl_callback", track_id=track.id),
- min_sendable=track.min_sendable,
- max_sendable=track.max_sendable,
+ callback=ClearnetUrl(
+ request.url_for("livestream.lnurl_callback", track_id=track.id),
+ scheme="https",
+ ),
+ minSendable=MilliSatoshi(track.min_sendable),
+ maxSendable=MilliSatoshi(track.max_sendable),
metadata=await track.lnurlpay_metadata(),
)
@@ -48,9 +48,12 @@ async def lnurl_track(track_id, request: Request):
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Track not found.")
resp = LnurlPayResponse(
- callback=request.url_for("livestream.lnurl_callback", track_id=track.id),
- min_sendable=track.min_sendable,
- max_sendable=track.max_sendable,
+ callback=ClearnetUrl(
+ request.url_for("livestream.lnurl_callback", track_id=track.id),
+ scheme="https",
+ ),
+ minSendable=MilliSatoshi(track.min_sendable),
+ maxSendable=MilliSatoshi(track.max_sendable),
metadata=await track.lnurlpay_metadata(),
)
@@ -85,6 +88,7 @@ async def lnurl_callback(
).dict()
ls = await get_livestream_by_track(track_id)
+ assert ls
extra_amount = amount_received - int(amount_received * (100 - ls.fee_pct) / 100)
@@ -101,13 +105,14 @@ async def lnurl_callback(
},
)
+ assert track.price_msat
if amount_received < track.price_msat:
success_action = None
else:
success_action = track.success_action(payment_hash, request=request)
resp = LnurlPayActionResponse(
- pr=payment_request, success_action=success_action, routes=[]
+ pr=LightningInvoice(payment_request), successAction=success_action, routes=[]
)
return resp.dict()
diff --git a/lnbits/extensions/livestream/models.py b/lnbits/extensions/livestream/models.py
index 0034f4a7..5d617da9 100644
--- a/lnbits/extensions/livestream/models.py
+++ b/lnbits/extensions/livestream/models.py
@@ -1,13 +1,12 @@
import json
from typing import Optional
-from fastapi import Query
+from fastapi import Query, Request
from lnurl import Lnurl
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
+from lnurl import encode as lnurl_encode
+from lnurl.models import ClearnetUrl, Max144Str, UrlAction
+from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
-from starlette.requests import Request
class CreateTrack(BaseModel):
@@ -32,7 +31,7 @@ class Livestream(BaseModel):
class Track(BaseModel):
id: int
download_url: Optional[str]
- price_msat: Optional[int]
+ price_msat: int = 0
name: str
producer: int
@@ -71,7 +70,7 @@ class Track(BaseModel):
def success_action(
self, payment_hash: str, request: Request
- ) -> Optional[LnurlPaySuccessAction]:
+ ) -> Optional[UrlAction]:
if not self.download_url:
return None
@@ -79,7 +78,8 @@ class Track(BaseModel):
url_with_query = f"{url}?p={payment_hash}"
return UrlAction(
- url=url_with_query, description=f"Download the track {self.name}!"
+ url=ClearnetUrl(url_with_query, scheme="https"),
+ description=Max144Str(f"Download the track {self.name}!"),
)
diff --git a/lnbits/extensions/livestream/views.py b/lnbits/extensions/livestream/views.py
index 97f803a3..ca12f16b 100644
--- a/lnbits/extensions/livestream/views.py
+++ b/lnbits/extensions/livestream/views.py
@@ -1,20 +1,16 @@
from http import HTTPStatus
-from fastapi.param_functions import Depends
-from fastapi.params import Query
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
+from fastapi import Depends, HTTPException, Query, Request
+from starlette.datastructures import URL
from starlette.responses import HTMLResponse, RedirectResponse
from lnbits.core.crud import get_wallet_payment
-from lnbits.core.models import Payment, User
+from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import livestream_ext, livestream_renderer
from .crud import get_livestream_by_track, get_track
-# from mmap import MAP_DENYWRITE
-
@livestream_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
@@ -28,12 +24,18 @@ async def track_redirect_download(track_id, p: str = Query(...)):
payment_hash = p
track = await get_track(track_id)
ls = await get_livestream_by_track(track_id)
- payment: Payment = await get_wallet_payment(ls.wallet, payment_hash)
+ assert ls
+ payment = await get_wallet_payment(ls.wallet, payment_hash)
if not payment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
- detail=f"Couldn't find the payment {payment_hash} or track {track.id}.",
+ detail=f"Couldn't find the payment {payment_hash}.",
+ )
+ if not track:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail=f"Couldn't find the track {track_id}.",
)
if payment.pending:
@@ -41,4 +43,6 @@ async def track_redirect_download(track_id, p: str = Query(...)):
status_code=HTTPStatus.PAYMENT_REQUIRED,
detail=f"Payment {payment_hash} wasn't received yet. Please try again in a minute.",
)
- return RedirectResponse(url=track.download_url)
+
+ assert track.download_url
+ return RedirectResponse(url=URL(track.download_url))
diff --git a/lnbits/extensions/livestream/views_api.py b/lnbits/extensions/livestream/views_api.py
index 0c169a71..63a01742 100644
--- a/lnbits/extensions/livestream/views_api.py
+++ b/lnbits/extensions/livestream/views_api.py
@@ -1,9 +1,7 @@
from http import HTTPStatus
-from fastapi.param_functions import Depends
+from fastapi import Depends, HTTPException, Request
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
-from starlette.exceptions import HTTPException
-from starlette.requests import Request # type: ignore
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.livestream.models import CreateTrack
@@ -27,6 +25,7 @@ async def api_livestream_from_wallet(
req: Request, g: WalletTypeInfo = Depends(get_key_type)
):
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
+ assert ls
tracks = await get_tracks(ls.id)
producers = await get_producers(ls.id)
@@ -55,17 +54,17 @@ async def api_update_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
id = int(track_id)
except ValueError:
id = 0
- if id <= 0:
- id = None
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
- await update_current_track(ls.id, id)
+ assert ls
+ await update_current_track(ls.id, None if id <= 0 else id)
return "", HTTPStatus.NO_CONTENT
@livestream_ext.put("/api/v1/livestream/fee/{fee_pct}")
async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)):
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
+ assert ls
await update_livestream_fee(ls.id, int(fee_pct))
return "", HTTPStatus.NO_CONTENT
@@ -76,9 +75,10 @@ async def api_add_track(
data: CreateTrack, id=None, g: WalletTypeInfo = Depends(get_key_type)
):
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
+ assert ls
if data.producer_id:
- p_id = data.producer_id
+ p_id = int(data.producer_id)
elif data.producer_name:
p_id = await add_producer(ls.id, data.producer_name)
else:
@@ -96,5 +96,6 @@ async def api_add_track(
@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}")
async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
+ assert ls
await delete_track_from_livestream(ls.id, track_id)
return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py
index 182df743..0ab520da 100644
--- a/lnbits/extensions/lnurldevice/crud.py
+++ b/lnbits/extensions/lnurldevice/crud.py
@@ -7,8 +7,6 @@ from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import createLnurldevice, lnurldevicepayment, lnurldevices
-###############lnurldeviceS##########################
-
async def create_lnurldevice(
data: createLnurldevice,
@@ -69,10 +67,12 @@ async def create_lnurldevice(
data.pin4,
),
)
- return await get_lnurldevice(lnurldevice_id)
+ device = await get_lnurldevice(lnurldevice_id)
+ assert device
+ return device
-async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldevices]:
+async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> lnurldevices:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevices SET {q} WHERE id = ?",
@@ -81,19 +81,18 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
- return lnurldevices(**row) if row else None
+ return lnurldevices(**row)
-async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
+async def get_lnurldevice(lnurldevice_id: str) -> Optional[lnurldevices]:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
return lnurldevices(**row) if row else None
-async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]:
- wallet_ids = [wallet_ids]
- q = ",".join(["?"] * len(wallet_ids[0]))
+async def get_lnurldevices(wallet_ids: List[str]) -> List[lnurldevices]:
+ q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"""
SELECT * FROM lnurldevice.lnurldevices WHERE wallet IN ({q})
@@ -102,7 +101,7 @@ async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevic
(*wallet_ids,),
)
- return [lnurldevices(**row) if row else None for row in rows]
+ return [lnurldevices(**row) for row in rows]
async def delete_lnurldevice(lnurldevice_id: str) -> None:
@@ -110,8 +109,6 @@ async def delete_lnurldevice(lnurldevice_id: str) -> None:
"DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
- ########################lnuldevice payments###########################
-
async def create_lnurldevicepayment(
deviceid: str,
@@ -121,6 +118,7 @@ async def create_lnurldevicepayment(
sats: Optional[int] = 0,
) -> lnurldevicepayment:
device = await get_lnurldevice(deviceid)
+ assert device
if device.device == "atm":
lnurldevicepayment_id = shortuuid.uuid(name=payload)
else:
@@ -139,7 +137,9 @@ async def create_lnurldevicepayment(
""",
(lnurldevicepayment_id, deviceid, payload, pin, payhash, sats),
)
- return await get_lnurldevicepayment(lnurldevicepayment_id)
+ dpayment = await get_lnurldevicepayment(lnurldevicepayment_id)
+ assert dpayment
+ return dpayment
async def update_lnurldevicepayment(
@@ -157,7 +157,9 @@ async def update_lnurldevicepayment(
return lnurldevicepayment(**row) if row else None
-async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
+async def get_lnurldevicepayment(
+ lnurldevicepayment_id: str,
+) -> Optional[lnurldevicepayment]:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,),
@@ -165,7 +167,9 @@ async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayme
return lnurldevicepayment(**row) if row else None
-async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
+async def get_lnurlpayload(
+ lnurldevicepayment_payload: str,
+) -> Optional[lnurldevicepayment]:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?",
(lnurldevicepayment_payload,),
diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py
index 34de20fa..eba2a693 100644
--- a/lnbits/extensions/lnurldevice/lnurl.py
+++ b/lnbits/extensions/lnurldevice/lnurl.py
@@ -1,16 +1,11 @@
import base64
-import hashlib
import hmac
from http import HTTPStatus
from io import BytesIO
-from typing import Optional
import shortuuid
from embit import bech32, compact
-from fastapi import Request
-from fastapi.param_functions import Query
-from loguru import logger
-from starlette.exceptions import HTTPException
+from fastapi import HTTPException, Query, Request
from lnbits import bolt11
from lnbits.core.services import create_invoice
@@ -44,7 +39,9 @@ def bech32_decode(bech):
encoding = bech32.bech32_verify_checksum(hrp, data)
if encoding is None:
return
- return bytes(bech32.convertbits(data[:-6], 5, 8, False))
+ bits = bech32.convertbits(data[:-6], 5, 8, False)
+ assert bits
+ return bytes(bits)
def xor_decrypt(key, blob):
@@ -105,6 +102,8 @@ async def lnurl_v1_params(
"reason": f"lnurldevice {device_id} not found on this server",
}
if device.device == "switch":
+ # TODO: AMOUNT IN CENT was never reference here
+ amount_in_cent = 0
price_msat = (
await fiat_amount_as_satoshis(float(profit), device.currency)
if device.currency != "sat"
@@ -160,23 +159,18 @@ async def lnurl_v1_params(
if device.device != "atm":
return {"status": "ERROR", "reason": "Not ATM device."}
price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000)
- lnurldevicepayment = await get_lnurldevicepayment(shortuuid.uuid(name=p))
- if lnurldevicepayment:
- logger.debug("lnurldevicepayment")
- logger.debug(lnurldevicepayment)
- logger.debug("lnurldevicepayment")
- if lnurldevicepayment.payload == lnurldevicepayment.payhash:
- return {"status": "ERROR", "reason": f"Payment already claimed"}
- else:
+ try:
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
- pin=pin,
+ pin=str(pin),
payhash="payment_hash",
)
+ except:
+ return {"status": "ERROR", "reason": "Could not create ATM payment."}
if not lnurldevicepayment:
- return {"status": "ERROR", "reason": "Could not create payment."}
+ return {"status": "ERROR", "reason": "Could not create ATM payment."}
return {
"tag": "withdrawRequest",
"callback": request.url_for(
@@ -193,7 +187,7 @@ async def lnurl_v1_params(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
- pin=pin,
+ pin=str(pin),
payhash="payment_hash",
)
if not lnurldevicepayment:
@@ -221,6 +215,10 @@ async def lnurl_callback(
k1: str = Query(None),
):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
+ if not lnurldevicepayment:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN, detail="lnurldevicepayment not found."
+ )
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
@@ -241,13 +239,17 @@ async def lnurl_callback(
else:
if lnurldevicepayment.payload != k1:
return {"status": "ERROR", "reason": "Bad K1"}
- lnurldevicepayment = await update_lnurldevicepayment(
+ if lnurldevicepayment.payhash != "payment_hash":
+ return {"status": "ERROR", "reason": f"Payment already claimed"}
+
+ lnurldevicepayment_updated = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
)
+ assert lnurldevicepayment_updated
await pay_invoice(
wallet_id=device.wallet,
payment_request=pr,
- max_sat=lnurldevicepayment.sats / 1000,
+ max_sat=int(lnurldevicepayment_updated.sats / 1000),
extra={"tag": "withdraw"},
)
return {"status": "OK"}
diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py
index 66b215f2..f9640de1 100644
--- a/lnbits/extensions/lnurldevice/models.py
+++ b/lnbits/extensions/lnurldevice/models.py
@@ -3,13 +3,9 @@ from sqlite3 import Row
from typing import List, Optional
from fastapi import Request
-from lnurl import Lnurl
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
-from loguru import logger
+from lnurl import encode as lnurl_encode
+from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
-from pydantic.main import BaseModel
class createLnurldevice(BaseModel):
@@ -58,6 +54,7 @@ class lnurldevices(BaseModel):
pin4: int
timestamp: str
+ @classmethod
def from_row(cls, row: Row) -> "lnurldevices":
return cls(**dict(row))
diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py
index 8ad9772c..9aec173e 100644
--- a/lnbits/extensions/lnurldevice/tasks.py
+++ b/lnbits/extensions/lnurldevice/tasks.py
@@ -1,18 +1,11 @@
import asyncio
-import json
-from http import HTTPStatus
-from urllib.parse import urlparse
-import httpx
-from fastapi import HTTPException
-
-from lnbits import bolt11
from lnbits.core.models import Payment
-from lnbits.core.services import pay_invoice, websocketUpdater
+from lnbits.core.services import websocketUpdater
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
-from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment
+from .crud import get_lnurldevicepayment, update_lnurldevicepayment
async def wait_for_paid_invoices():
@@ -27,14 +20,15 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
# (avoid loops)
if "Switch" == payment.extra.get("tag"):
- lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id"))
+ lnurldevicepayment = await get_lnurldevicepayment(payment.extra["id"])
if not lnurldevicepayment:
return
if lnurldevicepayment.payhash == "used":
return
lnurldevicepayment = await update_lnurldevicepayment(
- lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
+ lnurldevicepayment_id=payment.extra["id"], payhash="used"
)
+ assert lnurldevicepayment
return await websocketUpdater(
lnurldevicepayment.deviceid,
str(lnurldevicepayment.pin) + "-" + str(lnurldevicepayment.payload),
diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py
index f1be4f0d..a6256a41 100644
--- a/lnbits/extensions/lnurldevice/views.py
+++ b/lnbits/extensions/lnurldevice/views.py
@@ -1,12 +1,7 @@
from http import HTTPStatus
-from io import BytesIO
-import pyqrcode
-from fastapi import Request
-from fastapi.param_functions import Query
-from fastapi.params import Depends
+from fastapi import Depends, HTTPException, Query, Request
from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse, StreamingResponse
from lnbits.core.crud import update_payment_status
@@ -62,4 +57,6 @@ async def img(request: Request, lnurldevice_id):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist."
)
- return lnurldevice.lnurl(request)
+ # error: "lnurldevices" has no attribute "lnurl"
+ # return lnurldevice.lnurl(request)
+ return None
diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py
index c6766423..d657c879 100644
--- a/lnbits/extensions/lnurldevice/views_api.py
+++ b/lnbits/extensions/lnurldevice/views_api.py
@@ -1,9 +1,6 @@
from http import HTTPStatus
-from fastapi import Request
-from fastapi.param_functions import Query
-from fastapi.params import Depends
-from starlette.exceptions import HTTPException
+from fastapi import Depends, HTTPException, Query, Request
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
@@ -26,9 +23,6 @@ async def api_list_currencies_available():
return list(currencies.keys())
-#######################lnurldevice##########################
-
-
@lnurldevice_ext.post("/api/v1/lnurlpos")
@lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_create_or_update(
@@ -41,7 +35,7 @@ async def api_lnurldevice_create_or_update(
lnurldevice = await create_lnurldevice(data)
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
else:
- lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
+ lnurldevice = await update_lnurldevice(lnurldevice_id, **data.dict())
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
@@ -49,7 +43,8 @@ async def api_lnurldevice_create_or_update(
async def api_lnurldevices_retrieve(
req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
- wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
+ user = await get_user(wallet.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
try:
return [
{**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
@@ -65,10 +60,11 @@ async def api_lnurldevices_retrieve(
return ""
-@lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}")
+@lnurldevice_ext.get(
+ "/api/v1/lnurlpos/{lnurldevice_id}", dependencies=[Depends(get_key_type)]
+)
async def api_lnurldevice_retrieve(
req: Request,
- wallet: WalletTypeInfo = Depends(get_key_type),
lnurldevice_id: str = Query(None),
):
lnurldevice = await get_lnurldevice(lnurldevice_id)
@@ -76,23 +72,18 @@ async def api_lnurldevice_retrieve(
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist"
)
- if not lnurldevice.lnurl_toggle:
- return {**lnurldevice.dict()}
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
-@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
-async def api_lnurldevice_delete(
- wallet: WalletTypeInfo = Depends(require_admin_key),
- lnurldevice_id: str = Query(None),
-):
+@lnurldevice_ext.delete(
+ "/api/v1/lnurlpos/{lnurldevice_id}", dependencies=[Depends(require_admin_key)]
+)
+async def api_lnurldevice_delete(lnurldevice_id: str = Query(None)):
lnurldevice = await get_lnurldevice(lnurldevice_id)
-
if not lnurldevice:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
)
await delete_lnurldevice(lnurldevice_id)
-
return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/nostrnip5/README.md b/lnbits/extensions/nostrnip5/README.md
index b8912fa2..2bcbf054 100644
--- a/lnbits/extensions/nostrnip5/README.md
+++ b/lnbits/extensions/nostrnip5/README.md
@@ -41,4 +41,19 @@ location /.well-known/nostr.json {
proxy_cache_valid 200 300s;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
}
+```
+
+Example Caddy configuration
+
+```
+my.lnbits.instance {
+ reverse_proxy {your_lnbits}
+}
+
+nip.5.domain {
+ route /.well-known/nostr.json {
+ rewrite * /nostrnip5/api/v1/domain/{domain_id}/nostr.json
+ reverse_proxy {your_lnbits}
+ }
+}
```
\ No newline at end of file
diff --git a/lnbits/extensions/nostrnip5/templates/nostrnip5/index.html b/lnbits/extensions/nostrnip5/templates/nostrnip5/index.html
index b1459ee3..820d8718 100644
--- a/lnbits/extensions/nostrnip5/templates/nostrnip5/index.html
+++ b/lnbits/extensions/nostrnip5/templates/nostrnip5/index.html
@@ -201,7 +201,7 @@
dense
v-model.trim="formDialog.data.amount"
label="Amount"
- placeholder="10.00"
+ placeholder="How much do you want to charge?"
>
SMTP Extension
+
+This extension allows you to setup a smtp, to offer sending emails with it for a small fee.
+
+## Requirements
+
+- SMTP Server
+
+## Usage
+
+1. Create new emailaddress
+2. Verify if email goes to your testemail. Testmail is sent on create and update
+3. Share the link with the email form.
+
diff --git a/lnbits/extensions/smtp/__init__.py b/lnbits/extensions/smtp/__init__.py
new file mode 100644
index 00000000..e7419852
--- /dev/null
+++ b/lnbits/extensions/smtp/__init__.py
@@ -0,0 +1,34 @@
+import asyncio
+
+from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
+
+from lnbits.db import Database
+from lnbits.helpers import template_renderer
+from lnbits.tasks import catch_everything_and_restart
+
+db = Database("ext_smtp")
+
+smtp_static_files = [
+ {
+ "path": "/smtp/static",
+ "app": StaticFiles(directory="lnbits/extensions/smtp/static"),
+ "name": "smtp_static",
+ }
+]
+
+smtp_ext: APIRouter = APIRouter(prefix="/smtp", tags=["smtp"])
+
+
+def smtp_renderer():
+ return template_renderer(["lnbits/extensions/smtp/templates"])
+
+
+from .tasks import wait_for_paid_invoices
+from .views import * # noqa
+from .views_api import * # noqa
+
+
+def smtp_start():
+ loop = asyncio.get_event_loop()
+ loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/smtp/config.json b/lnbits/extensions/smtp/config.json
new file mode 100644
index 00000000..325ebfa7
--- /dev/null
+++ b/lnbits/extensions/smtp/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "SMTP",
+ "short_description": "Charge sats for sending emails",
+ "tile": "/smtp/static/smtp-bitcoin-email.png",
+ "contributors": ["dni"]
+}
diff --git a/lnbits/extensions/smtp/crud.py b/lnbits/extensions/smtp/crud.py
new file mode 100644
index 00000000..2eee4c3d
--- /dev/null
+++ b/lnbits/extensions/smtp/crud.py
@@ -0,0 +1,168 @@
+from http import HTTPStatus
+from typing import List, Optional, Union
+
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import CreateEmail, CreateEmailaddress, Emailaddresses, Emails
+from .smtp import send_mail
+
+
+def get_test_mail(email, testemail):
+ return CreateEmail(
+ emailaddress_id=email,
+ subject="LNBits SMTP - Test Email",
+ message="This is a test email from the LNBits SMTP extension! email is working!",
+ receiver=testemail,
+ )
+
+
+async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses:
+
+ emailaddress_id = urlsafe_short_hash()
+
+ # send test mail for checking connection
+ email = get_test_mail(data.email, data.testemail)
+ await send_mail(data, email)
+
+ await db.execute(
+ """
+ INSERT INTO smtp.emailaddress (id, wallet, email, testemail, smtp_server, smtp_user, smtp_password, smtp_port, anonymize, description, cost)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ emailaddress_id,
+ data.wallet,
+ data.email,
+ data.testemail,
+ data.smtp_server,
+ data.smtp_user,
+ data.smtp_password,
+ data.smtp_port,
+ data.anonymize,
+ data.description,
+ data.cost,
+ ),
+ )
+
+ new_emailaddress = await get_emailaddress(emailaddress_id)
+ assert new_emailaddress, "Newly created emailaddress couldn't be retrieved"
+ return new_emailaddress
+
+
+async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddresses:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE smtp.emailaddress SET {q} WHERE id = ?",
+ (*kwargs.values(), emailaddress_id),
+ )
+ row = await db.fetchone(
+ "SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,)
+ )
+
+ # send test mail for checking connection
+ email = get_test_mail(row.email, row.testemail)
+ await send_mail(row, email)
+
+ assert row, "Newly updated emailaddress couldn't be retrieved"
+ return Emailaddresses(**row)
+
+
+async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddresses]:
+ row = await db.fetchone(
+ "SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,)
+ )
+ return Emailaddresses(**row) if row else None
+
+
+async def get_emailaddress_by_email(email: str) -> Optional[Emailaddresses]:
+ row = await db.fetchone("SELECT * FROM smtp.emailaddress WHERE email = ?", (email,))
+ return Emailaddresses(**row) if row else None
+
+
+# async def get_emailAddressByEmail(email: str) -> Optional[Emails]:
+# row = await db.fetchone(
+# "SELECT s.*, d.emailaddress as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.emailaddress = ?",
+# (email,),
+# )
+# return Subdomains(**row) if row else None
+
+
+async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddresses]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM smtp.emailaddress WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ return [Emailaddresses(**row) for row in rows]
+
+
+async def delete_emailaddress(emailaddress_id: str) -> None:
+ await db.execute("DELETE FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,))
+
+
+## create emails
+async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails:
+ await db.execute(
+ """
+ INSERT INTO smtp.email (id, wallet, emailaddress_id, subject, receiver, message, paid)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ payment_hash,
+ wallet,
+ data.emailaddress_id,
+ data.subject,
+ data.receiver,
+ data.message,
+ False,
+ ),
+ )
+
+ new_email = await get_email(payment_hash)
+ assert new_email, "Newly created email couldn't be retrieved"
+ return new_email
+
+
+async def set_email_paid(payment_hash: str) -> Emails:
+ email = await get_email(payment_hash)
+ if email and email.paid == False:
+ await db.execute(
+ """
+ UPDATE smtp.email
+ SET paid = true
+ WHERE id = ?
+ """,
+ (payment_hash,),
+ )
+ new_email = await get_email(payment_hash)
+ assert new_email, "Newly paid email couldn't be retrieved"
+ return new_email
+
+
+async def get_email(email_id: str) -> Optional[Emails]:
+ row = await db.fetchone(
+ "SELECT s.*, d.email as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.id = ?",
+ (email_id,),
+ )
+ return Emails(**row) if row else None
+
+
+async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT s.*, d.email as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.wallet IN ({q})",
+ (*wallet_ids,),
+ )
+
+ return [Emails(**row) for row in rows]
+
+
+async def delete_email(email_id: str) -> None:
+ await db.execute("DELETE FROM smtp.email WHERE id = ?", (email_id,))
diff --git a/lnbits/extensions/smtp/migrations.py b/lnbits/extensions/smtp/migrations.py
new file mode 100644
index 00000000..16d50166
--- /dev/null
+++ b/lnbits/extensions/smtp/migrations.py
@@ -0,0 +1,35 @@
+async def m001_initial(db):
+
+ await db.execute(
+ f"""
+ CREATE TABLE smtp.emailaddress (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ email TEXT NOT NULL,
+ testemail TEXT NOT NULL,
+ smtp_server TEXT NOT NULL,
+ smtp_user TEXT NOT NULL,
+ smtp_password TEXT NOT NULL,
+ smtp_port TEXT NOT NULL,
+ anonymize BOOLEAN NOT NULL,
+ description TEXT NOT NULL,
+ cost INTEGER NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
+ );
+ """
+ )
+
+ await db.execute(
+ f"""
+ CREATE TABLE smtp.email (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ emailaddress_id TEXT NOT NULL,
+ subject TEXT NOT NULL,
+ receiver TEXT NOT NULL,
+ message TEXT NOT NULL,
+ paid BOOLEAN NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
+ );
+ """
+ )
diff --git a/lnbits/extensions/smtp/models.py b/lnbits/extensions/smtp/models.py
new file mode 100644
index 00000000..e2f3fc13
--- /dev/null
+++ b/lnbits/extensions/smtp/models.py
@@ -0,0 +1,47 @@
+from fastapi import Query
+from pydantic import BaseModel
+
+
+class CreateEmailaddress(BaseModel):
+ wallet: str = Query(...)
+ email: str = Query(...)
+ testemail: str = Query(...)
+ smtp_server: str = Query(...)
+ smtp_user: str = Query(...)
+ smtp_password: str = Query(...)
+ smtp_port: str = Query(...)
+ description: str = Query(...)
+ anonymize: bool
+ cost: int = Query(..., ge=0)
+
+
+class Emailaddresses(BaseModel):
+ id: str
+ wallet: str
+ email: str
+ testemail: str
+ smtp_server: str
+ smtp_user: str
+ smtp_password: str
+ smtp_port: str
+ anonymize: bool
+ description: str
+ cost: int
+
+
+class CreateEmail(BaseModel):
+ emailaddress_id: str = Query(...)
+ subject: str = Query(...)
+ receiver: str = Query(...)
+ message: str = Query(...)
+
+
+class Emails(BaseModel):
+ id: str
+ wallet: str
+ emailaddress_id: str
+ subject: str
+ receiver: str
+ message: str
+ paid: bool
+ time: int
diff --git a/lnbits/extensions/smtp/smtp.py b/lnbits/extensions/smtp/smtp.py
new file mode 100644
index 00000000..e77bc0fa
--- /dev/null
+++ b/lnbits/extensions/smtp/smtp.py
@@ -0,0 +1,86 @@
+import re
+import socket
+import time
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from email.utils import formatdate
+from http import HTTPStatus
+from smtplib import SMTP_SSL as SMTP
+
+from loguru import logger
+from starlette.exceptions import HTTPException
+
+
+def valid_email(s):
+ # https://regexr.com/2rhq7
+ pat = "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
+ if re.match(pat, s):
+ return True
+ msg = f"SMTP - invalid email: {s}."
+ logger.error(msg)
+ raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
+
+
+async def send_mail(emailaddress, email):
+ valid_email(emailaddress.email)
+ valid_email(email.receiver)
+
+ ts = time.time()
+ date = formatdate(ts, True)
+
+ msg = MIMEMultipart("alternative")
+ msg = MIMEMultipart("alternative")
+ msg["Date"] = date
+ msg["Subject"] = email.subject
+ msg["From"] = emailaddress.email
+ msg["To"] = email.receiver
+
+ signature = "Email sent anonymiously by LNbits Sendmail extension."
+ text = f"""
+{email.message}
+
+{signature}
+"""
+
+ html = f"""
+
+
+
+ {email.message}
+
+
{signature}
+
+
+"""
+
+ part1 = MIMEText(text, "plain")
+ part2 = MIMEText(html, "html")
+ msg.attach(part1)
+ msg.attach(part2)
+
+ try:
+ conn = SMTP(
+ host=emailaddress.smtp_server, port=emailaddress.smtp_port, timeout=10
+ )
+ logger.debug("SMTP - connected to smtp server.")
+ # conn.set_debuglevel(True)
+ except:
+ msg = f"SMTP - error connecting to smtp server: {emailaddress.smtp_server}:{emailaddress.smtp_port}."
+ logger.error(msg)
+ raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
+ try:
+ conn.login(emailaddress.smtp_user, emailaddress.smtp_password)
+ logger.debug("SMTP - successful login to smtp server.")
+ except:
+ msg = f"SMTP - error login into smtp {emailaddress.smtp_user}."
+ logger.error(msg)
+ raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
+ try:
+ conn.sendmail(emailaddress.email, email.receiver, msg.as_string())
+ logger.debug("SMTP - successfully send email.")
+ except socket.error as e:
+ msg = f"SMTP - error sending email: {str(e)}."
+ logger.error(msg)
+ raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
+ finally:
+ conn.quit()
diff --git a/lnbits/extensions/smtp/static/smtp-bitcoin-email.png b/lnbits/extensions/smtp/static/smtp-bitcoin-email.png
new file mode 100644
index 00000000..e80b6c9a
Binary files /dev/null and b/lnbits/extensions/smtp/static/smtp-bitcoin-email.png differ
diff --git a/lnbits/extensions/smtp/tasks.py b/lnbits/extensions/smtp/tasks.py
new file mode 100644
index 00000000..9c544473
--- /dev/null
+++ b/lnbits/extensions/smtp/tasks.py
@@ -0,0 +1,36 @@
+import asyncio
+
+from loguru import logger
+
+from lnbits.core.models import Payment
+from lnbits.tasks import register_invoice_listener
+
+from .crud import get_email, get_emailaddress, set_email_paid
+from .smtp import send_mail
+
+
+async def wait_for_paid_invoices():
+ invoice_queue = asyncio.Queue()
+ register_invoice_listener(invoice_queue)
+ while True:
+ payment = await invoice_queue.get()
+ await on_invoice_paid(payment)
+
+
+async def on_invoice_paid(payment: Payment) -> None:
+ if payment.extra.get("tag") != "smtp":
+ return
+
+ email = await get_email(payment.checking_id)
+ if not email:
+ logger.error("SMTP: email can not by fetched")
+ return
+
+ emailaddress = await get_emailaddress(email.emailaddress_id)
+ if not emailaddress:
+ logger.error("SMTP: emailaddress can not by fetched")
+ return
+
+ await payment.set_pending(False)
+ await send_mail(emailaddress, email)
+ await set_email_paid(payment_hash=payment.payment_hash)
diff --git a/lnbits/extensions/smtp/templates/smtp/_api_docs.html b/lnbits/extensions/smtp/templates/smtp/_api_docs.html
new file mode 100644
index 00000000..cfb811d1
--- /dev/null
+++ b/lnbits/extensions/smtp/templates/smtp/_api_docs.html
@@ -0,0 +1,23 @@
+
+
+
+
+ LNBits SMTP: Get paid sats to send emails
+
+
+ Charge people for using sending an email via your smtp server
+ More details
+
+ Created by, dni
+
+
+
+
diff --git a/lnbits/extensions/smtp/templates/smtp/display.html b/lnbits/extensions/smtp/templates/smtp/display.html
new file mode 100644
index 00000000..7db4a0d6
--- /dev/null
+++ b/lnbits/extensions/smtp/templates/smtp/display.html
@@ -0,0 +1,185 @@
+{% extends "public.html" %} {% block page %}
+
+
+
+
+ {{ email }}
+
+ {{ desc }}
+
+
+
+
+
+ Total cost: {{ cost }} sats
+
+ Submit
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+ Copy invoice
+ Close
+
+
+
+
+
+{% endblock %} {% block scripts %}
+
+{% endblock %}
diff --git a/lnbits/extensions/smtp/templates/smtp/index.html b/lnbits/extensions/smtp/templates/smtp/index.html
new file mode 100644
index 00000000..bf43ad7f
--- /dev/null
+++ b/lnbits/extensions/smtp/templates/smtp/index.html
@@ -0,0 +1,528 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+
+
+
+
+ New Emailaddress
+
+
+
+
+
+
+
+
Emailaddresses
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
Emails
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+ {{SITE_TITLE}} Sendmail extension
+
+
+
+
+ {% include "smtp/_api_docs.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update Form
+ Create Emailaddress
+ Cancel
+
+
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+{% endblock %}
diff --git a/lnbits/extensions/smtp/views.py b/lnbits/extensions/smtp/views.py
new file mode 100644
index 00000000..df208a77
--- /dev/null
+++ b/lnbits/extensions/smtp/views.py
@@ -0,0 +1,40 @@
+from http import HTTPStatus
+
+from fastapi import Depends, HTTPException, Request
+from fastapi.templating import Jinja2Templates
+from starlette.responses import HTMLResponse
+
+from lnbits.core.models import User
+from lnbits.decorators import check_user_exists
+
+from . import smtp_ext, smtp_renderer
+from .crud import get_emailaddress
+
+templates = Jinja2Templates(directory="templates")
+
+
+@smtp_ext.get("/", response_class=HTMLResponse)
+async def index(request: Request, user: User = Depends(check_user_exists)):
+ return smtp_renderer().TemplateResponse(
+ "smtp/index.html", {"request": request, "user": user.dict()}
+ )
+
+
+@smtp_ext.get("/{emailaddress_id}")
+async def display(request: Request, emailaddress_id):
+ emailaddress = await get_emailaddress(emailaddress_id)
+ if not emailaddress:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Emailaddress does not exist."
+ )
+
+ return smtp_renderer().TemplateResponse(
+ "smtp/display.html",
+ {
+ "request": request,
+ "emailaddress_id": emailaddress.id,
+ "email": emailaddress.email,
+ "desc": emailaddress.description,
+ "cost": emailaddress.cost,
+ },
+ )
diff --git a/lnbits/extensions/smtp/views_api.py b/lnbits/extensions/smtp/views_api.py
new file mode 100644
index 00000000..08a05ef3
--- /dev/null
+++ b/lnbits/extensions/smtp/views_api.py
@@ -0,0 +1,170 @@
+from http import HTTPStatus
+
+from fastapi import Depends, HTTPException, Query
+
+from lnbits.core.crud import get_user
+from lnbits.core.services import check_transaction_status, create_invoice
+from lnbits.decorators import WalletTypeInfo, get_key_type
+from lnbits.extensions.smtp.models import CreateEmail, CreateEmailaddress
+
+from . import smtp_ext
+from .crud import (
+ create_email,
+ create_emailaddress,
+ delete_email,
+ delete_emailaddress,
+ get_email,
+ get_emailaddress,
+ get_emailaddresses,
+ get_emails,
+ update_emailaddress,
+)
+from .smtp import valid_email
+
+
+## EMAILS
+@smtp_ext.get("/api/v1/email")
+async def api_email(
+ g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
+):
+ wallet_ids = [g.wallet.id]
+ if all_wallets:
+ user = await get_user(g.wallet.user)
+ if user:
+ wallet_ids = user.wallet_ids
+ return [email.dict() for email in await get_emails(wallet_ids)]
+
+
+@smtp_ext.get("/api/v1/email/{payment_hash}")
+async def api_smtp_send_email(payment_hash):
+ email = await get_email(payment_hash)
+ if not email:
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST, detail="paymenthash is wrong"
+ )
+
+ emailaddress = await get_emailaddress(email.emailaddress_id)
+
+ try:
+ status = await check_transaction_status(email.wallet, payment_hash)
+ is_paid = not status.pending
+ except Exception:
+ return {"paid": False}
+ if is_paid:
+ if emailaddress.anonymize:
+ await delete_email(email.id)
+ return {"paid": True}
+ return {"paid": False}
+
+
+@smtp_ext.post("/api/v1/email/{emailaddress_id}")
+async def api_smtp_make_email(emailaddress_id, data: CreateEmail):
+
+ valid_email(data.receiver)
+
+ emailaddress = await get_emailaddress(emailaddress_id)
+ # If the request is coming for the non-existant emailaddress
+ if not emailaddress:
+ raise HTTPException(
+ status_code=HTTPStatus.BAD_REQUEST,
+ detail="Emailaddress address does not exist.",
+ )
+ try:
+ memo = f"sent email from {emailaddress.email} to {data.receiver}"
+ if emailaddress.anonymize:
+ memo = "sent email"
+
+ payment_hash, payment_request = await create_invoice(
+ wallet_id=emailaddress.wallet,
+ amount=emailaddress.cost,
+ memo=memo,
+ extra={"tag": "smtp"},
+ )
+ except Exception as e:
+ raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
+
+ email = await create_email(
+ payment_hash=payment_hash, wallet=emailaddress.wallet, data=data
+ )
+
+ if not email:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Email could not be fetched."
+ )
+ return {"payment_hash": payment_hash, "payment_request": payment_request}
+
+
+@smtp_ext.delete("/api/v1/email/{email_id}")
+async def api_email_delete(email_id, g: WalletTypeInfo = Depends(get_key_type)):
+ email = await get_email(email_id)
+
+ if not email:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="LNsubdomain does not exist."
+ )
+
+ if email.wallet != g.wallet.id:
+ raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your email.")
+
+ await delete_email(email_id)
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+## EMAILADDRESSES
+@smtp_ext.get("/api/v1/emailaddress")
+async def api_emailaddresses(
+ g: WalletTypeInfo = Depends(get_key_type),
+ all_wallets: bool = Query(False),
+):
+ wallet_ids = [g.wallet.id]
+ if all_wallets:
+ user = await get_user(g.wallet.user)
+ if user:
+ wallet_ids = user.wallet_ids
+ return [
+ emailaddress.dict() for emailaddress in await get_emailaddresses(wallet_ids)
+ ]
+
+
+@smtp_ext.post("/api/v1/emailaddress")
+@smtp_ext.put("/api/v1/emailaddress/{emailaddress_id}")
+async def api_emailaddress_create(
+ data: CreateEmailaddress,
+ emailaddress_id=None,
+ g: WalletTypeInfo = Depends(get_key_type),
+):
+ if emailaddress_id:
+ emailaddress = await get_emailaddress(emailaddress_id)
+
+ if not emailaddress:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Emailadress does not exist."
+ )
+ if emailaddress.wallet != g.wallet.id:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN, detail="Not your emailaddress."
+ )
+
+ emailaddress = await update_emailaddress(emailaddress_id, **data.dict())
+ else:
+ emailaddress = await create_emailaddress(data=data)
+ return emailaddress.dict()
+
+
+@smtp_ext.delete("/api/v1/emailaddress/{emailaddress_id}")
+async def api_emailaddress_delete(
+ emailaddress_id, g: WalletTypeInfo = Depends(get_key_type)
+):
+ emailaddress = await get_emailaddress(emailaddress_id)
+
+ if not emailaddress:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Emailaddress does not exist."
+ )
+ if emailaddress.wallet != g.wallet.id:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN, detail="Not your Emailaddress."
+ )
+
+ await delete_emailaddress(emailaddress_id)
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
diff --git a/lnbits/extensions/tipjar/crud.py b/lnbits/extensions/tipjar/crud.py
index 1b58a43d..080eaf1c 100644
--- a/lnbits/extensions/tipjar/crud.py
+++ b/lnbits/extensions/tipjar/crud.py
@@ -33,7 +33,11 @@ async def create_tip(
async def create_tipjar(data: createTipJar) -> TipJar:
"""Create a new TipJar"""
- await db.execute(
+
+ returning = "" if db.type == SQLITE else "RETURNING ID"
+ method = db.execute if db.type == SQLITE else db.fetchone
+
+ result = await (method)(
f"""
INSERT INTO tipjar.TipJars (
name,
@@ -42,11 +46,16 @@ async def create_tipjar(data: createTipJar) -> TipJar:
onchain
)
VALUES (?, ?, ?, ?)
+ {returning}
""",
(data.name, data.wallet, data.webhook, data.onchain),
)
- row = await db.fetchone("SELECT * FROM tipjar.TipJars LIMIT 1")
- tipjar = TipJar(**row)
+ if db.type == SQLITE:
+ tipjar_id = result._result_proxy.lastrowid
+ else:
+ tipjar_id = result[0]
+
+ tipjar = await get_tipjar(tipjar_id)
assert tipjar
return tipjar
diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py
index 80ee1085..4b7bd9f9 100644
--- a/lnbits/extensions/tpos/tasks.py
+++ b/lnbits/extensions/tpos/tasks.py
@@ -20,9 +20,6 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if not payment.extra:
- return
-
if payment.extra.get("tag") != "tpos":
return
diff --git a/lnbits/extensions/watchonly/crud.py b/lnbits/extensions/watchonly/crud.py
index 61e47cfe..1d9abcec 100644
--- a/lnbits/extensions/watchonly/crud.py
+++ b/lnbits/extensions/watchonly/crud.py
@@ -41,8 +41,9 @@ async def create_watch_wallet(user: str, w: WalletAccount) -> WalletAccount:
w.meta,
),
)
-
- return await get_watch_wallet(wallet_id)
+ wallet = await get_watch_wallet(wallet_id)
+ assert wallet
+ return wallet
async def get_watch_wallet(wallet_id: str) -> Optional[WalletAccount]:
@@ -121,11 +122,11 @@ async def create_fresh_addresses(
change_address=False,
) -> List[Address]:
if start_address_index > end_address_index:
- return None
+ return []
wallet = await get_watch_wallet(wallet_id)
if not wallet:
- return None
+ return []
branch_index = 1 if change_address else 0
@@ -150,7 +151,7 @@ async def create_fresh_addresses(
# return fresh addresses
rows = await db.fetchall(
"""
- SELECT * FROM watchonly.addresses
+ SELECT * FROM watchonly.addresses
WHERE wallet = ? AND branch_index = ? AND address_index >= ? AND address_index < ?
ORDER BY branch_index, address_index
""",
@@ -172,7 +173,7 @@ async def get_address_at_index(
) -> Optional[Address]:
row = await db.fetchone(
"""
- SELECT * FROM watchonly.addresses
+ SELECT * FROM watchonly.addresses
WHERE wallet = ? AND branch_index = ? AND address_index = ?
""",
(
diff --git a/lnbits/extensions/watchonly/helpers.py b/lnbits/extensions/watchonly/helpers.py
index 74125dde..8db9ff57 100644
--- a/lnbits/extensions/watchonly/helpers.py
+++ b/lnbits/extensions/watchonly/helpers.py
@@ -1,6 +1,6 @@
-from embit.descriptor import Descriptor, Key # type: ignore
-from embit.descriptor.arguments import AllowedDerivation # type: ignore
-from embit.networks import NETWORKS # type: ignore
+from embit.descriptor import Descriptor, Key
+from embit.descriptor.arguments import AllowedDerivation
+from embit.networks import NETWORKS
def detect_network(k):
diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py
index c6265d6c..24d63bfd 100644
--- a/lnbits/extensions/watchonly/models.py
+++ b/lnbits/extensions/watchonly/models.py
@@ -1,7 +1,7 @@
from sqlite3 import Row
from typing import List, Optional
-from fastapi.param_functions import Query
+from fastapi import Query
from pydantic import BaseModel
@@ -35,7 +35,7 @@ class Address(BaseModel):
amount: int = 0
branch_index: int = 0
address_index: int
- note: str = None
+ note: Optional[str] = None
has_activity: bool = False
@classmethod
@@ -57,9 +57,9 @@ class TransactionInput(BaseModel):
class TransactionOutput(BaseModel):
amount: int
address: str
- branch_index: int = None
- address_index: int = None
- wallet: str = None
+ branch_index: Optional[int] = None
+ address_index: Optional[int] = None
+ wallet: Optional[str] = None
class MasterPublicKey(BaseModel):
diff --git a/lnbits/extensions/watchonly/views.py b/lnbits/extensions/watchonly/views.py
index 819d1248..8cebc6cc 100644
--- a/lnbits/extensions/watchonly/views.py
+++ b/lnbits/extensions/watchonly/views.py
@@ -1,6 +1,5 @@
-from fastapi.params import Depends
+from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
-from starlette.requests import Request
from starlette.responses import HTMLResponse
from lnbits.core.models import User
diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py
index c6e15ea6..a7086423 100644
--- a/lnbits/extensions/watchonly/views_api.py
+++ b/lnbits/extensions/watchonly/views_api.py
@@ -1,5 +1,6 @@
import json
from http import HTTPStatus
+from typing import List
import httpx
from embit import finalizer, script
@@ -7,9 +8,7 @@ from embit.ec import PublicKey
from embit.networks import NETWORKS
from embit.psbt import PSBT, DerivationPath
from embit.transaction import Transaction, TransactionInput, TransactionOutput
-from fastapi import Query, Request
-from fastapi.params import Depends
-from starlette.exceptions import HTTPException
+from fastapi import Depends, HTTPException, Query, Request
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.watchonly import watchonly_ext
@@ -57,10 +56,8 @@ async def api_wallets_retrieve(
return []
-@watchonly_ext.get("/api/v1/wallet/{wallet_id}")
-async def api_wallet_retrieve(
- wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
-):
+@watchonly_ext.get("/api/v1/wallet/{wallet_id}", dependencies=[Depends(get_key_type)])
+async def api_wallet_retrieve(wallet_id: str):
w_wallet = await get_watch_wallet(wallet_id)
if not w_wallet:
@@ -126,8 +123,10 @@ async def api_wallet_create_or_update(
return wallet.dict()
-@watchonly_ext.delete("/api/v1/wallet/{wallet_id}")
-async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin_key)):
+@watchonly_ext.delete(
+ "/api/v1/wallet/{wallet_id}", dependencies=[Depends(require_admin_key)]
+)
+async def api_wallet_delete(wallet_id: str):
wallet = await get_watch_wallet(wallet_id)
if not wallet:
@@ -144,16 +143,15 @@ async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin
#############################ADDRESSES##########################
-@watchonly_ext.get("/api/v1/address/{wallet_id}")
-async def api_fresh_address(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
+@watchonly_ext.get("/api/v1/address/{wallet_id}", dependencies=[Depends(get_key_type)])
+async def api_fresh_address(wallet_id: str):
address = await get_fresh_address(wallet_id)
+ assert address
return address.dict()
-@watchonly_ext.put("/api/v1/address/{id}")
-async def api_update_address(
- id: str, req: Request, w: WalletTypeInfo = Depends(require_admin_key)
-):
+@watchonly_ext.put("/api/v1/address/{id}", dependencies=[Depends(require_admin_key)])
+async def api_update_address(id: str, req: Request):
body = await req.json()
params = {}
# amout is only updated if the address has history
@@ -162,9 +160,10 @@ async def api_update_address(
params["has_activity"] = True
if "note" in body:
- params["note"] = str(body["note"])
+ params["note"] = body["note"]
address = await update_address(**params, id=id)
+ assert address
wallet = (
await get_watch_wallet(address.wallet)
@@ -189,6 +188,7 @@ async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)
addresses = await get_addresses(wallet_id)
config = await get_config(w.wallet.user)
+ assert config
if not addresses:
await create_fresh_addresses(wallet_id, 0, config.receive_gap_limit)
@@ -229,10 +229,8 @@ async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)
#############################PSBT##########################
-@watchonly_ext.post("/api/v1/psbt")
-async def api_psbt_create(
- data: CreatePsbt, w: WalletTypeInfo = Depends(require_admin_key)
-):
+@watchonly_ext.post("/api/v1/psbt", dependencies=[Depends(require_admin_key)])
+async def api_psbt_create(data: CreatePsbt):
try:
vin = [
TransactionInput(bytes.fromhex(inp.tx_id), inp.vout) for inp in data.inputs
@@ -246,7 +244,7 @@ async def api_psbt_create(
for _, masterpub in enumerate(data.masterpubs):
descriptors[masterpub.id] = parse_key(masterpub.public_key)
- inputs_extra = []
+ inputs_extra: List[dict] = []
for i, inp in enumerate(data.inputs):
bip32_derivations = {}
@@ -266,14 +264,15 @@ async def api_psbt_create(
tx = Transaction(vin=vin, vout=vout)
psbt = PSBT(tx)
- for i, inp in enumerate(inputs_extra):
- psbt.inputs[i].bip32_derivations = inp["bip32_derivations"]
- psbt.inputs[i].non_witness_utxo = inp.get("non_witness_utxo", None)
+ for i, inp_extra in enumerate(inputs_extra):
+ psbt.inputs[i].bip32_derivations = inp_extra["bip32_derivations"]
+ psbt.inputs[i].non_witness_utxo = inp_extra.get("non_witness_utxo", None)
outputs_extra = []
bip32_derivations = {}
for i, out in enumerate(data.outputs):
if out.branch_index == 1:
+ assert out.wallet
descriptor = descriptors[out.wallet][0]
d = descriptor.derive(out.address_index, out.branch_index)
for k in d.keys:
@@ -282,8 +281,8 @@ async def api_psbt_create(
)
outputs_extra.append({"bip32_derivations": bip32_derivations})
- for i, out in enumerate(outputs_extra):
- psbt.outputs[i].bip32_derivations = out["bip32_derivations"]
+ for i, out_extra in enumerate(outputs_extra):
+ psbt.outputs[i].bip32_derivations = out_extra["bip32_derivations"]
return psbt.to_string()
@@ -360,7 +359,8 @@ async def api_tx_broadcast(
else config.mempool_endpoint + "/testnet"
)
async with httpx.AsyncClient() as client:
- r = await client.post(endpoint + "/api/tx", data=data.tx_hex)
+ r = await client.post(endpoint + "/api/tx", content=data.tx_hex)
+ r.raise_for_status()
tx_id = r.text
return tx_id
except Exception as e:
@@ -375,6 +375,7 @@ async def api_update_config(
data: Config, w: WalletTypeInfo = Depends(require_admin_key)
):
config = await update_config(data, user=w.wallet.user)
+ assert config
return config.dict()
diff --git a/pyproject.toml b/pyproject.toml
index 03dbbc8d..c3026c6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,8 +92,6 @@ exclude = """(?x)(
^lnbits/extensions/bleskomat.
| ^lnbits/extensions/boltz.
| ^lnbits/extensions/livestream.
- | ^lnbits/extensions/lnurldevice.
- | ^lnbits/extensions/watchonly.
| ^lnbits/wallets/lnd_grpc_files.
)"""
diff --git a/tests/data/mock_data.zip b/tests/data/mock_data.zip
index 4070bee7..d5169e12 100644
Binary files a/tests/data/mock_data.zip and b/tests/data/mock_data.zip differ