Merge branch 'main' into fix/mypy-bleskomat
This commit is contained in:
commit
affcb9feca
108 changed files with 4196 additions and 1481 deletions
2
.github/workflows/formatting.yml
vendored
2
.github/workflows/formatting.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
2
.github/workflows/migrations.yml
vendored
2
.github/workflows/migrations.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
2
.github/workflows/mypy.yml
vendored
2
.github/workflows/mypy.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
6
.github/workflows/regtest.yml
vendored
6
.github/workflows/regtest.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -7,29 +7,29 @@ LNbits
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# LNbits v0.9 BETA, free and open-source lightning-network wallet/accounts system
|
# LNbits v0.9 BETA, free and open-source Lightning wallet accounts system
|
||||||
|
|
||||||
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
|
(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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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".
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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/
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,15 +141,15 @@
|
||||||
return {
|
return {
|
||||||
settings: {},
|
settings: {},
|
||||||
lnbits_theme_options: [
|
lnbits_theme_options: [
|
||||||
'classic',
|
'classic',
|
||||||
'bitcoin',
|
'bitcoin',
|
||||||
'flamingo',
|
'flamingo',
|
||||||
'freedom',
|
'freedom',
|
||||||
'mint',
|
'mint',
|
||||||
'autumn',
|
'autumn',
|
||||||
'monochrome',
|
'monochrome',
|
||||||
'salvador'
|
'salvador'
|
||||||
],
|
],
|
||||||
formData: {},
|
formData: {},
|
||||||
formAddAdmin: '',
|
formAddAdmin: '',
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
11
lnbits/extensions/deezy/README.md
Normal file
11
lnbits/extensions/deezy/README.md
Normal 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
|
||||||
25
lnbits/extensions/deezy/__init__.py
Normal file
25
lnbits/extensions/deezy/__init__.py
Normal 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
|
||||||
6
lnbits/extensions/deezy/config.json
Normal file
6
lnbits/extensions/deezy/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Deezy",
|
||||||
|
"short_description": "LN to onchain, onchain to LN swaps",
|
||||||
|
"tile": "/deezy/static/deezy.png",
|
||||||
|
"contributors": ["Uthpala"]
|
||||||
|
}
|
||||||
115
lnbits/extensions/deezy/crud.py
Normal file
115
lnbits/extensions/deezy/crud.py
Normal 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,
|
||||||
|
),
|
||||||
|
)
|
||||||
37
lnbits/extensions/deezy/migrations.py
Normal file
37
lnbits/extensions/deezy/migrations.py
Normal 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
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
34
lnbits/extensions/deezy/models.py
Normal file
34
lnbits/extensions/deezy/models.py
Normal 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 = ""
|
||||||
BIN
lnbits/extensions/deezy/static/deezy.png
Normal file
BIN
lnbits/extensions/deezy/static/deezy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
253
lnbits/extensions/deezy/templates/deezy/_api_docs.html
Normal file
253
lnbits/extensions/deezy/templates/deezy/_api_docs.html
Normal 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>
|
||||||
588
lnbits/extensions/deezy/templates/deezy/index.html
Normal file
588
lnbits/extensions/deezy/templates/deezy/index.html
Normal 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 %}
|
||||||
21
lnbits/extensions/deezy/views.py
Normal file
21
lnbits/extensions/deezy/views.py
Normal 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()}
|
||||||
|
)
|
||||||
65
lnbits/extensions/deezy/views_api.py
Normal file
65
lnbits/extensions/deezy/views_api.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/example/static/qrcode-example.png
Normal file
BIN
lnbits/extensions/example/static/qrcode-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
BIN
lnbits/extensions/example/static/qrcode-example1.png
Normal file
BIN
lnbits/extensions/example/static/qrcode-example1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
lnbits/extensions/example/static/websocket-example.png
Normal file
BIN
lnbits/extensions/example/static/websocket-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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}!"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,),
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
self.productDialog.data.categories = self.productDialog.data.categories.split(
|
if (self.productDialog.data.categories) {
|
||||||
','
|
self.productDialog.data.categories = self.productDialog.data.categories.split(
|
||||||
)
|
','
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (self.productDialog.data.image.startsWith('data:')) {
|
||||||
|
self.productDialog.url = false
|
||||||
|
}
|
||||||
|
|
||||||
self.productDialog.show = true
|
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}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,19 @@ location /.well-known/nostr.json {
|
||||||
proxy_cache_valid 200 300s;
|
proxy_cache_valid 200 300s;
|
||||||
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}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
)
|
)
|
||||||
|
|
||||||
obj.amount = parseFloat(obj.amount / 100).toFixed(2)
|
if (obj.currency != 'Satoshis') {
|
||||||
|
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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
14
lnbits/extensions/smtp/README.md
Normal file
14
lnbits/extensions/smtp/README.md
Normal 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.
|
||||||
|
|
||||||
34
lnbits/extensions/smtp/__init__.py
Normal file
34
lnbits/extensions/smtp/__init__.py
Normal 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))
|
||||||
6
lnbits/extensions/smtp/config.json
Normal file
6
lnbits/extensions/smtp/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "SMTP",
|
||||||
|
"short_description": "Charge sats for sending emails",
|
||||||
|
"tile": "/smtp/static/smtp-bitcoin-email.png",
|
||||||
|
"contributors": ["dni"]
|
||||||
|
}
|
||||||
168
lnbits/extensions/smtp/crud.py
Normal file
168
lnbits/extensions/smtp/crud.py
Normal 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,))
|
||||||
35
lnbits/extensions/smtp/migrations.py
Normal file
35
lnbits/extensions/smtp/migrations.py
Normal 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}
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
47
lnbits/extensions/smtp/models.py
Normal file
47
lnbits/extensions/smtp/models.py
Normal 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
|
||||||
86
lnbits/extensions/smtp/smtp.py
Normal file
86
lnbits/extensions/smtp/smtp.py
Normal 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()
|
||||||
BIN
lnbits/extensions/smtp/static/smtp-bitcoin-email.png
Normal file
BIN
lnbits/extensions/smtp/static/smtp-bitcoin-email.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
36
lnbits/extensions/smtp/tasks.py
Normal file
36
lnbits/extensions/smtp/tasks.py
Normal 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)
|
||||||
23
lnbits/extensions/smtp/templates/smtp/_api_docs.html
Normal file
23
lnbits/extensions/smtp/templates/smtp/_api_docs.html
Normal 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>
|
||||||
175
lnbits/extensions/smtp/templates/smtp/display.html
Normal file
175
lnbits/extensions/smtp/templates/smtp/display.html
Normal 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 %}
|
||||||
528
lnbits/extensions/smtp/templates/smtp/index.html
Normal file
528
lnbits/extensions/smtp/templates/smtp/index.html
Normal 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 %}
|
||||||
40
lnbits/extensions/smtp/views.py
Normal file
40
lnbits/extensions/smtp/views.py
Normal 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
170
lnbits/extensions/smtp/views_api.py
Normal file
170
lnbits/extensions/smtp/views_api.py
Normal 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)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -150,7 +151,7 @@ async def create_fresh_addresses(
|
||||||
# return fresh addresses
|
# return fresh addresses
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM watchonly.addresses
|
SELECT * FROM watchonly.addresses
|
||||||
WHERE wallet = ? AND branch_index = ? AND address_index >= ? AND address_index < ?
|
WHERE wallet = ? AND branch_index = ? AND address_index >= ? AND address_index < ?
|
||||||
ORDER BY branch_index, address_index
|
ORDER BY branch_index, address_index
|
||||||
""",
|
""",
|
||||||
|
|
@ -172,7 +173,7 @@ async def get_address_at_index(
|
||||||
) -> Optional[Address]:
|
) -> Optional[Address]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM watchonly.addresses
|
SELECT * FROM watchonly.addresses
|
||||||
WHERE wallet = ? AND branch_index = ? AND address_index = ?
|
WHERE wallet = ? AND branch_index = ? AND address_index = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue