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: strategy:
matrix: matrix:
python-version: ["3.9"] python-version: ["3.9"]
poetry-version: ["1.2.1"] poetry-version: ["1.3.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}

View file

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

View file

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

View file

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

View file

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

View file

@ -7,29 +7,29 @@ LNbits
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png) ![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)) (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! 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 * 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 * Extendable platform for exploring Lightning network functionality via the LNbits extension framework
* Part of a development stack via LNbits API * Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme * Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations * 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. 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. 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 ## 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) ![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). 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".
"Go to this website", has a lot less friction than "Download this app".
![lnurl ATM](https://i.imgur.com/xFWDnwy.png) ![lnurl ATM](https://i.imgur.com/xFWDnwy.png)
## Tip us ## 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/ [docs]: https://docs.lnbits.org/

View file

@ -28,7 +28,9 @@ Going over the example extension's structure:
Adding new dependencies 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 ```sh
$ poetry add <package> $ 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.** **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`. 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`.
`nix` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
SQLite to PostgreSQL migration SQLite to PostgreSQL migration

View file

@ -206,6 +206,10 @@ poetry add setuptools wheel
./venv/bin/pip install 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 ### 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: 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 decimal import Decimal
from typing import List, NamedTuple, Optional from typing import List, NamedTuple, Optional
import bitstring # type: ignore import bitstring
import embit import embit
import secp256k1 import secp256k1
from bech32 import CHARSET, bech32_decode, bech32_encode from bech32 import CHARSET, bech32_decode, bech32_encode
from ecdsa import SECP256k1, VerifyingKey # type: ignore from ecdsa import SECP256k1, VerifyingKey
from ecdsa.util import sigdecode_string # type: ignore from ecdsa.util import sigdecode_string
class Route(NamedTuple): class Route(NamedTuple):

View file

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

View file

@ -6,9 +6,9 @@ import time
from sqlite3 import Row from sqlite3 import Row
from typing import Dict, List, Optional from typing import Dict, List, Optional
from ecdsa import SECP256k1, SigningKey # type: ignore from ecdsa import SECP256k1, SigningKey
from fastapi import Query 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 loguru import logger
from pydantic import BaseModel from pydantic import BaseModel

View file

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

View file

@ -204,11 +204,11 @@
value: null, value: null,
label: 'Certificate' label: 'Certificate'
}, },
lnd_admin_macaroon: { lnd_rest_admin_macaroon: {
value: null, value: null,
label: 'Admin Macaroon' label: 'Admin Macaroon'
}, },
lnd_invoice_macaroon: { lnd_rest_invoice_macaroon: {
value: null, value: null,
label: 'Invoice Macaroon' label: 'Invoice Macaroon'
} }

View file

@ -16,7 +16,7 @@ from ..tasks import api_invoice_listeners
@core_app.get("/.well-known/lnurlp/{username}") @core_app.get("/.well-known/lnurlp/{username}")
async def lnaddress(username: str, request: Request): 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 domain = urlparse(str(request.url)).netloc
return await lnurl_response(username, domain, request) return await lnurl_response(username, domain, request)

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata from lnurl.types import LnurlPayMetadata
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse # type: ignore from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice 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 urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from fastapi.param_functions import Query from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request 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): class CreateCopilotData(BaseModel):

View file

@ -2,7 +2,7 @@ from typing import List
from fastapi import Depends, Request from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates 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.core.models import User
from lnbits.decorators import check_user_exists 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 import asyncio
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.extensions.events.models import CreateTicket
from lnbits.helpers import get_current_extension_name from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .models import CreateTicket
from .views_api import api_ticket_send_ticket 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.services import create_invoice
from lnbits.core.views.api import api_payment from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.events.models import CreateEvent, CreateTicket
from . import events_ext from . import events_ext
from .crud import ( from .crud import (
@ -24,6 +23,7 @@ from .crud import (
reg_ticket, reg_ticket,
update_event, update_event,
) )
from .models import CreateEvent, CreateTicket
# Events # 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 flat>
<q-card-section> <q-card-section>
<div class="text-h5 q-mb-md"> <div class="text-h5 q-mb-md">
{{SITE_TITLE}} Extension Development Guide Extension Development Guide
<small>(Collection of resources for extension developers)</small> <small
>(also check the
<a
class="text-primary"
href="http://docs.lnbits.org/devs/development.html"
>docs</a
>)</small
>
</div> </div>
<q-card unelevated flat> <q-card unelevated flat>
@ -188,8 +195,8 @@
<p> <p>
LNbits uses LNbits uses
<a href="https://vuejs.org/" class="text-primary">Vue</a> <a href="https://vuejs.org/" class="text-primary">Vue</a>
components for best-in-class high-performance and responsive for best-in-class, responsive and high-performance
performance. components.
</p> </p>
<p>Typical example of Vue components in a frontend script:</p> <p>Typical example of Vue components in a frontend script:</p>
@ -199,8 +206,7 @@
/><br /><br /> /><br /><br />
<p> <p>
In a page body, models can be called. <br />Content can be Content can be conditionally rendered using Vue's
conditionally rendered using Vue's
<code class="bg-grey-3 text-black">v-if</code>: <code class="bg-grey-3 text-black">v-if</code>:
</p> </p>
<img <img
@ -220,6 +226,8 @@
<q-tabs v-model="usefultab" align="left"> <q-tabs v-model="usefultab" align="left">
<q-tab name="magicalg">MAGICAL G</q-tab> <q-tab name="magicalg">MAGICAL G</q-tab>
<q-tab name="exchange">EXCHANGE RATES</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> </q-tabs>
</template> </template>
@ -255,6 +263,85 @@
>:<br /> >:<br />
<img src="./static/conversion-example2.png" /> <img src="./static/conversion-example2.png" />
</q-tab-panel> </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> </q-tab-panels>
</template> </template>
</div> </div>
@ -296,6 +383,8 @@
data: function () { data: function () {
return { return {
///// Declare models/variables ///// ///// Declare models/variables /////
protocol: window.location.protocol,
location: '//' + window.location.hostname,
thingDialog: { thingDialog: {
show: false, show: false,
data: {} data: {}
@ -310,7 +399,7 @@
}, },
///// Where functions live ///// ///// Where functions live /////
methods: { methods: {
exampleFunction(data) { exampleFunction: function (data) {
var theData = data var theData = data
LNbits.api LNbits.api
.request( .request(
@ -325,6 +414,28 @@
LNbits.utils.notifyApiError(error) // Error will be passed to the frontend 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() { sendThingDialog() {
console.log(this.thingDialog) console.log(this.thingDialog)
} }
@ -333,6 +444,7 @@
created: function () { created: function () {
self = this // Often used to run a real object, rather than the event (all a bit confusing really) self = this // Often used to run a real object, rather than the event (all a bit confusing really)
self.exampleFunction('lorum') self.exampleFunction('lorum')
self.initWs()
} }
}) })
</script> </script>

View file

@ -1,12 +1,9 @@
import hashlib
import math import math
from http import HTTPStatus from http import HTTPStatus
from os import name
from fastapi.exceptions import HTTPException from fastapi import HTTPException, Query, Request
from fastapi.params import Query
from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse 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 from lnbits.core.services import create_invoice
@ -29,9 +26,12 @@ async def lnurl_livestream(ls_id, request: Request):
) )
resp = LnurlPayResponse( resp = LnurlPayResponse(
callback=request.url_for("livestream.lnurl_callback", track_id=track.id), callback=ClearnetUrl(
min_sendable=track.min_sendable, request.url_for("livestream.lnurl_callback", track_id=track.id),
max_sendable=track.max_sendable, scheme="https",
),
minSendable=MilliSatoshi(track.min_sendable),
maxSendable=MilliSatoshi(track.max_sendable),
metadata=await track.lnurlpay_metadata(), 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.") raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Track not found.")
resp = LnurlPayResponse( resp = LnurlPayResponse(
callback=request.url_for("livestream.lnurl_callback", track_id=track.id), callback=ClearnetUrl(
min_sendable=track.min_sendable, request.url_for("livestream.lnurl_callback", track_id=track.id),
max_sendable=track.max_sendable, scheme="https",
),
minSendable=MilliSatoshi(track.min_sendable),
maxSendable=MilliSatoshi(track.max_sendable),
metadata=await track.lnurlpay_metadata(), metadata=await track.lnurlpay_metadata(),
) )
@ -85,6 +88,7 @@ async def lnurl_callback(
).dict() ).dict()
ls = await get_livestream_by_track(track_id) ls = await get_livestream_by_track(track_id)
assert ls
extra_amount = amount_received - int(amount_received * (100 - ls.fee_pct) / 100) 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: if amount_received < track.price_msat:
success_action = None success_action = None
else: else:
success_action = track.success_action(payment_hash, request=request) success_action = track.success_action(payment_hash, request=request)
resp = LnurlPayActionResponse( resp = LnurlPayActionResponse(
pr=payment_request, success_action=success_action, routes=[] pr=LightningInvoice(payment_request), successAction=success_action, routes=[]
) )
return resp.dict() return resp.dict()

View file

@ -1,13 +1,12 @@
import json import json
from typing import Optional from typing import Optional
from fastapi import Query from fastapi import Query, Request
from lnurl import Lnurl from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore from lnurl import encode as lnurl_encode
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore from lnurl.models import ClearnetUrl, Max144Str, UrlAction
from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request
class CreateTrack(BaseModel): class CreateTrack(BaseModel):
@ -32,7 +31,7 @@ class Livestream(BaseModel):
class Track(BaseModel): class Track(BaseModel):
id: int id: int
download_url: Optional[str] download_url: Optional[str]
price_msat: Optional[int] price_msat: int = 0
name: str name: str
producer: int producer: int
@ -71,7 +70,7 @@ class Track(BaseModel):
def success_action( def success_action(
self, payment_hash: str, request: Request self, payment_hash: str, request: Request
) -> Optional[LnurlPaySuccessAction]: ) -> Optional[UrlAction]:
if not self.download_url: if not self.download_url:
return None return None
@ -79,7 +78,8 @@ class Track(BaseModel):
url_with_query = f"{url}?p={payment_hash}" url_with_query = f"{url}?p={payment_hash}"
return UrlAction( 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 http import HTTPStatus
from fastapi.param_functions import Depends from fastapi import Depends, HTTPException, Query, Request
from fastapi.params import Query from starlette.datastructures import URL
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse, RedirectResponse from starlette.responses import HTMLResponse, RedirectResponse
from lnbits.core.crud import get_wallet_payment 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 lnbits.decorators import check_user_exists
from . import livestream_ext, livestream_renderer from . import livestream_ext, livestream_renderer
from .crud import get_livestream_by_track, get_track from .crud import get_livestream_by_track, get_track
# from mmap import MAP_DENYWRITE
@livestream_ext.get("/", response_class=HTMLResponse) @livestream_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): 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 payment_hash = p
track = await get_track(track_id) track = await get_track(track_id)
ls = await get_livestream_by_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: if not payment:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, 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: if payment.pending:
@ -41,4 +43,6 @@ async def track_redirect_download(track_id, p: str = Query(...)):
status_code=HTTPStatus.PAYMENT_REQUIRED, status_code=HTTPStatus.PAYMENT_REQUIRED,
detail=f"Payment {payment_hash} wasn't received yet. Please try again in a minute.", 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 http import HTTPStatus
from fastapi.param_functions import Depends from fastapi import Depends, HTTPException, Request
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl 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.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.livestream.models import CreateTrack
from . import livestream_ext from . import livestream_ext
from .crud import ( from .crud import (
@ -20,6 +17,7 @@ from .crud import (
update_livestream_fee, update_livestream_fee,
update_track, update_track,
) )
from .models import CreateTrack
@livestream_ext.get("/api/v1/livestream") @livestream_ext.get("/api/v1/livestream")
@ -27,6 +25,7 @@ async def api_livestream_from_wallet(
req: Request, g: WalletTypeInfo = Depends(get_key_type) req: Request, g: WalletTypeInfo = Depends(get_key_type)
): ):
ls = await get_or_create_livestream_by_wallet(g.wallet.id) ls = await get_or_create_livestream_by_wallet(g.wallet.id)
assert ls
tracks = await get_tracks(ls.id) tracks = await get_tracks(ls.id)
producers = await get_producers(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) id = int(track_id)
except ValueError: except ValueError:
id = 0 id = 0
if id <= 0:
id = None
ls = await get_or_create_livestream_by_wallet(g.wallet.id) 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 return "", HTTPStatus.NO_CONTENT
@livestream_ext.put("/api/v1/livestream/fee/{fee_pct}") @livestream_ext.put("/api/v1/livestream/fee/{fee_pct}")
async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)): async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)):
ls = await get_or_create_livestream_by_wallet(g.wallet.id) ls = await get_or_create_livestream_by_wallet(g.wallet.id)
assert ls
await update_livestream_fee(ls.id, int(fee_pct)) await update_livestream_fee(ls.id, int(fee_pct))
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -76,9 +75,10 @@ async def api_add_track(
data: CreateTrack, id=None, g: WalletTypeInfo = Depends(get_key_type) data: CreateTrack, id=None, g: WalletTypeInfo = Depends(get_key_type)
): ):
ls = await get_or_create_livestream_by_wallet(g.wallet.id) ls = await get_or_create_livestream_by_wallet(g.wallet.id)
assert ls
if data.producer_id: if data.producer_id:
p_id = data.producer_id p_id = int(data.producer_id)
elif data.producer_name: elif data.producer_name:
p_id = await add_producer(ls.id, data.producer_name) p_id = await add_producer(ls.id, data.producer_name)
else: else:
@ -96,5 +96,6 @@ async def api_add_track(
@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}") @livestream_ext.delete("/api/v1/livestream/tracks/{track_id}")
async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)): async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
ls = await get_or_create_livestream_by_wallet(g.wallet.id) ls = await get_or_create_livestream_by_wallet(g.wallet.id)
assert ls
await delete_track_from_livestream(ls.id, track_id) await delete_track_from_livestream(ls.id, track_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View file

@ -2,7 +2,7 @@ import json
import httpx import httpx
from lnbits.extensions.lnaddress.models import Domains from .models import Domains
async def cloudflare_create_record(domain: Domains, ip: str): 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.crud import get_user
from lnbits.core.services import check_transaction_status, create_invoice from lnbits.core.services import check_transaction_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.lnaddress.models import CreateAddress, CreateDomain
from . import lnaddress_ext from . import lnaddress_ext
from .cloudflare import cloudflare_create_record from .cloudflare import cloudflare_create_record
@ -23,6 +22,7 @@ from .crud import (
get_domains, get_domains,
update_domain, update_domain,
) )
from .models import CreateAddress, CreateDomain
# DOMAINS # DOMAINS

View file

@ -5,7 +5,7 @@ from fastapi.param_functions import Security
from fastapi.security.api_key import APIKeyHeader from fastapi.security.api_key import APIKeyHeader
from starlette.exceptions import HTTPException 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( api_key_header_auth = APIKeyHeader(
name="AUTHORIZATION", 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.services import create_invoice
from lnbits.core.views.api import api_payment from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.lnticket.models import CreateFormData, CreateTicketData
from . import lnticket_ext from . import lnticket_ext
from .crud import ( from .crud import (
@ -23,6 +22,7 @@ from .crud import (
set_ticket_paid, set_ticket_paid,
update_form, update_form,
) )
from .models import CreateFormData, CreateTicketData
# FORMS # FORMS

View file

@ -7,8 +7,6 @@ from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import createLnurldevice, lnurldevicepayment, lnurldevices from .models import createLnurldevice, lnurldevicepayment, lnurldevices
###############lnurldeviceS##########################
async def create_lnurldevice( async def create_lnurldevice(
data: createLnurldevice, data: createLnurldevice,
@ -69,10 +67,12 @@ async def create_lnurldevice(
data.pin4, 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()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute( await db.execute(
f"UPDATE lnurldevice.lnurldevices SET {q} WHERE id = ?", 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( row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,) "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( row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,) "SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
) )
return lnurldevices(**row) if row else None return lnurldevices(**row) if row else None
async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]: async def get_lnurldevices(wallet_ids: List[str]) -> List[lnurldevices]:
wallet_ids = [wallet_ids] q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids[0]))
rows = await db.fetchall( rows = await db.fetchall(
f""" f"""
SELECT * FROM lnurldevice.lnurldevices WHERE wallet IN ({q}) 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,), (*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: 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,) "DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
) )
########################lnuldevice payments###########################
async def create_lnurldevicepayment( async def create_lnurldevicepayment(
deviceid: str, deviceid: str,
@ -121,6 +118,7 @@ async def create_lnurldevicepayment(
sats: Optional[int] = 0, sats: Optional[int] = 0,
) -> lnurldevicepayment: ) -> lnurldevicepayment:
device = await get_lnurldevice(deviceid) device = await get_lnurldevice(deviceid)
assert device
if device.device == "atm": if device.device == "atm":
lnurldevicepayment_id = shortuuid.uuid(name=payload) lnurldevicepayment_id = shortuuid.uuid(name=payload)
else: else:
@ -139,7 +137,9 @@ async def create_lnurldevicepayment(
""", """,
(lnurldevicepayment_id, deviceid, payload, pin, payhash, sats), (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( async def update_lnurldevicepayment(
@ -157,7 +157,9 @@ async def update_lnurldevicepayment(
return lnurldevicepayment(**row) if row else None 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( row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", "SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,), (lnurldevicepayment_id,),
@ -165,7 +167,9 @@ async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayme
return lnurldevicepayment(**row) if row else None 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( row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?", "SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?",
(lnurldevicepayment_payload,), (lnurldevicepayment_payload,),

View file

@ -1,16 +1,11 @@
import base64 import base64
import hashlib
import hmac import hmac
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO from io import BytesIO
from typing import Optional
import shortuuid import shortuuid
from embit import bech32, compact from embit import bech32, compact
from fastapi import Request from fastapi import HTTPException, Query, Request
from fastapi.param_functions import Query
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
@ -44,7 +39,9 @@ def bech32_decode(bech):
encoding = bech32.bech32_verify_checksum(hrp, data) encoding = bech32.bech32_verify_checksum(hrp, data)
if encoding is None: if encoding is None:
return 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): def xor_decrypt(key, blob):
@ -105,6 +102,8 @@ async def lnurl_v1_params(
"reason": f"lnurldevice {device_id} not found on this server", "reason": f"lnurldevice {device_id} not found on this server",
} }
if device.device == "switch": if device.device == "switch":
# TODO: AMOUNT IN CENT was never reference here
amount_in_cent = 0
price_msat = ( price_msat = (
await fiat_amount_as_satoshis(float(profit), device.currency) await fiat_amount_as_satoshis(float(profit), device.currency)
if device.currency != "sat" if device.currency != "sat"
@ -160,23 +159,18 @@ async def lnurl_v1_params(
if device.device != "atm": if device.device != "atm":
return {"status": "ERROR", "reason": "Not ATM device."} return {"status": "ERROR", "reason": "Not ATM device."}
price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000) price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000)
lnurldevicepayment = await get_lnurldevicepayment(shortuuid.uuid(name=p)) try:
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:
lnurldevicepayment = await create_lnurldevicepayment( lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id, deviceid=device.id,
payload=p, payload=p,
sats=price_msat * 1000, sats=price_msat * 1000,
pin=pin, pin=str(pin),
payhash="payment_hash", payhash="payment_hash",
) )
except:
return {"status": "ERROR", "reason": "Could not create ATM payment."}
if not lnurldevicepayment: if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create payment."} return {"status": "ERROR", "reason": "Could not create ATM payment."}
return { return {
"tag": "withdrawRequest", "tag": "withdrawRequest",
"callback": request.url_for( "callback": request.url_for(
@ -193,7 +187,7 @@ async def lnurl_v1_params(
deviceid=device.id, deviceid=device.id,
payload=p, payload=p,
sats=price_msat * 1000, sats=price_msat * 1000,
pin=pin, pin=str(pin),
payhash="payment_hash", payhash="payment_hash",
) )
if not lnurldevicepayment: if not lnurldevicepayment:
@ -221,6 +215,10 @@ async def lnurl_callback(
k1: str = Query(None), k1: str = Query(None),
): ):
lnurldevicepayment = await get_lnurldevicepayment(paymentid) lnurldevicepayment = await get_lnurldevicepayment(paymentid)
if not lnurldevicepayment:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevicepayment not found."
)
device = await get_lnurldevice(lnurldevicepayment.deviceid) device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device: if not device:
raise HTTPException( raise HTTPException(
@ -241,13 +239,17 @@ async def lnurl_callback(
else: else:
if lnurldevicepayment.payload != k1: if lnurldevicepayment.payload != k1:
return {"status": "ERROR", "reason": "Bad 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 lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
) )
assert lnurldevicepayment_updated
await pay_invoice( await pay_invoice(
wallet_id=device.wallet, wallet_id=device.wallet,
payment_request=pr, payment_request=pr,
max_sat=lnurldevicepayment.sats / 1000, max_sat=int(lnurldevicepayment_updated.sats / 1000),
extra={"tag": "withdraw"}, extra={"tag": "withdraw"},
) )
return {"status": "OK"} return {"status": "OK"}

View file

@ -3,13 +3,9 @@ from sqlite3 import Row
from typing import List, Optional from typing import List, Optional
from fastapi import Request from fastapi import Request
from lnurl import Lnurl from lnurl import encode as lnurl_encode
from lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.main import BaseModel
class createLnurldevice(BaseModel): class createLnurldevice(BaseModel):
@ -58,6 +54,7 @@ class lnurldevices(BaseModel):
pin4: int pin4: int
timestamp: str timestamp: str
@classmethod
def from_row(cls, row: Row) -> "lnurldevices": def from_row(cls, row: Row) -> "lnurldevices":
return cls(**dict(row)) return cls(**dict(row))

View file

@ -1,18 +1,11 @@
import asyncio 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.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.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener 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(): async def wait_for_paid_invoices():
@ -27,14 +20,15 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
# (avoid loops) # (avoid loops)
if "Switch" == payment.extra.get("tag"): if "Switch" == payment.extra.get("tag"):
lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id")) lnurldevicepayment = await get_lnurldevicepayment(payment.extra["id"])
if not lnurldevicepayment: if not lnurldevicepayment:
return return
if lnurldevicepayment.payhash == "used": if lnurldevicepayment.payhash == "used":
return return
lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=payment.extra.get("id"), payhash="used" lnurldevicepayment_id=payment.extra["id"], payhash="used"
) )
assert lnurldevicepayment
return await websocketUpdater( return await websocketUpdater(
lnurldevicepayment.deviceid, lnurldevicepayment.deviceid,
str(lnurldevicepayment.pin) + "-" + str(lnurldevicepayment.payload), str(lnurldevicepayment.pin) + "-" + str(lnurldevicepayment.payload),

View file

@ -1,12 +1,7 @@
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO
import pyqrcode from fastapi import Depends, HTTPException, Query, Request
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse, StreamingResponse from starlette.responses import HTMLResponse, StreamingResponse
from lnbits.core.crud import update_payment_status from lnbits.core.crud import update_payment_status
@ -62,4 +57,6 @@ async def img(request: Request, lnurldevice_id):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." 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 http import HTTPStatus
from fastapi import Request from fastapi import Depends, HTTPException, Query, Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key 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 lnbits.utils.exchange_rates import currencies
from . import lnurldevice_ext from . import lnurldevice_ext
@ -26,9 +22,6 @@ async def api_list_currencies_available():
return list(currencies.keys()) return list(currencies.keys())
#######################lnurldevice##########################
@lnurldevice_ext.post("/api/v1/lnurlpos") @lnurldevice_ext.post("/api/v1/lnurlpos")
@lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}") @lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_create_or_update( async def api_lnurldevice_create_or_update(
@ -41,7 +34,7 @@ async def api_lnurldevice_create_or_update(
lnurldevice = await create_lnurldevice(data) lnurldevice = await create_lnurldevice(data)
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
else: 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)}} return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
@ -49,7 +42,8 @@ async def api_lnurldevice_create_or_update(
async def api_lnurldevices_retrieve( async def api_lnurldevices_retrieve(
req: Request, wallet: WalletTypeInfo = Depends(get_key_type) 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: try:
return [ return [
{**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
@ -65,10 +59,11 @@ async def api_lnurldevices_retrieve(
return "" 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( async def api_lnurldevice_retrieve(
req: Request, req: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
lnurldevice_id: str = Query(None), lnurldevice_id: str = Query(None),
): ):
lnurldevice = await get_lnurldevice(lnurldevice_id) lnurldevice = await get_lnurldevice(lnurldevice_id)
@ -76,23 +71,18 @@ async def api_lnurldevice_retrieve(
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist" 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)}} return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}") @lnurldevice_ext.delete(
async def api_lnurldevice_delete( "/api/v1/lnurlpos/{lnurldevice_id}", dependencies=[Depends(require_admin_key)]
wallet: WalletTypeInfo = Depends(require_admin_key), )
lnurldevice_id: str = Query(None), async def api_lnurldevice_delete(lnurldevice_id: str = Query(None)):
):
lnurldevice = await get_lnurldevice(lnurldevice_id) lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice: if not lnurldevice:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
) )
await delete_lnurldevice(lnurldevice_id) await delete_lnurldevice(lnurldevice_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View file

@ -3,11 +3,7 @@ import math
from http import HTTPStatus from http import HTTPStatus
from fastapi import Request from fastapi import Request
from lnurl import ( # type: ignore from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse
LnurlErrorResponse,
LnurlPayActionResponse,
LnurlPayResponse,
)
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice 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 urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from fastapi.param_functions import Query from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request 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): class CreatePayLinkData(BaseModel):

View file

@ -2,7 +2,7 @@ import json
from http import HTTPStatus from http import HTTPStatus
from fastapi import Depends, Query, Request 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 starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.core.crud import get_user

View file

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

View file

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

View file

@ -200,7 +200,10 @@
:href="props.row.wallet" :href="props.row.wallet"
target="_blank" target="_blank"
></q-btn> ></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>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }} {{ col.value }}

View file

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

View file

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

View file

@ -113,6 +113,23 @@ async def api_market_product_create(
if stall.currency != "sat": if stall.currency != "sat":
data.price *= settings.fiat_base_multiplier 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: if product_id:
product = await get_market_product(product_id) product = await get_market_product(product_id)
if not product: if not product:

View file

@ -1,4 +1,3 @@
# type: ignore
from os import getenv from os import getenv
from fastapi import Depends, Request from fastapi import Depends, Request
@ -36,5 +35,5 @@ ngrok_tunnel = ngrok.connect(port)
@ngrok_ext.get("/") @ngrok_ext.get("/")
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return ngrok_renderer().TemplateResponse( 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

@ -42,3 +42,18 @@ location /.well-known/nostr.json {
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; 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: async def create_domain_internal(wallet_id: str, data: CreateDomainData) -> Domain:
domain_id = urlsafe_short_hash() domain_id = urlsafe_short_hash()
if data.currency != "Satoshis":
amount = data.amount * 100
else:
amount = data.amount
await db.execute( await db.execute(
""" """
INSERT INTO nostrnip5.domains (id, wallet, currency, amount, domain) INSERT INTO nostrnip5.domains (id, wallet, currency, amount, domain)
VALUES (?, ?, ?, ?, ?) 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) domain = await get_domain(domain_id)

View file

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

View file

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

View file

@ -5,7 +5,7 @@ from http import HTTPStatus
from fastapi import Request from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from starlette.exceptions import HTTPException 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 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 import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from lnurl import Lnurl from lnurl import Lnurl
from lnurl import encode as lnurl_encode # type: ignore from lnurl import encode as lnurl_encode
from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.main import BaseModel from pydantic.main import BaseModel

View file

@ -1,7 +1,7 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import Depends, Query, Request 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 starlette.exceptions import HTTPException
from lnbits.core.crud import get_user 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.core.views.api import api_payment
from lnbits.helpers import urlsafe_short_hash 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 . import db
from .helpers import fetch_onchain_balance from .helpers import fetch_onchain_balance
from .models import Charges, CreateCharge, SatsPayThemes from .models import Charges, CreateCharge, SatsPayThemes

View file

@ -4,11 +4,10 @@ import json
from loguru import logger from loguru import logger
from lnbits.core.models import Payment 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.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener 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 from .helpers import call_webhook

View file

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

View file

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

View file

@ -3,7 +3,7 @@ from sqlite3 import Row
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request 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): 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.db import SQLITE
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
# todo: use the API, not direct import
from ..satspay.crud import delete_charge # type: ignore from ..satspay.crud import delete_charge # type: ignore
from . import db from . import db
from .models import CreateService, Donation, Service 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.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.satspay.models import CreateCharge
from lnbits.extensions.streamalerts.models import ( # todo: use the API, not direct import
CreateDonation, from lnbits.extensions.satspay.models import CreateCharge # type: ignore
CreateService,
ValidateDonation,
)
from lnbits.utils.exchange_rates import btc_price 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 . import streamalerts_ext
from .crud import ( from .crud import (
authenticate_service, authenticate_service,
@ -33,6 +31,7 @@ from .crud import (
update_donation, update_donation,
update_service, update_service,
) )
from .models import CreateDonation, CreateService, ValidateDonation
@streamalerts_ext.post("/api/v1/services") @streamalerts_ext.post("/api/v1/services")

View file

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

View file

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

View file

@ -6,7 +6,6 @@ from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.core.services import check_transaction_status, create_invoice from lnbits.core.services import check_transaction_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.subdomains.models import CreateDomain, CreateSubdomain
from . import subdomains_ext from . import subdomains_ext
from .cloudflare import cloudflare_create_subdomain, cloudflare_deletesubdomain from .cloudflare import cloudflare_create_subdomain, cloudflare_deletesubdomain
@ -22,6 +21,7 @@ from .crud import (
get_subdomains, get_subdomains,
update_domain, update_domain,
) )
from .models import CreateDomain, CreateSubdomain
# domainS # domainS

View file

@ -2,6 +2,7 @@ from typing import Optional
from lnbits.db import SQLITE from lnbits.db import SQLITE
# todo: use the API, not direct import
from ..satspay.crud import delete_charge # type: ignore from ..satspay.crud import delete_charge # type: ignore
from . import db from . import db
from .models import Tip, TipJar, createTipJar from .models import Tip, TipJar, createTipJar
@ -33,7 +34,11 @@ async def create_tip(
async def create_tipjar(data: createTipJar) -> TipJar: async def create_tipjar(data: createTipJar) -> TipJar:
"""Create a new 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""" f"""
INSERT INTO tipjar.TipJars ( INSERT INTO tipjar.TipJars (
name, name,
@ -42,11 +47,16 @@ async def create_tipjar(data: createTipJar) -> TipJar:
onchain onchain
) )
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
{returning}
""", """,
(data.name, data.wallet, data.webhook, data.onchain), (data.name, data.wallet, data.webhook, data.onchain),
) )
row = await db.fetchone("SELECT * FROM tipjar.TipJars LIMIT 1") if db.type == SQLITE:
tipjar = TipJar(**row) tipjar_id = result._result_proxy.lastrowid
else:
tipjar_id = result[0]
tipjar = await get_tipjar(tipjar_id)
assert tipjar assert tipjar
return tipjar return tipjar

View file

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

View file

@ -20,9 +20,6 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if not payment.extra:
return
if payment.extra.get("tag") != "tpos": if payment.extra.get("tag") != "tpos":
return return

View file

@ -41,8 +41,9 @@ async def create_watch_wallet(user: str, w: WalletAccount) -> WalletAccount:
w.meta, w.meta,
), ),
) )
wallet = await get_watch_wallet(wallet_id)
return await get_watch_wallet(wallet_id) assert wallet
return wallet
async def get_watch_wallet(wallet_id: str) -> Optional[WalletAccount]: async def get_watch_wallet(wallet_id: str) -> Optional[WalletAccount]:
@ -121,11 +122,11 @@ async def create_fresh_addresses(
change_address=False, change_address=False,
) -> List[Address]: ) -> List[Address]:
if start_address_index > end_address_index: if start_address_index > end_address_index:
return None return []
wallet = await get_watch_wallet(wallet_id) wallet = await get_watch_wallet(wallet_id)
if not wallet: if not wallet:
return None return []
branch_index = 1 if change_address else 0 branch_index = 1 if change_address else 0

View file

@ -1,6 +1,6 @@
from embit.descriptor import Descriptor, Key # type: ignore from embit.descriptor import Descriptor, Key
from embit.descriptor.arguments import AllowedDerivation # type: ignore from embit.descriptor.arguments import AllowedDerivation
from embit.networks import NETWORKS # type: ignore from embit.networks import NETWORKS
def detect_network(k): def detect_network(k):

View file

@ -1,7 +1,7 @@
from sqlite3 import Row from sqlite3 import Row
from typing import List, Optional from typing import List, Optional
from fastapi.param_functions import Query from fastapi import Query
from pydantic import BaseModel from pydantic import BaseModel
@ -35,7 +35,7 @@ class Address(BaseModel):
amount: int = 0 amount: int = 0
branch_index: int = 0 branch_index: int = 0
address_index: int address_index: int
note: str = None note: Optional[str] = None
has_activity: bool = False has_activity: bool = False
@classmethod @classmethod
@ -57,9 +57,9 @@ class TransactionInput(BaseModel):
class TransactionOutput(BaseModel): class TransactionOutput(BaseModel):
amount: int amount: int
address: str address: str
branch_index: int = None branch_index: Optional[int] = None
address_index: int = None address_index: Optional[int] = None
wallet: str = None wallet: Optional[str] = None
class MasterPublicKey(BaseModel): class MasterPublicKey(BaseModel):

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