Merge branch 'main' into fix/mypy-bleskomat

This commit is contained in:
calle 2023-01-11 17:50:37 +01:00 committed by GitHub
commit affcb9feca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 4196 additions and 1481 deletions

View file

@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

View file

@ -23,7 +23,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

View file

@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

View file

@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@ -51,7 +51,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@ -95,7 +95,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

View file

@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@ -31,7 +31,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@ -67,7 +67,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

View file

@ -7,29 +7,29 @@ LNbits
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png)
# 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
![lnurl ATM](https://i.imgur.com/Gi6bn3L.jpg)
## 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".
![lnurl ATM](https://i.imgur.com/xFWDnwy.png)
## 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/

View file

@ -28,7 +28,9 @@ Going over the example extension's structure:
Adding new dependencies
-----------------------
If for some reason your extensions needs a new python package to work, you can add a new package using `venv`, or `poerty`:
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `venv`, or `poerty`:
```sh
$ poetry add <package>
@ -37,8 +39,7 @@ $ ./venv/bin/pip install <package>
```
**But we need an extra step to make sure LNbits doesn't break in production.**
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry`.
`nix` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
SQLite to PostgreSQL migration

View file

@ -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:

View file

@ -4,12 +4,12 @@ import time
from decimal import Decimal
from typing import List, NamedTuple, Optional
import bitstring # type: ignore
import bitstring
import embit
import secp256k1
from bech32 import CHARSET, bech32_decode, bech32_encode
from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string # type: ignore
from ecdsa import SECP256k1, VerifyingKey
from ecdsa.util import sigdecode_string
class Route(NamedTuple):

View file

@ -1,7 +1,7 @@
import datetime
from loguru import logger
from sqlalchemy.exc import OperationalError # type: ignore
from sqlalchemy.exc import OperationalError
from lnbits import bolt11

View file

@ -6,9 +6,9 @@ import time
from sqlite3 import Row
from typing import Dict, List, Optional
from ecdsa import SECP256k1, SigningKey # type: ignore
from ecdsa import SECP256k1, SigningKey
from fastapi import Query
from lnurl import encode as lnurl_encode # type: ignore
from lnurl import encode as lnurl_encode
from loguru import logger
from pydantic import BaseModel

View file

@ -7,7 +7,7 @@ from urllib.parse import parse_qs, urlparse
import httpx
from fastapi import Depends, WebSocket
from lnurl import LnurlErrorResponse
from lnurl import decode as decode_lnurl # type: ignore
from lnurl import decode as decode_lnurl
from loguru import logger
from lnbits import bolt11
@ -44,7 +44,7 @@ from .crud import (
from .models import Payment
try:
from typing import TypedDict # type: ignore
from typing import TypedDict
except ImportError: # pragma: nocover
from typing_extensions import TypedDict

View file

@ -141,15 +141,15 @@
return {
settings: {},
lnbits_theme_options: [
'classic',
'bitcoin',
'flamingo',
'freedom',
'mint',
'autumn',
'monochrome',
'salvador'
],
'classic',
'bitcoin',
'flamingo',
'freedom',
'mint',
'autumn',
'monochrome',
'salvador'
],
formData: {},
formAddAdmin: '',
formAddUser: '',
@ -204,11 +204,11 @@
value: null,
label: 'Certificate'
},
lnd_admin_macaroon: {
lnd_rest_admin_macaroon: {
value: null,
label: 'Admin Macaroon'
},
lnd_invoice_macaroon: {
lnd_rest_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
}

View file

@ -16,7 +16,7 @@ from ..tasks import api_invoice_listeners
@core_app.get("/.well-known/lnurlp/{username}")
async def lnaddress(username: str, request: Request):
from lnbits.extensions.lnaddress.lnurl import lnurl_response
from lnbits.extensions.lnaddress.lnurl import lnurl_response # type: ignore
domain = urlparse(str(request.url)).netloc
return await lnurl_response(username, domain, request)

View file

@ -9,7 +9,7 @@ from typing import Optional
from loguru import logger
from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY
from lnbits.settings import settings
@ -129,7 +129,7 @@ class Database(Compat):
else:
self.type = POSTGRES
import psycopg2 # type: ignore
import psycopg2
def _parse_timestamp(value, _):
if value is None:

View file

@ -3,7 +3,7 @@ from typing import Dict, List, Optional
from fastapi.params import Query
from pydantic.main import BaseModel
from sqlalchemy.engine import base # type: ignore
from sqlalchemy.engine import base
class SubmarineSwap(BaseModel):
@ -24,9 +24,9 @@ class SubmarineSwap(BaseModel):
class CreateSubmarineSwap(BaseModel):
wallet: str = Query(...) # type: ignore
refund_address: str = Query(...) # type: ignore
amount: int = Query(...) # type: ignore
wallet: str = Query(...)
refund_address: str = Query(...)
amount: int = Query(...)
class ReverseSubmarineSwap(BaseModel):
@ -48,13 +48,13 @@ class ReverseSubmarineSwap(BaseModel):
class CreateReverseSubmarineSwap(BaseModel):
wallet: str = Query(...) # type: ignore
amount: int = Query(...) # type: ignore
instant_settlement: bool = Query(...) # type: ignore
wallet: str = Query(...)
amount: int = Query(...)
instant_settlement: bool = Query(...)
# validate on-address, bcrt1 for regtest addresses
onchain_address: str = Query(
..., regex="^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$"
) # type: ignore
)
class SwapStatus(BaseModel):

View file

@ -111,7 +111,7 @@ async def api_submarineswap(
)
async def api_submarineswap_refund(
swap_id: str,
g: WalletTypeInfo = Depends(require_admin_key), # type: ignore
g: WalletTypeInfo = Depends(require_admin_key),
):
if swap_id == None:
raise HTTPException(
@ -160,7 +160,7 @@ async def api_submarineswap_refund(
)
async def api_submarineswap_create(
data: CreateSubmarineSwap,
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
wallet: WalletTypeInfo = Depends(require_admin_key),
):
try:
swap_data = await create_swap(data)
@ -257,7 +257,7 @@ async def api_reverse_submarineswap_create(
},
)
async def api_swap_status(
swap_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # type: ignore
swap_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap(
swap_id
@ -290,7 +290,7 @@ async def api_swap_status(
response_description="list of pending swaps",
)
async def api_check_swaps(
g: WalletTypeInfo = Depends(require_admin_key), # type: ignore
g: WalletTypeInfo = Depends(require_admin_key),
all_wallets: bool = Query(False),
):
wallet_ids = [g.wallet.id]

View file

@ -1,6 +1,6 @@
import asyncio
from environs import Env # type: ignore
from environs import Env
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles

View file

@ -6,7 +6,7 @@ from fastapi import Request
from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse # type: ignore
from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice

View file

@ -4,11 +4,11 @@ from typing import Dict, Optional
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata # type: ignore
from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
from starlette.requests import Request
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnbits.lnurl import encode as lnurl_encode
class CreateCopilotData(BaseModel):

View file

@ -2,7 +2,7 @@ from typing import List
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse # type: ignore
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,6 @@
{
"name": "Deezy",
"short_description": "LN to onchain, onchain to LN swaps",
"tile": "/deezy/static/deezy.png",
"contributors": ["Uthpala"]
}

View file

@ -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,
),
)

View file

@ -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
);
"""
)

View file

@ -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 = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,253 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="About Deezy"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<img
alt=""
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTMwLjA5IDEzNi43MyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fS5jbHMtMntmaWxsLXJ1bGU6ZXZlbm9kZDtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ze2ZpbGw6I2ZmYzkyYjt9PC9zdHlsZT48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudCIgeDE9IjUxLjY5IiB5MT0iMzEuNjciIHgyPSIxODAuMjMiIHkyPSIxMDUuMTIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmZmYyMWYiLz48c3RvcCBvZmZzZXQ9IjAuMjkiIHN0b3AtY29sb3I9IiNmZmNkMmQiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmNzkyMzMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTYxLjg5LDBoNTcuNTVDMTMzLjksMCwxNDUsMS40NCwxNTIuOTIsNC4zM2MxNC4yMSw1LjA1LDIzLjYsMTQuMTgsMjguNjYsMjcuNjRMMTUyLjY4LDQ2LjRsLS4yMy0uNDhjLTIuMTgtNi43NC01LjA2LTExLjU0LTguNDMtMTQuOUEyNS40MywyNS40MywwLDAsMCwxMzIsMjUuNDlsLS4yNC0yLjg5LTMuMTMsMi4xNmE1NC4xMSw1NC4xMSwwLDAsMC05LjE2LS40OEg5MC43OVY1MUw2MS44OSw3MC42OFptMTI1LDU0LjgxQTEyNC43NiwxMjQuNzYsMCwwLDEsMTg3LjYsNjhhMTA4LjM4LDEwOC4zOCwwLDAsMS01LjMsMzQuNjJjLTMuMzcsMTEuMy05LjM5LDE5LjQ3LTE3LjU4LDI0Ljc2YTQ2LjE4LDQ2LjE4LDAsMCwxLTE3LjA5LDYuNDljLTYsMS4yLTE1LjQxLDEuNjgtMjguMTksMS42OEg2MS44OVY5OS4yOWwyOC45LTE0LjQzdjI2LjY5aDExLjU2bC4yNCwyLjE2LDMuMzctMi4xNmgxMy40OGMxMi43OCwwLDIxLjQ0LTIuODksMjYuMjYtOC40MiwzLjEzLTMuNiw1LjU0LTguNDEsNy4yMi0xNC45YTU0LjI4LDU0LjI4LDAsMCwwLDIuNDEtMTEuM1oiLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMCAxMjIuMTMgMTI1LjcxIDM1LjU4IDEyOC44NSA2Ni41OSAyMzEuOTIgMTQuNjcgMTA4LjM3IDEwMC45NyAxMDQuNzYgNjkuNzEgMCAxMjIuMTMiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yNjYuNjksMjguNjhoMTN2ODRoLTEzVjEwNHEtNy4zMiwxMC4yLTIxLDEwLjJhMjguMTQsMjguMTQsMCwwLDEtMjEuMTItOS4xOCwzMS4yMSwzMS4yMSwwLDAsMS04Ljc2LTIyLjM4LDMxLjE1LDMxLjE1LDAsMCwxLDguNzYtMjIuNDQsMjguMjMsMjguMjMsMCwwLDEsMjEuMTItOS4xMnExMy42OCwwLDIxLDEwLjA4Wk0yMzQuMTcsOTYuNDJhMTkuNTcsMTkuNTcsMCwwLDAsMjcuMTIsMCwxOC43NCwxOC43NCwwLDAsMCw1LjQtMTMuNzQsMTguNzQsMTguNzQsMCwwLDAtNS40LTEzLjc0LDE5LjU3LDE5LjU3LDAsMCwwLTI3LjEyLDAsMTguNzQsMTguNzQsMCwwLDAtNS40LDEzLjc0QTE4Ljc0LDE4Ljc0LDAsMCwwLDIzNC4xNyw5Ni40MloiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0zMDIsODguMmExNi40OCwxNi40OCwwLDAsMCw2LjYsMTAuNSwyMS4yMiwyMS4yMiwwLDAsMCwxMi42LDMuNjZxMTAuMzIsMCwxNS40OC03LjQ0bDEwLjY4LDYuMjRxLTguODgsMTMuMDgtMjYuMjgsMTMuMDgtMTQuNjQsMC0yMy42NC04Ljk0dC05LTIyLjYycTAtMTMuNDQsOC44OC0yMi41dDIyLjgtOS4wNnExMy4yLDAsMjEuNjYsOS4yNGEzMiwzMiwwLDAsMSw4LjQ2LDIyLjQ0LDQwLjA5LDQwLjA5LDAsMCwxLS40OCw1LjRabS0uMTItMTAuNTZoMzUuMjhxLTEuMzItNy4zMi02LjA2LTExQTE3LjQ1LDE3LjQ1LDAsMCwwLDMyMCw2Mi44OGExOC4yMywxOC4yMywwLDAsMC0xMiw0QTE3Ljg2LDE3Ljg2LDAsMCwwLDMwMS44NSw3Ny42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0zNjguNDUsODguMmExNi40OCwxNi40OCwwLDAsMCw2LjYsMTAuNSwyMS4yMiwyMS4yMiwwLDAsMCwxMi42LDMuNjZxMTAuMzIsMCwxNS40OC03LjQ0bDEwLjY4LDYuMjRxLTguODgsMTMuMDgtMjYuMjgsMTMuMDgtMTQuNjQsMC0yMy42NC04Ljk0dC05LTIyLjYycTAtMTMuNDQsOC44OC0yMi41dDIyLjgtOS4wNnExMy4yLDAsMjEuNjYsOS4yNGEzMiwzMiwwLDAsMSw4LjQ2LDIyLjQ0LDQwLjA5LDQwLjA5LDAsMCwxLS40OCw1LjRabS0uMTItMTAuNTZoMzUuMjhxLTEuMzItNy4zMi02LjA2LTExYTE3LjQ1LDE3LjQ1LDAsMCwwLTExLjEtMy43MiwxOC4yMywxOC4yMywwLDAsMC0xMiw0QTE3Ljg2LDE3Ljg2LDAsMCwwLDM2OC4zMyw3Ny42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik00MzcuNTgsMTAwLjQ0aDI5LjE2djEyLjI0SDQxOS45M1YxMDRMNDQ4LDY0LjkySDQyMS4xM1Y1Mi42OGg0NC4zOXY4LjYzWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTUxNi4yOSw1Mi42OGgxMy44bC0yMyw2MS45MnEtOC42NCwyMy4yOC0yOS4yOCwyMi4wOFYxMjQuNTZxNi4xMi4zNiw5Ljg0LTIuNTh0Ni4xMi05LjE4bC42LTEuMkw0NjguODksNTIuNjhoMTQuMTZsMTcuODksNDMuNTVaIi8+PC9nPjwvZz48L3N2Zz4="
height="40"
class="d-inline-block align-top my-2"
/>
<h5 class="text-subtitle1 q-my-none">
Deezy.io: Do onchain to offchain and vice-versa swaps
</h5>
<p>
Link :
<a class="text-light-blue" target="_blank" href="https://deezy.io/">
https://deezy.io/
</a>
</p>
<p>
<a class="text-light-blue" target="_blank" href="https://docs.deezy.io/"
>API DOCS</a
>
</p>
<p>
<small
>Created by,
<a
class="text-light-blue"
target="_blank"
href="https://twitter.com/Uthpala_419"
>Uthpala</a
></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item
group="swap-ln-to-btc"
dense
expand-separator
label="Swap (LIGHTNING TO BTC)"
:content-inset-level="0.5"
>
<q-expansion-item group="ln-to-btc" dense expand-separator label="GET Info">
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Get the current info about the swap service for converting LN btc to
on-chain BTC.
</h5>
<code class="text-light-blue">
<span class="text-white">GET (mainnet)</span>
https://api.deezy.io/v1/swap/info
</code>
<br />
<code class="text-light-blue">
<span class="text-white">GET (testnet)</span>
https://api-testnet.deezy.io/v1/swap/info
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"liquidity_fee_ppm": 2000,
"on_chain_bytes_estimate": 300,
"max_swap_amount_sats": 100000000,
"min_swap_amount_sats": 100000,
"available": true
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="ln-to-btc"
dense
expand-separator
label="POST New (LN to BTC) Swap"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Initiate a new swap to send lightning btc in exchange for on-chain
btc
</h5>
<code class="text-light-blue">
<span class="text-white">POST (mainnet)</span>
https://api.deezy.io/v1/swap
</code>
<br />
<code class="text-light-blue">
<span class="text-white">POST (testnet)</span>
https://api-testnet.deezy.io/v1/swap
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Payload</h6>
<pre>
{
"amount_sats": 500000,
"on_chain_address": "tb1qrcdhlm0m...",
"on_chain_sats_per_vbyte": 2
}
</pre>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"bolt11_invoice": "lntb603u1p3vmxj7p...",
"fee_sats": 600
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="ln-to-btc"
dense
expand-separator
label="GET Lookup (LN to BTC) Swap"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Lookup the on-chain transaction information for an existing swap
</h5>
<code class="text-light-blue">
<span class="text-white">GET (mainnet)</span>
https://api.deezy.io/v1/swap/lookup
</code>
<br />
<code class="text-light-blue">
<span class="text-white">GET (testnet)</span>
https://api-testnet.deezy.io/v1/swap/lookup
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Query Parameter</h6>
<pre>
"bolt11_invoice": "lntb603u1p3vmxj7pp54...",
</pre>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"on_chain_txid": "string",
"tx_hex": "string"
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
<q-expansion-item
group="swap-btc-to-ln"
dense
expand-separator
label="Swap (BTC TO LIGHTNING)"
:content-inset-level="0.5"
>
<q-expansion-item
group="btc-to-ln"
dense
expand-separator
label="POST New On-Chain Deposit Address"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Generate an on-chain deposit address for your lnurl or lightning
address.
</h5>
<code class="text-light-blue">
<span class="text-white">POST (mainnet)</span>
https://api.deezy.io/v1/source
</code>
<br />
<code class="text-light-blue">
<span class="text-white">POST (testnet)</span>
https://api-testnet.deezy.io/v1/source
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Payload</h6>
<pre>
{
"lnurl_or_lnaddress": "LNURL1DP68GURN8GHJ...",
"secret_access_key": "b3c6056d2845867fa7..",
"webhook_url": "https://your.website.com/dee.."
}
</pre>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"address": "bc1qkceyc5...",
"secret_access_key": "b3c6056d28458...",
"commitment": "for any satoshis sent to bc1..",
"signature": "d69j6aj1ssz5egmsr..",
"webhook_url": "https://your.website.com/deez.."
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="btc-to-ln"
dense
expand-separator
label="GET Lookup (BTC to LN) Swaps"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Lookup (BTC to LN) swaps
</h5>
<code class="text-light-blue">
<span class="text-white">GET (mainnet)</span>
https://api.deezy.io/v1/source/lookup
</code>
<br />
<code class="text-light-blue">
<span class="text-white">GET (testnet)</span>
https://api-testnet.deezy.io/v1/source/lookup
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"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
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
</q-expansion-item>

View file

@ -0,0 +1,588 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md">Deezy</h5>
<p class="text-subtitle2 q-mt-none q-mb-md">
An access token is required to use the swap service. Email
support@deezy.io or contact @dannydeezy on telegram to get one.
</p>
<div>
<div class="flex justify-between items-center">
<span>Deezy token </span>
<q-btn
type="button"
@click="showDeezyTokenForm = !showDeezyTokenForm"
>Add or Update token</q-btn
>
</div>
<p v-if="storedDeezyToken" v-text="storedDeezyToken"></p>
</div>
<q-form
v-if="showDeezyTokenForm"
@submit="storeDeezyToken"
class="q-gutter-md q-mt-lg"
>
<q-input
filled
dense
emit-value
:placeholder="storedDeezyToken"
v-model.trim="deezyToken"
label="Deezy Token"
type="text"
></q-input>
<q-btn color="grey" type="submit" label="Store Deezy Token"></q-btn>
</q-form>
<q-separator class="q-my-lg"></q-separator>
<q-card>
<q-card-section>
<q-btn
label="SWAP (LIGHTNING -> BTC)"
unelevated
color="primary"
@click="showLnToBtcForm"
:disabled="!storedDeezyToken"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send lightning btc and receive on-chain btc
</q-tooltip>
</q-btn>
<q-btn
label="SWAP (BTC -> LIGHTNING)"
unelevated
color="primary"
@click="swapBtcToLn.show = true; swapLnToBtc.show = false;"
:disabled="!storedDeezyToken"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send on-chain btc and receive via lightning
</q-tooltip>
</q-btn>
</q-card-section>
</q-card>
<div
v-show="swapLnToBtc.show"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<h6 class="q-mt-none">LIGHTNING BTC -> BTC</h6>
<q-form @submit="sendLnToBtc" class="q-gutter-md">
<q-input
filled
dense
emit-value
v-model.trim="swapLnToBtc.data.amount"
label="Amount (sats)"
type="number"
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="swapLnToBtc.data.on_chain_address"
type="string"
label="Onchain address to receive funds"
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="swapLnToBtc.data.on_chain_sats_per_vbyte"
label="On chain fee rate (sats/vbyte)"
min="1"
type="number"
:hint="swapLnToBtc.suggested_fees && `Economy Fee - ${swapLnToBtc.suggested_fees?.economyFee} | Half an hour fee - ${swapLnToBtc.suggested_fees?.halfHourFee} | Fastest fee - ${swapLnToBtc.suggested_fees?.fastestFee}`"
>
</q-input>
<q-btn
unelevated
color="primary"
type="submit"
label="Create Swap"
></q-btn>
<q-btn flat color="grey" class="q-ml-auto" @click="resetSwapLnToBtc"
>Cancel</q-btn
>
</q-form>
<q-dialog v-model="swapLnToBtc.showInvoice" persistent>
<q-card flat bordered class="my-card">
<q-card-section>
<div class="flex justify-between">
<div class="text-h6">Pay invoice to complete swap</div>
<q-btn flat v-close-popup>
<q-icon name="close" />
</q-btn>
</div>
</q-card-section>
<q-card-section class="q-pt-none">
<qrcode
:value="swapLnToBtc.response"
:options="{width: 360}"
class="rounded-borders"
></qrcode>
</q-card-section>
<q-card-section>
<q-btn
outline
@click="copyLnInvoice"
label="Copy"
color="primary"
></q-btn>
<q-input
v-model="swapLnToBtc.response"
type="textarea"
readonly
@click="$event.target.select()"
/>
</q-card-section>
</q-card>
</q-dialog>
</div>
<div
v-show="swapBtcToLn.show"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<h6 class="q-mt-none">BTC -> LIGHTNING BTC</h6>
<q-form @submit="sendBtcToLn" class="q-gutter-md">
<q-input
filled
dense
emit-value
v-model.trim="swapBtcToLn.data.lnurl_or_lnaddress"
label="Lnurl or Lightning Address"
type="string"
></q-input>
<q-btn
unelevated
color="primary"
type="submit"
label="Generate Onchain Address"
></q-btn>
<q-btn flat color="grey" class="q-ml-auto" @click="resetSwapBtcToLn"
>Cancel</q-btn
>
</q-form>
<q-dialog v-model="swapBtcToLn.showDetails" persistent>
<q-card flat bordered class="my-card">
<q-card-section>
<div class="flex justify-between">
<div class="text-h6">Onchain Address</div>
<q-btn flat v-close-popup>
<q-icon name="close" />
</q-btn>
</div>
</q-card-section>
<q-card-section>
<q-input
v-model="swapBtcToLn.response.address"
type="textarea"
readonly
@click="$event.target.select()"
/>
</q-card-section>
<q-card-section>
<q-btn
outline
@click="copyBtcToLnBtcAddress"
label="Copy Address"
color="primary"
></q-btn>
</q-card-section>
<q-card-section>
<q-input
v-model="swapBtcToLn.response.commitment"
type="textarea"
readonly
@click="$event.target.select()"
/>
</q-card-section>
</q-card>
</q-dialog>
</div>
</q-card-section>
</q-card>
{% raw %}
<q-dialog v-model="swapLnToBtc.invoicePaid">
<q-card class="bg-teal text-white" style="width: 400px">
<q-card-section>
<div class="text-h6">Success Bitcoin is on its way</div>
</q-card-section>
<q-card-section class="q-pt-none">
Onchain tx id {{ swapLnToBtc.onchainTxId }}
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn flat label="OK" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
{% endraw %}
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Boltz extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "deezy/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<div class="q-pa-md full-width">
<q-table
title="Swaps Lightning -> BTC"
:data="rowsLnToBtc"
:columns="columnsLnToBtc"
row-key="name"
/>
</div>
<div class="q-pa-md full-width">
<q-table
title="Swaps BTC -> Lightning"
:data="rowsBtcToLn"
:columns="columnsBtcToLn"
row-key="name"
/>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
columnsLnToBtc: [
{
name: 'amount_sats',
label: 'Amount Sats',
align: 'left',
field: 'amount_sats',
sortable: true
},
{
name: 'on_chain_address',
align: 'left',
label: 'On chain address',
field: 'on_chain_address'
},
{
name: 'on_chain_sats_per_vbyte',
align: 'left',
label: 'Onchin sats per vbyte',
field: 'on_chain_sats_per_vbyte',
sortable: true
},
{
name: 'fee_sats',
label: 'Fee sats',
align: 'left',
field: 'fee_sats'
},
{name: 'txid', label: 'Tx Id', align: 'left', field: 'txid'},
{name: 'tx_hex', label: 'Tx Hex', align: 'left', field: 'tx_hex'},
{
name: 'created_at',
label: 'Created at',
align: 'left',
field: 'created_at',
sortable: true,
sort: true
}
],
rowsLnToBtc: [],
columnsBtcToLn: [
{
name: 'ln_address',
align: 'left',
label: 'Ln Address or Invoice',
field: 'ln_address'
},
{
name: 'on_chain_address',
align: 'left',
label: 'Onchain Address',
field: 'on_chain_address'
},
{
name: 'secret_access_key',
align: 'left',
label: 'Secret Access Key',
field: 'secret_access_key'
},
{
name: 'commitment',
align: 'left',
label: 'Commitment',
field: 'commitment'
},
{
name: 'signature',
align: 'left',
label: 'Signature',
field: 'signature'
},
{
name: 'created_at',
label: 'Created at',
field: 'created_at',
align: 'left',
sortable: true,
sort: true
}
],
rowsBtcToLn: [],
showDeezyTokenForm: false,
storedDeezyToken: null,
deezyToken: null,
lightning_btc: '',
tools: [],
swapLnToBtc: {
show: false,
showInvoice: false,
data: {
on_chain_sats_per_vbyte: 1
},
suggested_fees: null,
response: null,
invoicePaid: false,
onchainTxId: null
},
swapBtcToLn: {
show: false,
showDetails: false,
data: {},
response: {}
}
}
},
created: async function () {
this.getToken()
this.getLnToBtc()
this.getBtcToLn()
},
methods: {
updateLnToBtc(payload) {
var self = this
return axios
.post('/deezy/api/v1/update-ln-to-btc', {
...payload
})
.then(function (response) {
console.log('btc to ln is update', response)
})
.catch(function (error) {
console.log(error)
})
},
getToken() {
var self = this
axios({
method: 'GET',
url: '/deezy/api/v1/token'
}).then(function (response) {
self.storedDeezyToken = response.data.deezy_token
if (!self.storeDeezyToken) {
showDeezyTokenForm = true
}
})
},
getLnToBtc() {
var self = this
axios.get('/deezy/api/v1/ln-to-btc').then(function (response) {
if (response.data.length) {
self.rowsLnToBtc = response.data
}
})
},
getBtcToLn() {
var self = this
axios.get('/deezy/api/v1/btc-to-ln').then(function (response) {
if (response.data.length) {
self.rowsBtcToLn = response.data
}
})
},
showLnToBtcForm() {
if (!this.swapLnToBtc.show) {
this.getSuggestedOnChainFees()
}
this.swapLnToBtc.show = true
this.swapBtcToLn.show = false
},
getSuggestedOnChainFees() {
axios
.get('https://mempool.space/api/v1/fees/recommended')
.then(result => {
this.swapLnToBtc.suggested_fees = result.data
})
},
checkIfInvoiceIsPaid() {
if (this.swapLnToBtc.response && !this.swapLnToBtc.invoicePaid) {
var self = this
let interval = setInterval(() => {
axios
.get(
`https://api.deezy.io/v1/swap/lookup?bolt11_invoice=${self.swapLnToBtc.response}`
)
.then(async function (response) {
if (response.data.on_chain_txid) {
self.swapLnToBtc = {
...self.swapLnToBtc,
invoicePaid: true,
onchainTxId: response.data.on_chain_txid
}
self
.updateLnToBtc({
txid: response.data.on_chain_txid,
tx_hex: response.data.tx_hex,
bolt11_invoice: self.swapLnToBtc.response
})
.then(() => {
self.getLnToBtc()
})
clearInterval(interval)
}
})
}, 4000)
}
},
copyLnInvoice() {
Quasar.utils.copyToClipboard(this.swapLnToBtc.response)
},
copyBtcToLnBtcAddress() {
Quasar.utils.copyToClipboard(this.swapBtcToLn.response.address)
},
sendLnToBtc() {
var self = this
axios
.post(
'https://api.deezy.io/v1/swap',
{
amount_sats: parseInt(self.swapLnToBtc.data.amount),
on_chain_address: self.swapLnToBtc.data.on_chain_address,
on_chain_sats_per_vbyte: parseInt(
self.swapLnToBtc.data.on_chain_sats_per_vbyte
)
},
{
headers: {
'x-api-token': self.storedDeezyToken
}
}
)
.then(function (response) {
self.swapLnToBtc = {
...self.swapLnToBtc,
showInvoice: true,
response: response.data.bolt11_invoice
}
const payload = {
amount_sats: parseInt(self.swapLnToBtc.data.amount),
on_chain_address: self.swapLnToBtc.data.on_chain_address,
on_chain_sats_per_vbyte:
self.swapLnToBtc.data.on_chain_sats_per_vbyte,
bolt11_invoice: response.data.bolt11_invoice,
fee_sats: response.data.fee_sats
}
self.storeLnToBtc(payload)
self.checkIfInvoiceIsPaid()
})
.catch(function (error) {
console.log(error)
})
},
sendBtcToLn() {
var self = this
axios
.post(
'https://api.deezy.io/v1/source',
{
lnurl_or_lnaddress: self.swapBtcToLn.data.lnurl_or_lnaddress
},
{
headers: {
'x-api-token': self.storedDeezyToken
}
}
)
.then(function (response) {
self.swapBtcToLn = {
...self.swapBtcToLn,
response: response.data,
showDetails: true
}
const payload = {
ln_address: self.swapBtcToLn.data.lnurl_or_lnaddress,
on_chain_address: response.data.address,
secret_access_key: response.data.secret_access_key,
commitment: response.data.commitment,
signature: response.data.signature
}
self.storeBtcToLn(payload)
})
.catch(function (error) {
console.log(error)
})
},
storeBtcToLn(payload) {
var self = this
axios
.post('/deezy/api/v1/store-btc-to-ln', {
...payload
})
.then(function (response) {
console.log('btc to ln is stored', response)
})
.catch(function (error) {
console.log(error)
})
},
storeLnToBtc(payload) {
var self = this
axios
.post('/deezy/api/v1/store-ln-to-btc', {
...payload
})
.then(function (response) {
console.log('ln to btc is stored', response)
})
.catch(function (error) {
console.log(error)
})
},
storeDeezyToken() {
var self = this
axios
.post('/deezy/api/v1/store-token', {
deezy_token: self.deezyToken
})
.then(function (response) {
self.storedDeezyToken = response.data
self.showDeezyTokenForm = false
})
.catch(function (error) {
console.log(error)
})
},
resetSwapBtcToLn() {
this.swapBtcToLn = {
...this.swapBtcToLn,
data: {}
}
},
resetSwapLnToBtc() {
this.swapLnToBtc = {
...this.swapLnToBtc,
data: {}
}
}
}
})
</script>
{% endblock %}

View file

@ -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()}
)

View file

@ -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

View file

@ -1,10 +1,10 @@
import asyncio
from lnbits.core.models import Payment
from lnbits.extensions.events.models import CreateTicket
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .models import CreateTicket
from .views_api import api_ticket_send_ticket

View file

@ -7,7 +7,6 @@ from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.events.models import CreateEvent, CreateTicket
from . import events_ext
from .crud import (
@ -24,6 +23,7 @@ from .crud import (
reg_ticket,
update_event,
)
from .models import CreateEvent, CreateTicket
# Events

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -51,8 +51,15 @@
<q-card flat>
<q-card-section>
<div class="text-h5 q-mb-md">
{{SITE_TITLE}} Extension Development Guide
<small>(Collection of resources for extension developers)</small>
Extension Development Guide
<small
>(also check the
<a
class="text-primary"
href="http://docs.lnbits.org/devs/development.html"
>docs</a
>)</small
>
</div>
<q-card unelevated flat>
@ -188,8 +195,8 @@
<p>
LNbits uses
<a href="https://vuejs.org/" class="text-primary">Vue</a>
components for best-in-class high-performance and responsive
performance.
for best-in-class, responsive and high-performance
components.
</p>
<p>Typical example of Vue components in a frontend script:</p>
@ -199,8 +206,7 @@
/><br /><br />
<p>
In a page body, models can be called. <br />Content can be
conditionally rendered using Vue's
Content can be conditionally rendered using Vue's
<code class="bg-grey-3 text-black">v-if</code>:
</p>
<img
@ -220,6 +226,8 @@
<q-tabs v-model="usefultab" align="left">
<q-tab name="magicalg">MAGICAL G</q-tab>
<q-tab name="exchange">EXCHANGE RATES</q-tab>
<q-tab name="qrcodes">QR CODES</q-tab>
<q-tab name="websockets">WEBSOCKETS</q-tab>
</q-tabs>
</template>
@ -255,6 +263,85 @@
>:<br />
<img src="./static/conversion-example2.png" />
</q-tab-panel>
<q-tab-panel name="qrcodes" class="text-body1">
<div class="text-h5 q-mb-md">QR Codes</div>
<p>
For most purposes use Quasar's inbuilt VueQrcode library:
</p>
<img src="./static/qrcode-example1.png" />
<p>
LNbits does also include a handy
<a
href="../docs#/default/img_api_v1_qrcode__data__get"
class="text-primary"
>
QR code enpoint</a
>
</p>
{% raw %} You can use via
<a
href="/api/v1/qrcode/some-data-you-want-in-a-qrcode"
class="text-primary"
>{{protocol + location}}{% endraw
%}/api/v1/qrcode/some-data-you-want-in-a-qrcode:</a
><br />
<br />
<img src="./static/qrcode-example.png" />
<br />
<img
class="bg-white"
width="300px"
src="/api/v1/qrcode/some-data-you-want-in-a-qrcode"
/>
<br />
</q-tab-panel>
<q-tab-panel name="websockets" class="text-body1">
<div class="text-h5 q-mb-md">Websockets</div>
<p>
Fastapi includes a great
<a
class="text-primary"
href="https://fastapi.tiangolo.com/advanced/websockets/#websockets-client"
>websocket tool</a
>
</p>
{% raw %}
<p>
A few LNbits extensions also make use of a weird and useful
websocket/GET tool built into LNbits, such as extensions
Copilot and LNURLDevices<br />
You can subscribe to websocket with
<code class="bg-grey-3 text-black"
>wss:{{location}}/api/v1/ws/{SOME-ID}</code
><br />
You can post to any clients subscribed to the endpoint with
<code class="bg-grey-3 text-black"
>{{protocol +
location}}/api/v1/ws/{SOME-ID}/{THE-DATA-YOU-WANT-TO-POST}</code
><br />
<br />
<strong
><div id="text-to-change">
DEMO: Hit
<a
target="_blank"
href="/api/v1/ws/32872r23g29/blah%20blah%20blah"
class="text-primary"
>{{protocol +
location}}/api/v1/ws/32872r23g29/blah%20blah%20blah</a
>
in a different browser window to change this text to
`blah blah blah`.
</div></strong
>
<br />
Function used in this demo:<br />
<img src="./static/websocket-example.png" /></p
></q-tab-panel>
{% endraw %}
</q-tab-panels>
</template>
</div>
@ -296,6 +383,8 @@
data: function () {
return {
///// Declare models/variables /////
protocol: window.location.protocol,
location: '//' + window.location.hostname,
thingDialog: {
show: false,
data: {}
@ -310,7 +399,7 @@
},
///// Where functions live /////
methods: {
exampleFunction(data) {
exampleFunction: function (data) {
var theData = data
LNbits.api
.request(
@ -325,6 +414,28 @@
LNbits.utils.notifyApiError(error) // Error will be passed to the frontend
})
},
initWs: async function () {
if (location.protocol !== 'http:') {
localUrl =
'wss://' +
document.domain +
':' +
location.port +
'/api/v1/ws/32872r23g29'
} else {
localUrl =
'ws://' +
document.domain +
':' +
location.port +
'/api/v1/ws/32872r23g29'
}
this.ws = new WebSocket(localUrl)
this.ws.addEventListener('message', async ({data}) => {
const res = data.toString()
document.getElementById('text-to-change').innerHTML = res
})
},
sendThingDialog() {
console.log(this.thingDialog)
}
@ -333,6 +444,7 @@
created: function () {
self = this // Often used to run a real object, rather than the event (all a bit confusing really)
self.exampleFunction('lorum')
self.initWs()
}
})
</script>

View file

@ -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()

View file

@ -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}!"),
)

View file

@ -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))

View file

@ -1,12 +1,9 @@
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
from . import livestream_ext
from .crud import (
@ -20,6 +17,7 @@ from .crud import (
update_livestream_fee,
update_track,
)
from .models import CreateTrack
@livestream_ext.get("/api/v1/livestream")
@ -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

View file

@ -2,7 +2,7 @@ import json
import httpx
from lnbits.extensions.lnaddress.models import Domains
from .models import Domains
async def cloudflare_create_record(domain: Domains, ip: str):

View file

@ -6,7 +6,6 @@ from fastapi import Depends, HTTPException, Query, Request
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.lnaddress.models import CreateAddress, CreateDomain
from . import lnaddress_ext
from .cloudflare import cloudflare_create_record
@ -23,6 +22,7 @@ from .crud import (
get_domains,
update_domain,
)
from .models import CreateAddress, CreateDomain
# DOMAINS

View file

@ -5,7 +5,7 @@ from fastapi.param_functions import Security
from fastapi.security.api_key import APIKeyHeader
from starlette.exceptions import HTTPException
from lnbits.decorators import WalletTypeInfo, get_key_type # type: ignore
from lnbits.decorators import WalletTypeInfo, get_key_type
api_key_header_auth = APIKeyHeader(
name="AUTHORIZATION",

View file

@ -8,7 +8,6 @@ from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.lnticket.models import CreateFormData, CreateTicketData
from . import lnticket_ext
from .crud import (
@ -23,6 +22,7 @@ from .crud import (
set_ticket_paid,
update_form,
)
from .models import CreateFormData, CreateTicketData
# FORMS

View file

@ -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,),

View file

@ -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"}

View file

@ -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))

View file

@ -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),

View file

@ -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

View file

@ -1,13 +1,9 @@
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
from lnbits.extensions.lnurldevice import lnurldevice_ext
from lnbits.utils.exchange_rates import currencies
from . import lnurldevice_ext
@ -26,9 +22,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 +34,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 +42,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 +59,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 +71,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

View file

@ -3,11 +3,7 @@ import math
from http import HTTPStatus
from fastapi import Request
from lnurl import ( # type: ignore
LnurlErrorResponse,
LnurlPayActionResponse,
LnurlPayResponse,
)
from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice

View file

@ -4,11 +4,11 @@ from typing import Dict, Optional
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata # type: ignore
from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
from starlette.requests import Request
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnbits.lnurl import encode as lnurl_encode
class CreatePayLinkData(BaseModel):

View file

@ -2,7 +2,7 @@ import json
from http import HTTPStatus
from fastapi import Depends, Query, Request
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user

View file

@ -10,8 +10,8 @@ from collections import defaultdict
from fastapi import WebSocket
from loguru import logger
from lnbits.extensions.market.crud import create_chat_message
from lnbits.extensions.market.models import CreateChatMessage
from .crud import create_chat_message
from .models import CreateChatMessage
class Notifier:

View file

@ -55,8 +55,16 @@
></q-select>
<!-- </div> -->
<!-- </div> -->
<q-input
v-if="productDialog.url"
filled
dense
v-model.trim="productDialog.data.image"
type="url"
label="Image URL"
></q-input>
<q-file
v-else
class="q-pr-md"
filled
dense
@ -79,6 +87,10 @@
/>
</template>
</q-file>
<q-toggle
:label="`${productDialog.url ? 'Insert image URL' : 'Upload image file'}`"
v-model="productDialog.url"
></q-toggle>
<q-input
filled
dense

View file

@ -200,7 +200,10 @@
:href="props.row.wallet"
target="_blank"
></q-btn>
<q-tooltip> Link to pass to stall relay </q-tooltip>
<q-tooltip
>Disabled: link to pass to stall relays when using
nostr</q-tooltip
>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}

View file

@ -498,6 +498,7 @@
},
productDialog: {
show: false,
url: true,
data: {}
},
stallDialog: {
@ -536,6 +537,9 @@
methods: {
resetDialog(dialog) {
this[dialog].show = false
if (dialog == 'productDialog') {
this[dialog].url = true
}
this[dialog].data = {}
},
toggleDA(value, evt) {
@ -798,11 +802,17 @@
var link = _.findWhere(self.products, {id: linkId})
self.productDialog.data = _.clone(link._data)
self.productDialog.data.categories = self.productDialog.data.categories.split(
','
)
if (self.productDialog.data.categories) {
self.productDialog.data.categories = self.productDialog.data.categories.split(
','
)
}
if (self.productDialog.data.image.startsWith('data:')) {
self.productDialog.url = false
}
self.productDialog.show = true
console.log(self.productDialog)
},
sendProductFormData: function () {
let _data = {...this.productDialog.data}
@ -831,14 +841,8 @@
let canvas = document.createElement('canvas')
canvas.setAttribute('width', fit.width)
canvas.setAttribute('height', fit.height)
await pica.resize(image, canvas, {
quality: 0,
alpha: true,
unsharpAmount: 95,
unsharpRadius: 0.9,
unsharpThreshold: 70
})
this.productDialog.data.image = canvas.toDataURL()
output = await pica.resize(image, canvas)
this.productDialog.data.image = output.toDataURL('image/jpeg', 0.4)
this.productDialog = {...this.productDialog}
}
},

View file

@ -16,11 +16,9 @@ from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists # type: ignore
from lnbits.extensions.market import market_ext, market_renderer
from lnbits.extensions.market.models import CreateChatMessage, SetSettings
from lnbits.extensions.market.notifier import Notifier
from lnbits.decorators import check_user_exists
from . import market_ext, market_renderer
from .crud import (
create_chat_message,
create_market_settings,
@ -35,6 +33,8 @@ from .crud import (
get_market_zones,
update_market_product_stock,
)
from .models import CreateChatMessage, SetSettings
from .notifier import Notifier
templates = Jinja2Templates(directory="templates")

View file

@ -113,6 +113,23 @@ async def api_market_product_create(
if stall.currency != "sat":
data.price *= settings.fiat_base_multiplier
if data.image:
image_is_url = data.image.startswith("https://") or data.image.startswith(
"http://"
)
if not image_is_url:
def size(b64string):
return int((len(b64string) * 3) / 4 - b64string.count("=", -2))
image_size = size(data.image) / 1024
if image_size > 100:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Image size is too big, {int(image_size)}Kb. Max: 100kb, Compress the image at https://tinypng.com, or use an URL.",
)
if product_id:
product = await get_market_product(product_id)
if not product:

View file

@ -1,4 +1,3 @@
# type: ignore
from os import getenv
from fastapi import Depends, Request
@ -36,5 +35,5 @@ ngrok_tunnel = ngrok.connect(port)
@ngrok_ext.get("/")
async def index(request: Request, user: User = Depends(check_user_exists)):
return ngrok_renderer().TemplateResponse(
"ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()}
"ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()} # type: ignore
)

View file

@ -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}
}
}
```

View file

@ -173,12 +173,17 @@ async def create_address_internal(domain_id: str, data: CreateAddressData) -> Ad
async def create_domain_internal(wallet_id: str, data: CreateDomainData) -> Domain:
domain_id = urlsafe_short_hash()
if data.currency != "Satoshis":
amount = data.amount * 100
else:
amount = data.amount
await db.execute(
"""
INSERT INTO nostrnip5.domains (id, wallet, currency, amount, domain)
VALUES (?, ?, ?, ?, ?)
""",
(domain_id, wallet_id, data.currency, int(data.amount * 100), data.domain),
(domain_id, wallet_id, data.currency, int(amount), data.domain),
)
domain = await get_domain(domain_id)

View file

@ -201,7 +201,7 @@
dense
v-model.trim="formDialog.data.amount"
label="Amount"
placeholder="10.00"
placeholder="How much do you want to charge?"
></q-input>
<q-input
filled
@ -280,7 +280,9 @@
'YYYY-MM-DD HH:mm'
)
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
if (obj.currency != 'Satoshis') {
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
}
return obj
}
@ -293,6 +295,7 @@
domains: [],
addresses: [],
currencyOptions: [
'Satoshis',
'USD',
'EUR',
'GBP',

View file

@ -36,12 +36,14 @@ context %} {% block page %}
the {{ domain.domain }} domain.
</p>
<p>
The current price is
The current price is {% if domain.currency != "Satoshis" %}
<b
>{{ "{:0,.2f}".format(domain.amount / 100) }} {{ domain.currency }}</b
>
for an account (if you do not own the domain, the service provider can
disable at any time).
{% else %}
<b>{{ "{}".format(domain.amount) }} {{ domain.currency }}</b>
{% endif %} for an account (if you do not own the domain, the service
provider can disable at any time).
</p>
<p>After submitting payment, your address will be</p>

View file

@ -196,7 +196,12 @@ async def api_address_create(
)
address = await create_address_internal(domain_id=domain_id, data=post_data)
price_in_sats = await fiat_amount_as_satoshis(domain.amount / 100, domain.currency)
if domain.currency == "Satoshis":
price_in_sats = domain.amount
else:
price_in_sats = await fiat_amount_as_satoshis(
domain.amount / 100, domain.currency
)
try:
payment_hash, payment_request = await create_invoice(

View file

@ -5,7 +5,7 @@ from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse # type: ignore
from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice, pay_invoice

View file

@ -5,8 +5,8 @@ from typing import Dict, Optional
from fastapi import Request
from fastapi.param_functions import Query
from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from lnurl import encode as lnurl_encode
from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
from pydantic.main import BaseModel

View file

@ -1,7 +1,7 @@
from http import HTTPStatus
from fastapi import Depends, Query, Request
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user

View file

@ -7,7 +7,7 @@ from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.helpers import urlsafe_short_hash
from ..watchonly.crud import get_config, get_fresh_address
from ..watchonly.crud import get_config, get_fresh_address # type: ignore
from . import db
from .helpers import fetch_onchain_balance
from .models import Charges, CreateCharge, SatsPayThemes

View file

@ -4,11 +4,10 @@ import json
from loguru import logger
from lnbits.core.models import Payment
from lnbits.extensions.satspay.crud import check_address_balance, get_charge
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import update_charge
from .crud import check_address_balance, get_charge, update_charge
from .helpers import call_webhook

View file

@ -6,10 +6,10 @@ from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.extensions.satspay.helpers import public_charge
from . import satspay_ext, satspay_renderer
from .crud import get_charge, get_theme
from .helpers import public_charge
templates = Jinja2Templates(directory="templates")

View file

@ -11,8 +11,8 @@ from lnbits.decorators import (
require_admin_key,
require_invoice_key,
)
from lnbits.extensions.satspay import satspay_ext
from . import satspay_ext
from .crud import (
check_address_balance,
create_charge,

View file

@ -3,7 +3,7 @@ from sqlite3 import Row
from pydantic import BaseModel
from starlette.requests import Request
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnbits.lnurl import encode as lnurl_encode
class CreateScrubLink(BaseModel):

View file

@ -0,0 +1,14 @@
<h1>SMTP Extension</h1>
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.

View file

@ -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))

View file

@ -0,0 +1,6 @@
{
"name": "SMTP",
"short_description": "Charge sats for sending emails",
"tile": "/smtp/static/smtp-bitcoin-email.png",
"contributors": ["dni"]
}

View file

@ -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,))

View file

@ -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}
);
"""
)

View file

@ -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

View file

@ -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"""
<html>
<head></head>
<body>
<p>{email.message}<p>
<br>
<p>{signature}</p>
</body>
</html>
"""
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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -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)

View file

@ -0,0 +1,23 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="About LNBits SMTP"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
LNBits SMTP: Get paid sats to send emails
</h5>
<p>
Charge people for using sending an email via your smtp server<br />
<a
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/smtp"
>More details</a
>
<br />
<small>Created by, <a href="https://github.com/dni">dni</a></small>
</p>
</q-card-section>
</q-card>
</q-expansion-item>

View file

@ -0,0 +1,175 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ email }}</h3>
<br />
<h5 class="q-my-none">{{ desc }}</h5>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.receiver"
type="text"
label="Receiver"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.subject"
type="text"
label="Subject"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.message"
type="textarea"
label="Message "
></q-input>
<p>Total cost: {{ cost }} sats</p>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.receiver == '' || formDialog.data.subject == '' || formDialog.data.message == ''"
type="submit"
>Submit</q-btn
>
</div>
</q-form>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %}
<script>
console.log('{{ cost }}')
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {
subject: '',
receiver: '',
message: ''
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null
}
}
},
methods: {
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
dismissMsg()
clearInterval(paymentChecker)
setTimeout(function () {}, 10000)
},
Invoice: function () {
var self = this
axios
.post('/smtp/api/v1/email/{{ emailaddress_id }}', {
emailaddress_id: '{{ emailaddress_id }}',
subject: self.formDialog.data.subject,
receiver: self.formDialog.data.receiver,
message: self.formDialog.data.message
})
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.receive = {
show: true,
status: 'pending',
paymentReq: self.paymentReq
}
paymentChecker = setInterval(function () {
axios
.get('/smtp/api/v1/email/' + self.paymentCheck)
.then(function (res) {
console.log(res.data)
if (res.data.paid) {
clearInterval(paymentChecker)
self.receive = {
show: false,
status: 'complete',
paymentReq: null
}
dismissMsg()
console.log(self.formDialog)
self.formDialog.data.subject = ''
self.formDialog.data.receiver = ''
self.formDialog.data.message = ''
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: 'thumb_up'
})
console.log('END')
}
})
.catch(function (error) {
console.log(error)
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
console.log(error)
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,528 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
unelevated
color="primary"
@click="emailaddressDialog.show = true"
>New Emailaddress</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Emailaddresses</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportEmailaddressesCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="emailaddresses"
row-key="id"
:columns="emailaddressTable.columns"
:pagination.sync="emailaddressTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateEmailaddressDialog(props.row.id)"
icon="edit"
color="light-blue"
>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteEmailaddress(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Emails</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportEmailsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="emails"
row-key="id"
:columns="emailsTable.columns"
:pagination.sync="emailsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteEmail(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Sendmail extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "smtp/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="emailaddressDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="emailaddressDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
emit-value
v-model.trim="emailaddressDialog.data.email"
type="text"
label="Emailaddress "
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="emailaddressDialog.data.testemail"
type="text"
label="Emailaddress to test the server"
></q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_server"
type="text"
label="SMTP Host"
>
</q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_user"
type="text"
label="SMTP User"
>
</q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_password"
type="password"
label="SMTP Password"
>
</q-input>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.smtp_port"
type="text"
label="SMTP Port"
>
</q-input>
<div id="lolcheck">
<q-checkbox
name="anonymize"
v-model="emailaddressDialog.data.anonymize"
label="ANONYMIZE, don't save mails, no addresses in tx"
/>
</div>
<q-input
filled
dense
v-model.trim="emailaddressDialog.data.description"
type="textarea"
label="Description "
>
</q-input>
<q-input
filled
dense
v-model.number="emailaddressDialog.data.cost"
type="number"
label="Amount per email in satoshis"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
v-if="emailaddressDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Form</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="enableButton()"
type="submit"
>Create Emailaddress</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var LNSendmail = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.displayUrl = ['/smtp/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
emailaddresses: [],
emails: [],
emailaddressTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{
name: 'anonymize',
align: 'left',
label: 'Anonymize',
field: 'anonymize'
},
{
name: 'email',
align: 'left',
label: 'Emailaddress',
field: 'email'
},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
{
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{
name: 'cost',
align: 'left',
label: 'Cost',
field: 'cost'
}
],
pagination: {
rowsPerPage: 10
}
},
emailsTable: {
columns: [
{
name: 'emailaddress',
align: 'left',
label: 'From',
field: 'emailaddress'
},
{
name: 'receiver',
align: 'left',
label: 'Receiver',
field: 'receiver'
},
{
name: 'subject',
align: 'left',
label: 'Subject',
field: 'subject'
},
{
name: 'message',
align: 'left',
label: 'Message',
field: 'message'
},
{
name: 'paid',
align: 'left',
label: 'Is paid',
field: 'paid'
}
],
pagination: {
rowsPerPage: 10
}
},
emailaddressDialog: {
show: false,
data: {}
}
}
},
methods: {
enableButton: function () {
return (
this.emailaddressDialog.data.cost == null ||
this.emailaddressDialog.data.cost < 0 ||
this.emailaddressDialog.data.testemail == null ||
this.emailaddressDialog.data.smtp_user == null ||
this.emailaddressDialog.data.smtp_password == null ||
this.emailaddressDialog.data.smtp_server == null ||
this.emailaddressDialog.data.smtp_port == null ||
this.emailaddressDialog.data.email == null ||
this.emailaddressDialog.data.description == null
)
},
getEmails: function () {
var self = this
LNbits.api
.request(
'GET',
'/smtp/api/v1/email?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.emails = response.data.map(function (obj) {
return LNSendmail(obj)
})
})
},
deleteEmail: function (emailId) {
var self = this
var email = _.findWhere(this.emails, {id: emailId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this email')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/smtp/api/v1/email/' + emailId,
_.findWhere(self.g.user.wallets, {id: email.wallet}).inkey
)
.then(function (response) {
self.emails = _.reject(self.emails, function (obj) {
return obj.id == emailId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportEmailsCSV: function () {
LNbits.utils.exportCSV(this.emailsTable.columns, this.emails)
},
getEmailAddresses: function () {
var self = this
LNbits.api
.request(
'GET',
'/smtp/api/v1/emailaddress?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.emailaddresses = response.data.map(function (obj) {
return LNSendmail(obj)
})
})
},
sendFormData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.emailaddressDialog.data.wallet
})
var data = this.emailaddressDialog.data
if (data.id) {
this.updateEmailaddress(wallet, data)
} else {
this.createEmailaddress(wallet, data)
}
},
createEmailaddress: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/smtp/api/v1/emailaddress', wallet.inkey, data)
.then(function (response) {
self.emailaddresses.push(LNSendmail(response.data))
self.emailaddressDialog.show = false
self.emailaddressDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateEmailaddressDialog: function (formId) {
var link = _.findWhere(this.emailaddresses, {id: formId})
this.emailaddressDialog.data = _.clone(link)
this.emailaddressDialog.show = true
},
updateEmailaddress: function (wallet, data) {
var self = this
LNbits.api
.request(
'PUT',
'/smtp/api/v1/emailaddress/' + data.id,
wallet.inkey,
data
)
.then(function (response) {
self.emailaddresses = _.reject(self.emailaddresses, function (obj) {
return obj.id == data.id
})
self.emailaddresses.push(LNSendmail(response.data))
self.emailaddressDialog.show = false
self.emailaddressDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteEmailaddress: function (emailaddressId) {
var self = this
var emailaddresses = _.findWhere(this.emailaddresses, {
id: emailaddressId
})
LNbits.utils
.confirmDialog(
'Are you sure you want to delete this emailaddress link?'
)
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/smtp/api/v1/emailaddress/' + emailaddressId,
_.findWhere(self.g.user.wallets, {id: emailaddresses.wallet})
.inkey
)
.then(function (response) {
self.emailaddresses = _.reject(self.emailaddresses, function (
obj
) {
return obj.id == emailaddressId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportEmailaddressesCSV: function () {
LNbits.utils.exportCSV(
this.emailaddressTable.columns,
this.emailaddresses
)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getEmailAddresses()
this.getEmails()
}
}
})
</script>
{% endblock %}

View file

@ -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,
},
)

View file

@ -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 . 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 .models import CreateEmail, CreateEmailaddress
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)

View file

@ -7,6 +7,7 @@ from lnbits.core.crud import get_wallet
from lnbits.db import SQLITE
from lnbits.helpers import urlsafe_short_hash
# todo: use the API, not direct import
from ..satspay.crud import delete_charge # type: ignore
from . import db
from .models import CreateService, Donation, Service

View file

@ -7,15 +7,13 @@ from starlette.responses import RedirectResponse
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.satspay.models import CreateCharge
from lnbits.extensions.streamalerts.models import (
CreateDonation,
CreateService,
ValidateDonation,
)
# todo: use the API, not direct import
from lnbits.extensions.satspay.models import CreateCharge # type: ignore
from lnbits.utils.exchange_rates import btc_price
from ..satspay.crud import create_charge, get_charge
# todo: use the API, not direct import
from ..satspay.crud import create_charge, get_charge # type: ignore
from . import streamalerts_ext
from .crud import (
authenticate_service,
@ -33,6 +31,7 @@ from .crud import (
update_donation,
update_service,
)
from .models import CreateDonation, CreateService, ValidateDonation
@streamalerts_ext.post("/api/v1/services")

View file

@ -2,7 +2,7 @@ import json
import httpx
from lnbits.extensions.subdomains.models import Domains
from .models import Domains
async def cloudflare_create_subdomain(

View file

@ -30,7 +30,7 @@ async def on_invoice_paid(payment: Payment) -> None:
### Create subdomain
cf_response = await cloudflare_create_subdomain(
domain=domain,
domain=domain, # type: ignore
subdomain=subdomain.subdomain,
record_type=subdomain.record_type,
ip=subdomain.ip,

View file

@ -6,7 +6,6 @@ from starlette.exceptions import HTTPException
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.subdomains.models import CreateDomain, CreateSubdomain
from . import subdomains_ext
from .cloudflare import cloudflare_create_subdomain, cloudflare_deletesubdomain
@ -22,6 +21,7 @@ from .crud import (
get_subdomains,
update_domain,
)
from .models import CreateDomain, CreateSubdomain
# domainS

View file

@ -2,6 +2,7 @@ from typing import Optional
from lnbits.db import SQLITE
# todo: use the API, not direct import
from ..satspay.crud import delete_charge # type: ignore
from . import db
from .models import Tip, TipJar, createTipJar
@ -33,7 +34,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 +47,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

View file

@ -6,8 +6,9 @@ from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type
from ..satspay.crud import create_charge
from ..satspay.models import CreateCharge
# todo: use the API, not direct import
from ..satspay.crud import create_charge # type: ignore
from ..satspay.models import CreateCharge # type: ignore
from . import tipjar_ext
from .crud import (
create_tip,

View file

@ -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

View file

@ -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 = ?
""",
(

View file

@ -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):

View file

@ -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):

Some files were not shown because too many files have changed in this diff Show more