diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml
index 709ebcb3..f3dca3ad 100644
--- a/.github/workflows/formatting.yml
+++ b/.github/workflows/formatting.yml
@@ -27,12 +27,11 @@ jobs:
- name: Install packages
run: |
poetry install
+ npm install prettier
- name: Check black
run: make checkblack
- name: Check isort
run: make checkisort
- uses: actions/setup-node@v3
- name: Check prettier
- run: |
- npm install prettier
- make checkprettier
+ run: make checkprettier
diff --git a/.gitignore b/.gitignore
index 79ff36ab..d686ae12 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,4 @@ fly.toml
# Ignore extensions (post installable extension PR)
extensions/
-upgrades/
\ No newline at end of file
+upgrades/
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..2844476d
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,10 @@
+**/.git
+**/.svn
+**/.hg
+**/node_modules
+
+*.yml
+
+**/lnbits/static/vendor
+**/lnbits/static/bundle.*
+**/lnbits/static/css/*
diff --git a/Makefile b/Makefile
index 636f8c33..a38ad8f9 100644
--- a/Makefile
+++ b/Makefile
@@ -6,8 +6,8 @@ format: prettier isort black
check: mypy pyright pylint flake8 checkisort checkblack checkprettier
-prettier: $(shell find lnbits -name "*.js" -o -name ".html")
- ./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
+prettier:
+ poetry run ./node_modules/.bin/prettier --write lnbits
pyright:
poetry run ./node_modules/.bin/pyright
@@ -27,8 +27,8 @@ isort:
pylint:
poetry run pylint *.py lnbits/ tools/ tests/
-checkprettier: $(shell find lnbits -name "*.js" -o -name ".html")
- ./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
+checkprettier:
+ poetry run ./node_modules/.bin/prettier --check lnbits
checkblack:
poetry run black --check .
diff --git a/lnbits/core/helpers.py b/lnbits/core/helpers.py
index 214fee2f..6769d585 100644
--- a/lnbits/core/helpers.py
+++ b/lnbits/core/helpers.py
@@ -2,10 +2,12 @@ import importlib
import re
from typing import Any
+import httpx
from loguru import logger
from lnbits.db import Connection
from lnbits.extension_manager import Extension
+from lnbits.settings import settings
from . import db as core_db
from .crud import update_migration_version
@@ -42,3 +44,22 @@ async def run_migration(db: Connection, migrations_module: Any, current_version:
else:
async with core_db.connect() as conn:
await update_migration_version(conn, db_name, version)
+
+
+async def stop_extension_background_work(ext_id: str, user: str):
+ """
+ Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
+ Extensions SHOULD expose a DELETE enpoint at the root level of their API.
+ This function tries first to call the endpoint using `http` and if if fails it tries using `https`.
+ """
+ async with httpx.AsyncClient() as client:
+ try:
+ url = f"http://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
+ await client.delete(url)
+ except Exception as ex:
+ logger.warning(ex)
+ try:
+ # try https
+ url = f"https://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
+ except Exception as ex:
+ logger.warning(ex)
diff --git a/lnbits/core/templates/admin/_tab_funding.html b/lnbits/core/templates/admin/_tab_funding.html
index 35349e38..9fe3e831 100644
--- a/lnbits/core/templates/admin/_tab_funding.html
+++ b/lnbits/core/templates/admin/_tab_funding.html
@@ -38,7 +38,7 @@
filled
v-model="formData.lightning_invoice_expiry"
label="Invoice expiry (seconds)"
- mask="#######"
+ mask="#######"
>
diff --git a/lnbits/core/templates/admin/_tab_users.html b/lnbits/core/templates/admin/_tab_users.html
index 46483c18..3fbc5381 100644
--- a/lnbits/core/templates/admin/_tab_users.html
+++ b/lnbits/core/templates/admin/_tab_users.html
@@ -58,6 +58,5 @@
-
diff --git a/lnbits/core/templates/admin/index.html b/lnbits/core/templates/admin/index.html
index 239830e3..4f1bea04 100644
--- a/lnbits/core/templates/admin/index.html
+++ b/lnbits/core/templates/admin/index.html
@@ -395,14 +395,23 @@
addExtensionsManifest() {
const addManifest = this.formAddExtensionsManifest.trim()
const manifests = this.formData.lnbits_extensions_manifests
- if (addManifest && addManifest.length && !manifests.includes(addManifest)) {
- this.formData.lnbits_extensions_manifests = [...manifests, addManifest]
+ if (
+ addManifest &&
+ addManifest.length &&
+ !manifests.includes(addManifest)
+ ) {
+ this.formData.lnbits_extensions_manifests = [
+ ...manifests,
+ addManifest
+ ]
this.formAddExtensionsManifest = ''
}
},
removeExtensionsManifest(manifest) {
const manifests = this.formData.lnbits_extensions_manifests
- this.formData.lnbits_extensions_manifests = manifests.filter(m => m !== manifest)
+ this.formData.lnbits_extensions_manifests = manifests.filter(
+ m => m !== manifest
+ )
},
restartServer() {
LNbits.api
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index abfecb18..43ffffd9 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -29,7 +29,10 @@ from sse_starlette.sse import EventSourceResponse
from starlette.responses import RedirectResponse, StreamingResponse
from lnbits import bolt11, lnurl
-from lnbits.core.helpers import migrate_extension_database
+from lnbits.core.helpers import (
+ migrate_extension_database,
+ stop_extension_background_work,
+)
from lnbits.core.models import Payment, User, Wallet
from lnbits.decorators import (
WalletTypeInfo,
@@ -729,7 +732,6 @@ async def websocket_update_get(item_id: str, data: str):
async def api_install_extension(
data: CreateExtension, user: User = Depends(check_admin)
):
-
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive
)
@@ -752,6 +754,10 @@ async def api_install_extension(
await migrate_extension_database(extension, db_version)
await add_installed_extension(ext_info)
+
+ # call stop while the old routes are still active
+ await stop_extension_background_work(data.ext_id, user.id)
+
if data.ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [data.ext_id]
@@ -796,6 +802,9 @@ async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)
)
try:
+ # call stop while the old routes are still active
+ await stop_extension_background_work(ext_id, user.id)
+
if ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [ext_id]
diff --git a/lnbits/core/views/public_api.py b/lnbits/core/views/public_api.py
index b5773bbe..303929fe 100644
--- a/lnbits/core/views/public_api.py
+++ b/lnbits/core/views/public_api.py
@@ -1,11 +1,9 @@
import asyncio
import datetime
from http import HTTPStatus
-from urllib.parse import urlparse
from fastapi import HTTPException
from loguru import logger
-from starlette.requests import Request
from lnbits import bolt11
@@ -14,14 +12,6 @@ from ..crud import get_standalone_payment
from ..tasks import api_invoice_listeners
-@core_app.get("/.well-known/lnurlp/{username}")
-async def lnaddress(username: str, request: Request):
- from lnbits.extensions.lnaddress.lnurl import lnurl_response # type: ignore
-
- domain = urlparse(str(request.url)).netloc
- return await lnurl_response(username, domain, request)
-
-
@core_app.get("/public/v1/payment/{payment_hash}")
async def api_public_payment_longpolling(payment_hash):
payment = await get_standalone_payment(payment_hash)
diff --git a/lnbits/extension_manager.py b/lnbits/extension_manager.py
index 5cfae533..aad67636 100644
--- a/lnbits/extension_manager.py
+++ b/lnbits/extension_manager.py
@@ -51,13 +51,16 @@ class Extension(NamedTuple):
)
+# All subdirectories in the current directory, not recursive.
+
+
class ExtensionManager:
def __init__(self):
self._disabled: List[str] = settings.lnbits_disabled_extensions
self._admin_only: List[str] = settings.lnbits_admin_extensions
- self._extension_folders: List[str] = [
- x[1] for x in os.walk(os.path.join(settings.lnbits_path, "extensions"))
- ][0]
+ p = Path(settings.lnbits_path, "extensions")
+ os.makedirs(p, exist_ok=True)
+ self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()]
@property
def extensions(self) -> List[Extension]:
@@ -70,11 +73,7 @@ class ExtensionManager:
ext for ext in self._extension_folders if ext not in self._disabled
]:
try:
- with open(
- os.path.join(
- settings.lnbits_path, "extensions", extension, "config.json"
- )
- ) as json_file:
+ with open(extension / "config.json") as json_file:
config = json.load(json_file)
is_valid = True
is_admin_only = True if extension in self._admin_only else False
@@ -83,9 +82,10 @@ class ExtensionManager:
is_valid = False
is_admin_only = False
+ *_, extension_code = extension.parts
output.append(
Extension(
- extension,
+ extension_code,
is_valid,
is_admin_only,
config.get("name"),
diff --git a/tests/extensions/__init__.py b/lnbits/extensions/.gitkeep
similarity index 100%
rename from tests/extensions/__init__.py
rename to lnbits/extensions/.gitkeep
diff --git a/lnbits/extensions/bleskomat/README.md b/lnbits/extensions/bleskomat/README.md
deleted file mode 100644
index 97c70700..00000000
--- a/lnbits/extensions/bleskomat/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Bleskomat Extension for lnbits
-
-This extension allows you to connect a Bleskomat ATM to an lnbits wallet. It will work with both the [open-source DIY Bleskomat ATM project](https://github.com/samotari/bleskomat) as well as the [commercial Bleskomat ATM](https://www.bleskomat.com/).
-
-
-## Connect Your Bleskomat ATM
-
-* Click the "Add Bleskomat" button on this page to begin.
-* Choose a wallet. This will be the wallet that is used to pay satoshis to your ATM customers.
-* Choose the fiat currency. This should match the fiat currency that your ATM accepts.
-* Pick an exchange rate provider. This is the API that will be used to query the fiat to satoshi exchange rate at the time your customer attempts to withdraw their funds.
-* Set your ATM's fee percentage.
-* Click the "Done" button.
-* Find the new Bleskomat in the list and then click the export icon to download a new configuration file for your ATM.
-* Copy the configuration file ("bleskomat.conf") to your ATM's SD card.
-* Restart Your Bleskomat ATM. It should automatically reload the configurations from the SD card.
-
-
-## How Does It Work?
-
-Since the Bleskomat ATMs are designed to be offline, a cryptographic signing scheme is used to verify that the URL was generated by an authorized device. When one of your customers inserts fiat money into the device, a signed URL (lnurl-withdraw) is created and displayed as a QR code. Your customer scans the QR code with their lnurl-supporting mobile app, their mobile app communicates with the web API of lnbits to verify the signature, the fiat currency amount is converted to sats, the customer accepts the withdrawal, and finally lnbits will pay the customer from your lnbits wallet.
diff --git a/lnbits/extensions/bleskomat/__init__.py b/lnbits/extensions/bleskomat/__init__.py
deleted file mode 100644
index df54fc5f..00000000
--- a/lnbits/extensions/bleskomat/__init__.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from fastapi import APIRouter
-from starlette.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_bleskomat")
-
-bleskomat_static_files = [
- {
- "path": "/bleskomat/static",
- "app": StaticFiles(packages=[("lnbits", "extensions/bleskomat/static")]),
- "name": "bleskomat_static",
- }
-]
-
-bleskomat_ext: APIRouter = APIRouter(prefix="/bleskomat", tags=["Bleskomat"])
-
-
-def bleskomat_renderer():
- return template_renderer(["lnbits/extensions/bleskomat/templates"])
-
-
-from .lnurl_api import * # noqa: F401,F403
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/bleskomat/config.json b/lnbits/extensions/bleskomat/config.json
deleted file mode 100644
index f3cd7d8e..00000000
--- a/lnbits/extensions/bleskomat/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Bleskomat",
- "short_description": "Connect a Bleskomat ATM to an lnbits",
- "tile": "/bleskomat/static/image/bleskomat.png",
- "contributors": ["chill117"]
-}
diff --git a/lnbits/extensions/bleskomat/crud.py b/lnbits/extensions/bleskomat/crud.py
deleted file mode 100644
index 37af56cb..00000000
--- a/lnbits/extensions/bleskomat/crud.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import secrets
-import time
-from typing import List, Optional, Union
-from uuid import uuid4
-
-from . import db
-from .helpers import generate_bleskomat_lnurl_hash
-from .models import Bleskomat, BleskomatLnurl, CreateBleskomat
-
-
-async def create_bleskomat(data: CreateBleskomat, wallet_id: str) -> Bleskomat:
- bleskomat_id = uuid4().hex
- api_key_id = secrets.token_hex(8)
- api_key_secret = secrets.token_hex(32)
- api_key_encoding = "hex"
- await db.execute(
- """
- INSERT INTO bleskomat.bleskomats (id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- bleskomat_id,
- wallet_id,
- api_key_id,
- api_key_secret,
- api_key_encoding,
- data.name,
- data.fiat_currency,
- data.exchange_rate_provider,
- data.fee,
- ),
- )
- bleskomat = await get_bleskomat(bleskomat_id)
- assert bleskomat, "Newly created bleskomat couldn't be retrieved"
- return bleskomat
-
-
-async def get_bleskomat(bleskomat_id: str) -> Optional[Bleskomat]:
- row = await db.fetchone(
- "SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)
- )
- return Bleskomat(**row) if row else None
-
-
-async def get_bleskomat_by_api_key_id(api_key_id: str) -> Optional[Bleskomat]:
- row = await db.fetchone(
- "SELECT * FROM bleskomat.bleskomats WHERE api_key_id = ?", (api_key_id,)
- )
- return Bleskomat(**row) if row else None
-
-
-async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM bleskomat.bleskomats WHERE wallet IN ({q})", (*wallet_ids,)
- )
- return [Bleskomat(**row) for row in rows]
-
-
-async def update_bleskomat(bleskomat_id: str, **kwargs) -> Optional[Bleskomat]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE bleskomat.bleskomats SET {q} WHERE id = ?",
- (*kwargs.values(), bleskomat_id),
- )
- row = await db.fetchone(
- "SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)
- )
- return Bleskomat(**row) if row else None
-
-
-async def delete_bleskomat(bleskomat_id: str) -> None:
- await db.execute("DELETE FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,))
-
-
-async def create_bleskomat_lnurl(
- *, bleskomat: Bleskomat, secret: str, tag: str, params: str, uses: int = 1
-) -> BleskomatLnurl:
- bleskomat_lnurl_id = uuid4().hex
- hash = generate_bleskomat_lnurl_hash(secret)
- now = int(time.time())
- await db.execute(
- """
- INSERT INTO bleskomat.bleskomat_lnurls (id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- bleskomat_lnurl_id,
- bleskomat.id,
- bleskomat.wallet,
- hash,
- tag,
- params,
- bleskomat.api_key_id,
- uses,
- uses,
- now,
- now,
- ),
- )
- bleskomat_lnurl = await get_bleskomat_lnurl(secret)
- assert bleskomat_lnurl, "Newly created bleskomat LNURL couldn't be retrieved"
- return bleskomat_lnurl
-
-
-async def get_bleskomat_lnurl(secret: str) -> Optional[BleskomatLnurl]:
- hash = generate_bleskomat_lnurl_hash(secret)
- row = await db.fetchone(
- "SELECT * FROM bleskomat.bleskomat_lnurls WHERE hash = ?", (hash,)
- )
- return BleskomatLnurl(**row) if row else None
diff --git a/lnbits/extensions/bleskomat/exchange_rates.py b/lnbits/extensions/bleskomat/exchange_rates.py
deleted file mode 100644
index c316e7e3..00000000
--- a/lnbits/extensions/bleskomat/exchange_rates.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import json
-import os
-from typing import Callable, Union
-
-import httpx
-
-fiat_currencies = json.load(
- open(
- os.path.join(
- os.path.dirname(os.path.realpath(__file__)), "fiat_currencies.json"
- ),
- "r",
- )
-)
-
-exchange_rate_providers: dict[
- str, dict[str, Union[str, Callable[[dict, dict], str]]]
-] = {
- "bitfinex": {
- "name": "Bitfinex",
- "domain": "bitfinex.com",
- "api_url": "https://api.bitfinex.com/v1/pubticker/{from}{to}",
- "getter": lambda data, replacements: data["last_price"],
- },
- "bitstamp": {
- "name": "Bitstamp",
- "domain": "bitstamp.net",
- "api_url": "https://www.bitstamp.net/api/v2/ticker/{from}{to}/",
- "getter": lambda data, replacements: data["last"],
- },
- "coinbase": {
- "name": "Coinbase",
- "domain": "coinbase.com",
- "api_url": "https://api.coinbase.com/v2/exchange-rates?currency={FROM}",
- "getter": lambda data, replacements: data["data"]["rates"][replacements["TO"]],
- },
- "coinmate": {
- "name": "CoinMate",
- "domain": "coinmate.io",
- "api_url": "https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}",
- "getter": lambda data, replacements: data["data"]["last"],
- },
- "kraken": {
- "name": "Kraken",
- "domain": "kraken.com",
- "api_url": "https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
- "getter": lambda data, replacements: data["result"][
- "XXBTZ" + replacements["TO"]
- ]["c"][0],
- },
-}
-
-exchange_rate_providers_serializable = {}
-for ref, exchange_rate_provider in exchange_rate_providers.items():
- exchange_rate_provider_serializable = {}
- for key, value in exchange_rate_provider.items():
- if not callable(value):
- exchange_rate_provider_serializable[key] = value
- exchange_rate_providers_serializable[ref] = exchange_rate_provider_serializable
-
-
-async def fetch_fiat_exchange_rate(currency: str, provider: str):
-
- replacements = {
- "FROM": "BTC",
- "from": "btc",
- "TO": currency.upper(),
- "to": currency.lower(),
- }
-
- api_url_or_none = exchange_rate_providers[provider]["api_url"]
- if api_url_or_none is not None:
- api_url = str(api_url_or_none)
- for key in replacements.keys():
- api_url = api_url.replace("{" + key + "}", replacements[key])
- async with httpx.AsyncClient() as client:
- r = await client.get(api_url)
- r.raise_for_status()
- data = r.json()
- else:
- data = {}
- getter = exchange_rate_providers[provider]["getter"]
- if not callable(getter):
- return None
- return float(getter(data, replacements))
diff --git a/lnbits/extensions/bleskomat/fiat_currencies.json b/lnbits/extensions/bleskomat/fiat_currencies.json
deleted file mode 100644
index ff831f3e..00000000
--- a/lnbits/extensions/bleskomat/fiat_currencies.json
+++ /dev/null
@@ -1,166 +0,0 @@
-{
- "AED": "United Arab Emirates Dirham",
- "AFN": "Afghan Afghani",
- "ALL": "Albanian Lek",
- "AMD": "Armenian Dram",
- "ANG": "Netherlands Antillean Gulden",
- "AOA": "Angolan Kwanza",
- "ARS": "Argentine Peso",
- "AUD": "Australian Dollar",
- "AWG": "Aruban Florin",
- "AZN": "Azerbaijani Manat",
- "BAM": "Bosnia and Herzegovina Convertible Mark",
- "BBD": "Barbadian Dollar",
- "BDT": "Bangladeshi Taka",
- "BGN": "Bulgarian Lev",
- "BHD": "Bahraini Dinar",
- "BIF": "Burundian Franc",
- "BMD": "Bermudian Dollar",
- "BND": "Brunei Dollar",
- "BOB": "Bolivian Boliviano",
- "BRL": "Brazilian Real",
- "BSD": "Bahamian Dollar",
- "BTN": "Bhutanese Ngultrum",
- "BWP": "Botswana Pula",
- "BYN": "Belarusian Ruble",
- "BYR": "Belarusian Ruble",
- "BZD": "Belize Dollar",
- "CAD": "Canadian Dollar",
- "CDF": "Congolese Franc",
- "CHF": "Swiss Franc",
- "CLF": "Unidad de Fomento",
- "CLP": "Chilean Peso",
- "CNH": "Chinese Renminbi Yuan Offshore",
- "CNY": "Chinese Renminbi Yuan",
- "COP": "Colombian Peso",
- "CRC": "Costa Rican Colón",
- "CUC": "Cuban Convertible Peso",
- "CVE": "Cape Verdean Escudo",
- "CZK": "Czech Koruna",
- "DJF": "Djiboutian Franc",
- "DKK": "Danish Krone",
- "DOP": "Dominican Peso",
- "DZD": "Algerian Dinar",
- "EGP": "Egyptian Pound",
- "ERN": "Eritrean Nakfa",
- "ETB": "Ethiopian Birr",
- "EUR": "Euro",
- "FJD": "Fijian Dollar",
- "FKP": "Falkland Pound",
- "GBP": "British Pound",
- "GEL": "Georgian Lari",
- "GGP": "Guernsey Pound",
- "GHS": "Ghanaian Cedi",
- "GIP": "Gibraltar Pound",
- "GMD": "Gambian Dalasi",
- "GNF": "Guinean Franc",
- "GTQ": "Guatemalan Quetzal",
- "GYD": "Guyanese Dollar",
- "HKD": "Hong Kong Dollar",
- "HNL": "Honduran Lempira",
- "HRK": "Croatian Kuna",
- "HTG": "Haitian Gourde",
- "HUF": "Hungarian Forint",
- "IDR": "Indonesian Rupiah",
- "ILS": "Israeli New Sheqel",
- "IMP": "Isle of Man Pound",
- "INR": "Indian Rupee",
- "IQD": "Iraqi Dinar",
- "ISK": "Icelandic Króna",
- "JEP": "Jersey Pound",
- "JMD": "Jamaican Dollar",
- "JOD": "Jordanian Dinar",
- "JPY": "Japanese Yen",
- "KES": "Kenyan Shilling",
- "KGS": "Kyrgyzstani Som",
- "KHR": "Cambodian Riel",
- "KMF": "Comorian Franc",
- "KRW": "South Korean Won",
- "KWD": "Kuwaiti Dinar",
- "KYD": "Cayman Islands Dollar",
- "KZT": "Kazakhstani Tenge",
- "LAK": "Lao Kip",
- "LBP": "Lebanese Pound",
- "LKR": "Sri Lankan Rupee",
- "LRD": "Liberian Dollar",
- "LSL": "Lesotho Loti",
- "LYD": "Libyan Dinar",
- "MAD": "Moroccan Dirham",
- "MDL": "Moldovan Leu",
- "MGA": "Malagasy Ariary",
- "MKD": "Macedonian Denar",
- "MMK": "Myanmar Kyat",
- "MNT": "Mongolian Tögrög",
- "MOP": "Macanese Pataca",
- "MRO": "Mauritanian Ouguiya",
- "MUR": "Mauritian Rupee",
- "MVR": "Maldivian Rufiyaa",
- "MWK": "Malawian Kwacha",
- "MXN": "Mexican Peso",
- "MYR": "Malaysian Ringgit",
- "MZN": "Mozambican Metical",
- "NAD": "Namibian Dollar",
- "NGN": "Nigerian Naira",
- "NIO": "Nicaraguan Córdoba",
- "NOK": "Norwegian Krone",
- "NPR": "Nepalese Rupee",
- "NZD": "New Zealand Dollar",
- "OMR": "Omani Rial",
- "PAB": "Panamanian Balboa",
- "PEN": "Peruvian Sol",
- "PGK": "Papua New Guinean Kina",
- "PHP": "Philippine Peso",
- "PKR": "Pakistani Rupee",
- "PLN": "Polish Złoty",
- "PYG": "Paraguayan Guaraní",
- "QAR": "Qatari Riyal",
- "RON": "Romanian Leu",
- "RSD": "Serbian Dinar",
- "RUB": "Russian Ruble",
- "RWF": "Rwandan Franc",
- "SAR": "Saudi Riyal",
- "SBD": "Solomon Islands Dollar",
- "SCR": "Seychellois Rupee",
- "SEK": "Swedish Krona",
- "SGD": "Singapore Dollar",
- "SHP": "Saint Helenian Pound",
- "SLL": "Sierra Leonean Leone",
- "SOS": "Somali Shilling",
- "SRD": "Surinamese Dollar",
- "SSP": "South Sudanese Pound",
- "STD": "São Tomé and Príncipe Dobra",
- "SVC": "Salvadoran Colón",
- "SZL": "Swazi Lilangeni",
- "THB": "Thai Baht",
- "TJS": "Tajikistani Somoni",
- "TMT": "Turkmenistani Manat",
- "TND": "Tunisian Dinar",
- "TOP": "Tongan Paʻanga",
- "TRY": "Turkish Lira",
- "TTD": "Trinidad and Tobago Dollar",
- "TWD": "New Taiwan Dollar",
- "TZS": "Tanzanian Shilling",
- "UAH": "Ukrainian Hryvnia",
- "UGX": "Ugandan Shilling",
- "USD": "US Dollar",
- "UYU": "Uruguayan Peso",
- "UZS": "Uzbekistan Som",
- "VEF": "Venezuelan Bolívar",
- "VES": "Venezuelan Bolívar Soberano",
- "VND": "Vietnamese Đồng",
- "VUV": "Vanuatu Vatu",
- "WST": "Samoan Tala",
- "XAF": "Central African Cfa Franc",
- "XAG": "Silver (Troy Ounce)",
- "XAU": "Gold (Troy Ounce)",
- "XCD": "East Caribbean Dollar",
- "XDR": "Special Drawing Rights",
- "XOF": "West African Cfa Franc",
- "XPD": "Palladium",
- "XPF": "Cfp Franc",
- "XPT": "Platinum",
- "YER": "Yemeni Rial",
- "ZAR": "South African Rand",
- "ZMW": "Zambian Kwacha",
- "ZWL": "Zimbabwean Dollar"
-}
\ No newline at end of file
diff --git a/lnbits/extensions/bleskomat/helpers.py b/lnbits/extensions/bleskomat/helpers.py
deleted file mode 100644
index f61a5640..00000000
--- a/lnbits/extensions/bleskomat/helpers.py
+++ /dev/null
@@ -1,153 +0,0 @@
-import base64
-import hashlib
-import hmac
-from http import HTTPStatus
-from typing import Dict
-from urllib import parse
-
-from fastapi import Request
-
-
-def generate_bleskomat_lnurl_hash(secret: str):
- m = hashlib.sha256()
- m.update(f"{secret}".encode())
- return m.hexdigest()
-
-
-def generate_bleskomat_lnurl_signature(
- payload: str, api_key_secret: str, api_key_encoding: str = "hex"
-):
- if api_key_encoding == "hex":
- key = bytes.fromhex(api_key_secret)
- elif api_key_encoding == "base64":
- key = base64.b64decode(api_key_secret)
- else:
- key = bytes.fromhex(api_key_secret)
- return hmac.new(key=key, msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
-
-
-def generate_bleskomat_lnurl_secret(api_key_id: str, signature: str):
- # The secret is not randomly generated by the server.
- # Instead it is the hash of the API key ID and signature concatenated together.
- m = hashlib.sha256()
- m.update(f"{api_key_id}-{signature}".encode())
- return m.hexdigest()
-
-
-def get_callback_url(req: Request):
- return req.url_for("bleskomat.api_bleskomat_lnurl")
-
-
-def is_supported_lnurl_subprotocol(tag: str) -> bool:
- return tag == "withdrawRequest"
-
-
-class LnurlHttpError(Exception):
- def __init__(
- self,
- message: str = "",
- http_status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
- ):
- self.message = message
- self.http_status = http_status
- super().__init__(self.message)
-
-
-class LnurlValidationError(Exception):
- pass
-
-
-def prepare_lnurl_params(tag: str, query: dict) -> dict:
- params: dict = {}
- if not is_supported_lnurl_subprotocol(tag):
- raise LnurlValidationError(f'Unsupported subprotocol: "{tag}"')
- if tag == "withdrawRequest":
- params["minWithdrawable"] = float(query["minWithdrawable"])
- params["maxWithdrawable"] = float(query["maxWithdrawable"])
- params["defaultDescription"] = query["defaultDescription"]
- if not params["minWithdrawable"] > 0:
- raise LnurlValidationError('"minWithdrawable" must be greater than zero')
- if not params["maxWithdrawable"] >= params["minWithdrawable"]:
- raise LnurlValidationError(
- '"maxWithdrawable" must be greater than or equal to "minWithdrawable"'
- )
- return params
-
-
-encode_uri_component_safe_chars = (
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()"
-)
-
-
-def query_to_signing_payload(query: Dict[str, str]) -> str:
- # Sort the query by key, then stringify it to create the payload.
- sorted_keys = sorted(query.keys(), key=str.lower)
- payload = []
- for key in sorted_keys:
- if not key == "signature":
- encoded_key = parse.quote(key, safe=encode_uri_component_safe_chars)
- encoded_value = parse.quote(
- query[key], safe=encode_uri_component_safe_chars
- )
- payload.append(f"{encoded_key}={encoded_value}")
- return "&".join(payload)
-
-
-unshorten_rules: dict[str, dict] = {
- "query": {"n": "nonce", "s": "signature", "t": "tag"},
- "tags": {
- "c": "channelRequest",
- "l": "login",
- "p": "payRequest",
- "w": "withdrawRequest",
- },
- "params": {
- "channelRequest": {"pl": "localAmt", "pp": "pushAmt"},
- "login": {},
- "payRequest": {"pn": "minSendable", "px": "maxSendable", "pm": "metadata"},
- "withdrawRequest": {
- "pn": "minWithdrawable",
- "px": "maxWithdrawable",
- "pd": "defaultDescription",
- },
- },
-}
-
-
-def unshorten_lnurl_query(query: dict) -> Dict[str, str]:
- new_query = {}
- rules = unshorten_rules
- if "tag" in query:
- tag = query["tag"]
- elif "t" in query:
- tag = query["t"]
- else:
- raise LnurlValidationError('Missing required query parameter: "tag"')
- # Unshorten tag:
- if tag in rules["tags"]:
- long_tag = rules["tags"][tag]
- new_query["tag"] = long_tag
- tag = long_tag
- if tag not in rules["params"]:
- raise LnurlValidationError(f'Unknown tag: "{tag}"')
- for key in query:
- if key in rules["params"][str(tag)]:
- short_param_key = key
- long_param_key = rules["params"][str(tag)][short_param_key]
- if short_param_key in query:
- new_query[long_param_key] = query[short_param_key]
- else:
- new_query[long_param_key] = query[long_param_key]
- elif key in rules["query"]:
- # Unshorten general keys:
- short_key = key
- long_key = rules["query"][short_key]
- if long_key not in new_query:
- if short_key in query:
- new_query[long_key] = query[short_key]
- else:
- new_query[long_key] = query[str(long_key)]
- else:
- # Keep unknown key/value pairs unchanged:
- new_query[key] = query[key]
- return new_query
diff --git a/lnbits/extensions/bleskomat/lnurl_api.py b/lnbits/extensions/bleskomat/lnurl_api.py
deleted file mode 100644
index c892cc5a..00000000
--- a/lnbits/extensions/bleskomat/lnurl_api.py
+++ /dev/null
@@ -1,132 +0,0 @@
-import json
-import math
-from http import HTTPStatus
-
-from loguru import logger
-from starlette.requests import Request
-
-from . import bleskomat_ext
-from .crud import (
- create_bleskomat_lnurl,
- get_bleskomat_by_api_key_id,
- get_bleskomat_lnurl,
-)
-from .exchange_rates import fetch_fiat_exchange_rate
-from .helpers import (
- LnurlHttpError,
- LnurlValidationError,
- generate_bleskomat_lnurl_secret,
- generate_bleskomat_lnurl_signature,
- prepare_lnurl_params,
- query_to_signing_payload,
- unshorten_lnurl_query,
-)
-
-
-# Handles signed URL from Bleskomat ATMs and "action" callback of auto-generated LNURLs.
-@bleskomat_ext.get("/u", name="bleskomat.api_bleskomat_lnurl")
-async def api_bleskomat_lnurl(req: Request):
- try:
- query = dict(req.query_params)
-
- # Unshorten query if "s" is used instead of "signature".
- if "s" in query:
- query = unshorten_lnurl_query(query)
-
- if "signature" in query:
-
- # Signature provided.
- # Use signature to verify that the URL was generated by an authorized device.
- # Later validate parameters, auto-generate LNURL, reply with LNURL response object.
- signature = query["signature"]
-
- # The API key ID, nonce, and tag should be present in the query string.
- for field in ["id", "nonce", "tag"]:
- if field not in query:
- raise LnurlHttpError(
- f'Failed API key signature check: Missing "{field}"',
- HTTPStatus.BAD_REQUEST,
- )
-
- # URL signing scheme is described here:
- # https://github.com/chill117/lnurl-node#how-to-implement-url-signing-scheme
- payload = query_to_signing_payload(query)
- api_key_id = query["id"]
- bleskomat = await get_bleskomat_by_api_key_id(api_key_id)
- if not bleskomat:
- raise LnurlHttpError("Unknown API key", HTTPStatus.BAD_REQUEST)
- api_key_secret = bleskomat.api_key_secret
- api_key_encoding = bleskomat.api_key_encoding
- expected_signature = generate_bleskomat_lnurl_signature(
- payload, api_key_secret, api_key_encoding
- )
- if signature != expected_signature:
- raise LnurlHttpError("Invalid API key signature", HTTPStatus.FORBIDDEN)
-
- # Signature is valid.
- # In the case of signed URLs, the secret is deterministic based on the API key ID and signature.
- secret = generate_bleskomat_lnurl_secret(api_key_id, signature)
- lnurl = await get_bleskomat_lnurl(secret)
- if not lnurl:
- try:
- tag = query["tag"]
- params = prepare_lnurl_params(tag, query)
- if "f" in query:
- rate = await fetch_fiat_exchange_rate(
- currency=query["f"],
- provider=bleskomat.exchange_rate_provider,
- )
- # Convert fee (%) to decimal:
- fee = float(bleskomat.fee) / 100
- if tag == "withdrawRequest":
- for key in ["minWithdrawable", "maxWithdrawable"]:
- amount_sats = int(
- math.floor((params[key] / rate) * 1e8)
- )
- fee_sats = int(math.floor(amount_sats * fee))
- amount_sats_less_fee = amount_sats - fee_sats
- # Convert to msats:
- params[key] = int(amount_sats_less_fee * 1e3)
- except LnurlValidationError as e:
- raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST)
- # Create a new LNURL using the query parameters provided in the signed URL.
- json_params = json.JSONEncoder().encode(params)
- lnurl = await create_bleskomat_lnurl(
- bleskomat=bleskomat,
- secret=secret,
- tag=tag,
- params=json_params,
- uses=1,
- )
-
- # Reply with LNURL response object.
- return lnurl.get_info_response_object(secret, req)
-
- # No signature provided.
- # Treat as "action" callback.
-
- if "k1" not in query:
- raise LnurlHttpError("Missing secret", HTTPStatus.BAD_REQUEST)
-
- secret = query["k1"]
- lnurl = await get_bleskomat_lnurl(secret)
- if not lnurl:
- raise LnurlHttpError("Invalid secret", HTTPStatus.BAD_REQUEST)
-
- if not lnurl.has_uses_remaining():
- raise LnurlHttpError(
- "Maximum number of uses already reached", HTTPStatus.BAD_REQUEST
- )
-
- try:
- await lnurl.execute_action(query)
- except LnurlValidationError as e:
- raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST)
-
- except LnurlHttpError as e:
- return {"status": "ERROR", "reason": str(e)}
- except Exception as e:
- logger.error(str(e))
- return {"status": "ERROR", "reason": "Unexpected error"}
-
- return {"status": "OK"}
diff --git a/lnbits/extensions/bleskomat/migrations.py b/lnbits/extensions/bleskomat/migrations.py
deleted file mode 100644
index 84e886e5..00000000
--- a/lnbits/extensions/bleskomat/migrations.py
+++ /dev/null
@@ -1,37 +0,0 @@
-async def m001_initial(db):
-
- await db.execute(
- """
- CREATE TABLE bleskomat.bleskomats (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- api_key_id TEXT NOT NULL,
- api_key_secret TEXT NOT NULL,
- api_key_encoding TEXT NOT NULL,
- name TEXT NOT NULL,
- fiat_currency TEXT NOT NULL,
- exchange_rate_provider TEXT NOT NULL,
- fee TEXT NOT NULL,
- UNIQUE(api_key_id)
- );
- """
- )
-
- await db.execute(
- """
- CREATE TABLE bleskomat.bleskomat_lnurls (
- id TEXT PRIMARY KEY,
- bleskomat TEXT NOT NULL,
- wallet TEXT NOT NULL,
- hash TEXT NOT NULL,
- tag TEXT NOT NULL,
- params TEXT NOT NULL,
- api_key_id TEXT NOT NULL,
- initial_uses INTEGER DEFAULT 1,
- remaining_uses INTEGER DEFAULT 0,
- created_time INTEGER,
- updated_time INTEGER,
- UNIQUE(hash)
- );
- """
- )
diff --git a/lnbits/extensions/bleskomat/models.py b/lnbits/extensions/bleskomat/models.py
deleted file mode 100644
index 5aec7969..00000000
--- a/lnbits/extensions/bleskomat/models.py
+++ /dev/null
@@ -1,142 +0,0 @@
-import json
-import time
-from typing import Dict
-
-from fastapi import Query, Request
-from loguru import logger
-from pydantic import BaseModel, validator
-
-from lnbits import bolt11
-from lnbits.core.services import PaymentFailure, pay_invoice
-
-from . import db
-from .exchange_rates import exchange_rate_providers, fiat_currencies
-from .helpers import LnurlValidationError, get_callback_url
-
-
-class CreateBleskomat(BaseModel):
- name: str = Query(...)
- fiat_currency: str = Query(...)
- exchange_rate_provider: str = Query(...)
- fee: str = Query(...)
-
- @validator("fiat_currency")
- def allowed_fiat_currencies(cls, v):
- if v not in fiat_currencies.keys():
- raise ValueError("Not allowed currency")
- return v
-
- @validator("exchange_rate_provider")
- def allowed_providers(cls, v):
- if v not in exchange_rate_providers.keys():
- raise ValueError("Not allowed provider")
- return v
-
- @validator("fee")
- def fee_type(cls, v):
- if not isinstance(v, (str, float, int)):
- raise ValueError("Fee type not allowed")
- return v
-
-
-class Bleskomat(BaseModel):
- id: str
- wallet: str
- api_key_id: str
- api_key_secret: str
- api_key_encoding: str
- name: str
- fiat_currency: str
- exchange_rate_provider: str
- fee: str
-
-
-class BleskomatLnurl(BaseModel):
- id: str
- bleskomat: str
- wallet: str
- hash: str
- tag: str
- params: str
- api_key_id: str
- initial_uses: int
- remaining_uses: int
- created_time: int
- updated_time: int
-
- def has_uses_remaining(self) -> bool:
- # When initial uses is 0 then the LNURL has unlimited uses.
- return self.initial_uses == 0 or self.remaining_uses > 0
-
- def get_info_response_object(self, secret: str, req: Request) -> Dict[str, str]:
- tag = self.tag
- params = json.loads(self.params)
- response = {"tag": tag}
- if tag == "withdrawRequest":
- for key in ["minWithdrawable", "maxWithdrawable", "defaultDescription"]:
- response[key] = params[key]
- response["callback"] = get_callback_url(req)
- response["k1"] = secret
- return response
-
- def validate_action(self, query) -> None:
- tag = self.tag
- params = json.loads(self.params)
- # Perform tag-specific checks.
- if tag == "withdrawRequest":
- for field in ["pr"]:
- if field not in query:
- raise LnurlValidationError(f'Missing required parameter: "{field}"')
- # Check the bolt11 invoice(s) provided.
- pr = query["pr"]
- if "," in pr:
- raise LnurlValidationError("Multiple payment requests not supported")
- try:
- invoice = bolt11.decode(pr)
- except ValueError:
- raise LnurlValidationError(
- 'Invalid parameter ("pr"): Lightning payment request expected'
- )
- if invoice.amount_msat < params["minWithdrawable"]:
- raise LnurlValidationError(
- 'Amount in invoice must be greater than or equal to "minWithdrawable"'
- )
- if invoice.amount_msat > params["maxWithdrawable"]:
- raise LnurlValidationError(
- 'Amount in invoice must be less than or equal to "maxWithdrawable"'
- )
- else:
- raise LnurlValidationError(f'Unknown subprotocol: "{tag}"')
-
- async def execute_action(self, query):
- self.validate_action(query)
- used = False
- async with db.connect() as conn:
- if self.initial_uses > 0:
- used = await self.use(conn)
- if not used:
- raise LnurlValidationError("Maximum number of uses already reached")
- tag = self.tag
- if tag == "withdrawRequest":
- try:
- await pay_invoice(
- wallet_id=self.wallet, payment_request=query["pr"]
- )
- except (ValueError, PermissionError, PaymentFailure) as e:
- raise LnurlValidationError("Failed to pay invoice: " + str(e))
- except Exception as e:
- logger.error(str(e))
- raise LnurlValidationError("Unexpected error")
-
- async def use(self, conn) -> bool:
- now = int(time.time())
- result = await conn.execute(
- """
- UPDATE bleskomat.bleskomat_lnurls
- SET remaining_uses = remaining_uses - 1, updated_time = ?
- WHERE id = ?
- AND remaining_uses > 0
- """,
- (now, self.id),
- )
- return result.rowcount > 0
diff --git a/lnbits/extensions/bleskomat/static/image/bleskomat.png b/lnbits/extensions/bleskomat/static/image/bleskomat.png
deleted file mode 100644
index cc728083..00000000
Binary files a/lnbits/extensions/bleskomat/static/image/bleskomat.png and /dev/null differ
diff --git a/lnbits/extensions/bleskomat/static/js/index.js b/lnbits/extensions/bleskomat/static/js/index.js
deleted file mode 100644
index f20a4659..00000000
--- a/lnbits/extensions/bleskomat/static/js/index.js
+++ /dev/null
@@ -1,216 +0,0 @@
-/* global Vue, VueQrcode, _, Quasar, LOCALE, windowMixin, LNbits */
-
-Vue.component(VueQrcode.name, VueQrcode)
-
-var mapBleskomat = function (obj) {
- obj._data = _.clone(obj)
- return obj
-}
-
-var defaultValues = {
- name: 'My Bleskomat',
- fiat_currency: 'EUR',
- exchange_rate_provider: 'coinbase',
- fee: '0.00'
-}
-
-new Vue({
- el: '#vue',
- mixins: [windowMixin],
- data: function () {
- return {
- checker: null,
- bleskomats: [],
- bleskomatsTable: {
- columns: [
- {
- name: 'api_key_id',
- align: 'left',
- label: 'API Key ID',
- field: 'api_key_id'
- },
- {
- name: 'name',
- align: 'left',
- label: 'Name',
- field: 'name'
- },
- {
- name: 'fiat_currency',
- align: 'left',
- label: 'Fiat Currency',
- field: 'fiat_currency'
- },
- {
- name: 'exchange_rate_provider',
- align: 'left',
- label: 'Exchange Rate Provider',
- field: 'exchange_rate_provider'
- },
- {
- name: 'fee',
- align: 'left',
- label: 'Fee (%)',
- field: 'fee'
- }
- ],
- pagination: {
- rowsPerPage: 10
- }
- },
- formDialog: {
- show: false,
- fiatCurrencies: _.keys(window.bleskomat_vars.fiat_currencies),
- exchangeRateProviders: _.keys(
- window.bleskomat_vars.exchange_rate_providers
- ),
- data: _.clone(defaultValues)
- }
- }
- },
- computed: {
- sortedBleskomats: function () {
- return this.bleskomats.sort(function (a, b) {
- // Sort by API Key ID alphabetically.
- var apiKeyId_A = a.api_key_id.toLowerCase()
- var apiKeyId_B = b.api_key_id.toLowerCase()
- return apiKeyId_A < apiKeyId_B ? -1 : apiKeyId_A > apiKeyId_B ? 1 : 0
- })
- }
- },
- methods: {
- getBleskomats: function () {
- var self = this
- LNbits.api
- .request(
- 'GET',
- '/bleskomat/api/v1/bleskomats?all_wallets=true',
- this.g.user.wallets[0].adminkey
- )
- .then(function (response) {
- self.bleskomats = response.data.map(function (obj) {
- return mapBleskomat(obj)
- })
- })
- .catch(function (error) {
- clearInterval(self.checker)
- LNbits.utils.notifyApiError(error)
- })
- },
- closeFormDialog: function () {
- this.formDialog.data = _.clone(defaultValues)
- },
- exportConfigFile: function (bleskomatId) {
- var bleskomat = _.findWhere(this.bleskomats, {id: bleskomatId})
- var fieldToKey = {
- api_key_id: 'apiKey.id',
- api_key_secret: 'apiKey.key',
- api_key_encoding: 'apiKey.encoding',
- fiat_currency: 'fiatCurrency'
- }
- var lines = _.chain(bleskomat)
- .map(function (value, field) {
- var key = fieldToKey[field] || null
- return key ? [key, value].join('=') : null
- })
- .compact()
- .value()
- lines.push('callbackUrl=' + window.bleskomat_vars.callback_url)
- lines.push('shorten=true')
- var content = lines.join('\n')
- var status = Quasar.utils.exportFile(
- 'bleskomat.conf',
- content,
- 'text/plain'
- )
- if (status !== true) {
- Quasar.plugins.Notify.create({
- message: 'Browser denied file download...',
- color: 'negative',
- icon: null
- })
- }
- },
- openUpdateDialog: function (bleskomatId) {
- var bleskomat = _.findWhere(this.bleskomats, {id: bleskomatId})
- this.formDialog.data = _.clone(bleskomat._data)
- this.formDialog.show = true
- },
- sendFormData: function () {
- var wallet = _.findWhere(this.g.user.wallets, {
- id: this.formDialog.data.wallet
- })
- var data = _.omit(this.formDialog.data, 'wallet')
- if (data.id) {
- this.updateBleskomat(wallet, data)
- } else {
- this.createBleskomat(wallet, data)
- }
- },
- updateBleskomat: function (wallet, data) {
- var self = this
- LNbits.api
- .request(
- 'PUT',
- '/bleskomat/api/v1/bleskomat/' + data.id,
- wallet.adminkey,
- _.pick(data, 'name', 'fiat_currency', 'exchange_rate_provider', 'fee')
- )
- .then(function (response) {
- self.bleskomats = _.reject(self.bleskomats, function (obj) {
- return obj.id === data.id
- })
- self.bleskomats.push(mapBleskomat(response.data))
- self.formDialog.show = false
- })
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
- })
- },
- createBleskomat: function (wallet, data) {
- var self = this
- LNbits.api
- .request('POST', '/bleskomat/api/v1/bleskomat', wallet.adminkey, data)
- .then(function (response) {
- self.bleskomats.push(mapBleskomat(response.data))
- self.formDialog.show = false
- })
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
- })
- },
- deleteBleskomat: function (bleskomatId) {
- var self = this
- var bleskomat = _.findWhere(this.bleskomats, {id: bleskomatId})
- LNbits.utils
- .confirmDialog(
- 'Are you sure you want to delete "' + bleskomat.name + '"?'
- )
- .onOk(function () {
- LNbits.api
- .request(
- 'DELETE',
- '/bleskomat/api/v1/bleskomat/' + bleskomatId,
- _.findWhere(self.g.user.wallets, {id: bleskomat.wallet}).adminkey
- )
- .then(function (response) {
- self.bleskomats = _.reject(self.bleskomats, function (obj) {
- return obj.id === bleskomatId
- })
- })
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
- })
- })
- }
- },
- created: function () {
- if (this.g.user.wallets.length) {
- var getBleskomats = this.getBleskomats
- getBleskomats()
- this.checker = setInterval(function () {
- getBleskomats()
- }, 20000)
- }
- }
-})
diff --git a/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html b/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html
deleted file mode 100644
index 5bbd826e..00000000
--- a/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
- This extension allows you to connect a Bleskomat ATM to an lnbits
- wallet. It will work with both the
- open-source DIY Bleskomat ATM project
- as well as the
- commercial Bleskomat ATM .
-
- Connect Your Bleskomat ATM
-
-
- Click the "Add Bleskomat" button on this page to begin.
-
- Choose a wallet. This will be the wallet that is used to pay
- satoshis to your ATM customers.
-
-
- Choose the fiat currency. This should match the fiat currency that
- your ATM accepts.
-
-
- Pick an exchange rate provider. This is the API that will be used to
- query the fiat to satoshi exchange rate at the time your customer
- attempts to withdraw their funds.
-
- Set your ATM's fee percentage.
- Click the "Done" button.
-
- Find the new Bleskomat in the list and then click the export icon to
- download a new configuration file for your ATM.
-
-
- Copy the configuration file ("bleskomat.conf") to your ATM's SD
- card.
-
-
- Restart Your Bleskomat ATM. It should automatically reload the
- configurations from the SD card.
-
-
-
- How does it work?
-
- Since the Bleskomat ATMs are designed to be offline, a cryptographic
- signing scheme is used to verify that the URL was generated by an
- authorized device. When one of your customers inserts fiat money into
- the device, a signed URL (lnurl-withdraw) is created and displayed as a
- QR code. Your customer scans the QR code with their lnurl-supporting
- mobile app, their mobile app communicates with the web API of lnbits to
- verify the signature, the fiat currency amount is converted to sats, the
- customer accepts the withdrawal, and finally lnbits will pay the
- customer from your lnbits wallet.
-
-
-
-
-
diff --git a/lnbits/extensions/bleskomat/templates/bleskomat/index.html b/lnbits/extensions/bleskomat/templates/bleskomat/index.html
deleted file mode 100644
index 0cc51237..00000000
--- a/lnbits/extensions/bleskomat/templates/bleskomat/index.html
+++ /dev/null
@@ -1,180 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block scripts %} {{ window_vars(user) }}
-
-
-{% endblock %} {% block page %}
-
-
-
-
- Add Bleskomat
-
-
-
-
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
- Export Configuration
-
-
-
- {{ col.value }}
-
-
-
- Edit
-
-
- Delete
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} Bleskomat extension
-
-
-
-
- {% include "bleskomat/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update Bleskomat
- Add Bleskomat
- Cancel
-
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/bleskomat/views.py b/lnbits/extensions/bleskomat/views.py
deleted file mode 100644
index 370f2ec3..00000000
--- a/lnbits/extensions/bleskomat/views.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from fastapi import Depends, 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 bleskomat_ext, bleskomat_renderer
-from .exchange_rates import exchange_rate_providers_serializable, fiat_currencies
-from .helpers import get_callback_url
-
-templates = Jinja2Templates(directory="templates")
-
-
-@bleskomat_ext.get("/", response_class=HTMLResponse)
-async def index(req: Request, user: User = Depends(check_user_exists)):
- bleskomat_vars = {
- "callback_url": get_callback_url(req),
- "exchange_rate_providers": exchange_rate_providers_serializable,
- "fiat_currencies": fiat_currencies,
- }
- return bleskomat_renderer().TemplateResponse(
- "bleskomat/index.html",
- {"request": req, "user": user.dict(), "bleskomat_vars": bleskomat_vars},
- )
diff --git a/lnbits/extensions/bleskomat/views_api.py b/lnbits/extensions/bleskomat/views_api.py
deleted file mode 100644
index 3e7573bc..00000000
--- a/lnbits/extensions/bleskomat/views_api.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Query
-from loguru import logger
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_user
-from lnbits.decorators import WalletTypeInfo, require_admin_key
-
-from . import bleskomat_ext
-from .crud import (
- create_bleskomat,
- delete_bleskomat,
- get_bleskomat,
- get_bleskomats,
- update_bleskomat,
-)
-from .exchange_rates import fetch_fiat_exchange_rate
-from .models import CreateBleskomat
-
-
-@bleskomat_ext.get("/api/v1/bleskomats")
-async def api_bleskomats(
- wallet: WalletTypeInfo = Depends(require_admin_key),
- all_wallets: bool = Query(False),
-):
- wallet_ids = [wallet.wallet.id]
-
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- return [bleskomat.dict() for bleskomat in await get_bleskomats(wallet_ids)]
-
-
-@bleskomat_ext.get("/api/v1/bleskomat/{bleskomat_id}")
-async def api_bleskomat_retrieve(
- bleskomat_id, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- bleskomat = await get_bleskomat(bleskomat_id)
-
- if not bleskomat or bleskomat.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="Bleskomat configuration not found.",
- )
-
- return bleskomat.dict()
-
-
-@bleskomat_ext.post("/api/v1/bleskomat")
-@bleskomat_ext.put("/api/v1/bleskomat/{bleskomat_id}")
-async def api_bleskomat_create_or_update(
- data: CreateBleskomat,
- wallet: WalletTypeInfo = Depends(require_admin_key),
- bleskomat_id=None,
-):
- fiat_currency = data.fiat_currency
- exchange_rate_provider = data.exchange_rate_provider
- try:
- await fetch_fiat_exchange_rate(
- currency=fiat_currency, provider=exchange_rate_provider
- )
- except Exception as e:
- logger.error(e)
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail=f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"',
- )
-
- if bleskomat_id:
- bleskomat = await get_bleskomat(bleskomat_id)
- if not bleskomat or bleskomat.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="Bleskomat configuration not found.",
- )
-
- bleskomat = await update_bleskomat(bleskomat_id, **data.dict())
- else:
- bleskomat = await create_bleskomat(wallet_id=wallet.wallet.id, data=data)
-
- assert bleskomat
- return bleskomat.dict()
-
-
-@bleskomat_ext.delete("/api/v1/bleskomat/{bleskomat_id}")
-async def api_bleskomat_delete(
- bleskomat_id, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- bleskomat = await get_bleskomat(bleskomat_id)
-
- if not bleskomat or bleskomat.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="Bleskomat configuration not found.",
- )
-
- await delete_bleskomat(bleskomat_id)
- return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/boltz/README.md b/lnbits/extensions/boltz/README.md
deleted file mode 100644
index 9ca38d49..00000000
--- a/lnbits/extensions/boltz/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Swap on [Boltz](https://boltz.exchange)
-providing **trustless** and **account-free** swap services since **2018.**
-move **IN** and **OUT** of the **lightning network** and remain in control of your bitcoin, at all times.
-* [Lightning Node](https://amboss.space/node/026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2)
-* [Documentation](https://docs.boltz.exchange/en/latest/)
-* [Discord](https://discord.gg/d6EK85KK)
-* [Twitter](https://twitter.com/Boltzhq)
-* [FAQ](https://www.notion.so/Frequently-Asked-Questions-585328ae43944e2eba351050790d5eec) very cool!
-
-# usage
-This extension lets you create swaps, reverse swaps and in the case of failure refund your onchain funds.
-
-## create normal swap (Onchain -> Lightning)
-1. click on "Swap (IN)" button to open following dialog, select a wallet, choose a proper amount in the min-max range and choose a onchain address to do your refund to if the swap fails after you already commited onchain funds.
----
-
----
-2. after you confirm your inputs, following dialog with the QR code for the onchain transaction, onchain- address and amount, will pop up.
----
-
----
-3. after you pay this onchain address with the correct amount, boltz will see it and will pay your invoice and the sats will appear on your wallet.
-
-if anything goes wrong when boltz is trying to pay your invoice, the swap will fail and you will need to refund your onchain funds after the timeout block height hit. (if boltz can pay the invoice, it wont be able to redeem your onchain funds either).
-
-## create reverse swap (Lightning -> Onchain)
-1. click on "Swap (OUT)" button to open following dialog, select a wallet, choose a proper amount in the min-max range and choose a onchain address to receive your funds to. Instant settlement: means that LNbits will create the onchain claim transaction if it sees the boltz lockup transaction in the mempool, but it is not confirmed yet. it is advised to leave this checked because it is faster and the longer is takes to settle, the higher the chances are that the lightning invoice expires and the swap fails.
----
-
----
-if this swap fails, boltz is doing the onchain refunding, because they have to commit onchain funds.
-
-# refund locked onchain funds from a normal swap (Onchain -> Lightning)
-if for some reason the normal swap fails and you already paid onchain, you can easily refund your btc.
-this can happen if boltz is not able to pay your lightning invoice after you locked up your funds.
-in case that happens, there is a info icon in the Swap (In) List which opens following dialog.
----
-
-----
-if the timeout block height is exceeded you can either press refund and lnbits will do the refunding to the address you specified when creating the swap. Or download the refundfile so you can manually refund your onchain directly on the boltz.exchange website.
-if you think there is something wrong and/or you are unsure, you can ask for help either in LNbits telegram or in Boltz [Discord](https://discord.gg/d6EK85KK).
-In a recent update we made *automated check*, every 15 minutes, to check if LNbits can refund your failed swap.
diff --git a/lnbits/extensions/boltz/__init__.py b/lnbits/extensions/boltz/__init__.py
deleted file mode 100644
index 98255e5e..00000000
--- a/lnbits/extensions/boltz/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-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_boltz")
-
-boltz_ext: APIRouter = APIRouter(prefix="/boltz", tags=["boltz"])
-
-
-def boltz_renderer():
- return template_renderer(["lnbits/extensions/boltz/templates"])
-
-
-boltz_static_files = [
- {
- "path": "/boltz/static",
- "app": StaticFiles(directory="lnbits/extensions/boltz/static"),
- "name": "boltz_static",
- }
-]
-
-from .tasks import check_for_pending_swaps, wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def boltz_start():
- loop = asyncio.get_event_loop()
- loop.create_task(check_for_pending_swaps())
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/boltz/config.json b/lnbits/extensions/boltz/config.json
deleted file mode 100644
index db678207..00000000
--- a/lnbits/extensions/boltz/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Boltz",
- "short_description": "Perform onchain/offchain swaps",
- "tile": "/boltz/static/image/boltz.png",
- "contributors": ["dni"]
-}
diff --git a/lnbits/extensions/boltz/crud.py b/lnbits/extensions/boltz/crud.py
deleted file mode 100644
index 5ad923f6..00000000
--- a/lnbits/extensions/boltz/crud.py
+++ /dev/null
@@ -1,284 +0,0 @@
-import time
-from typing import List, Optional, Union
-
-from boltz_client.boltz import BoltzReverseSwapResponse, BoltzSwapResponse
-from loguru import logger
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import (
- AutoReverseSubmarineSwap,
- CreateAutoReverseSubmarineSwap,
- CreateReverseSubmarineSwap,
- CreateSubmarineSwap,
- ReverseSubmarineSwap,
- SubmarineSwap,
-)
-
-
-async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[SubmarineSwap]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM boltz.submarineswap WHERE wallet IN ({q}) order by time DESC",
- (*wallet_ids,),
- )
-
- return [SubmarineSwap(**row) for row in rows]
-
-
-async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]:
- rows = await db.fetchall(
- "SELECT * FROM boltz.submarineswap WHERE status='pending' order by time DESC",
- )
- return [SubmarineSwap(**row) for row in rows]
-
-
-async def get_submarine_swap(swap_id) -> Optional[SubmarineSwap]:
- row = await db.fetchone(
- "SELECT * FROM boltz.submarineswap WHERE id = ?", (swap_id,)
- )
- return SubmarineSwap(**row) if row else None
-
-
-async def create_submarine_swap(
- data: CreateSubmarineSwap,
- swap: BoltzSwapResponse,
- swap_id: str,
- refund_privkey_wif: str,
- payment_hash: str,
-) -> Optional[SubmarineSwap]:
-
- await db.execute(
- """
- INSERT INTO boltz.submarineswap (
- id,
- wallet,
- payment_hash,
- status,
- boltz_id,
- refund_privkey,
- refund_address,
- expected_amount,
- timeout_block_height,
- address,
- bip21,
- redeem_script,
- amount
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- swap_id,
- data.wallet,
- payment_hash,
- "pending",
- swap.id,
- refund_privkey_wif,
- data.refund_address,
- swap.expectedAmount,
- swap.timeoutBlockHeight,
- swap.address,
- swap.bip21,
- swap.redeemScript,
- data.amount,
- ),
- )
- return await get_submarine_swap(swap_id)
-
-
-async def get_reverse_submarine_swaps(
- wallet_ids: Union[str, List[str]]
-) -> List[ReverseSubmarineSwap]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM boltz.reverse_submarineswap WHERE wallet IN ({q}) order by time DESC",
- (*wallet_ids,),
- )
-
- return [ReverseSubmarineSwap(**row) for row in rows]
-
-
-async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap]:
- rows = await db.fetchall(
- "SELECT * FROM boltz.reverse_submarineswap WHERE status='pending' order by time DESC"
- )
-
- return [ReverseSubmarineSwap(**row) for row in rows]
-
-
-async def get_reverse_submarine_swap(swap_id) -> Optional[ReverseSubmarineSwap]:
- row = await db.fetchone(
- "SELECT * FROM boltz.reverse_submarineswap WHERE id = ?", (swap_id,)
- )
- return ReverseSubmarineSwap(**row) if row else None
-
-
-async def create_reverse_submarine_swap(
- data: CreateReverseSubmarineSwap,
- claim_privkey_wif: str,
- preimage_hex: str,
- swap: BoltzReverseSwapResponse,
-) -> ReverseSubmarineSwap:
-
- swap_id = urlsafe_short_hash()
-
- reverse_swap = ReverseSubmarineSwap(
- id=swap_id,
- wallet=data.wallet,
- status="pending",
- boltz_id=swap.id,
- instant_settlement=data.instant_settlement,
- preimage=preimage_hex,
- claim_privkey=claim_privkey_wif,
- lockup_address=swap.lockupAddress,
- invoice=swap.invoice,
- onchain_amount=swap.onchainAmount,
- onchain_address=data.onchain_address,
- timeout_block_height=swap.timeoutBlockHeight,
- redeem_script=swap.redeemScript,
- amount=data.amount,
- time=int(time.time()),
- )
-
- await db.execute(
- """
- INSERT INTO boltz.reverse_submarineswap (
- id,
- wallet,
- status,
- boltz_id,
- instant_settlement,
- preimage,
- claim_privkey,
- lockup_address,
- invoice,
- onchain_amount,
- onchain_address,
- timeout_block_height,
- redeem_script,
- amount
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- reverse_swap.id,
- reverse_swap.wallet,
- reverse_swap.status,
- reverse_swap.boltz_id,
- reverse_swap.instant_settlement,
- reverse_swap.preimage,
- reverse_swap.claim_privkey,
- reverse_swap.lockup_address,
- reverse_swap.invoice,
- reverse_swap.onchain_amount,
- reverse_swap.onchain_address,
- reverse_swap.timeout_block_height,
- reverse_swap.redeem_script,
- reverse_swap.amount,
- ),
- )
- return reverse_swap
-
-
-async def get_auto_reverse_submarine_swaps(
- wallet_ids: List[str],
-) -> List[AutoReverseSubmarineSwap]:
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM boltz.auto_reverse_submarineswap WHERE wallet IN ({q}) order by time DESC",
- (*wallet_ids,),
- )
- return [AutoReverseSubmarineSwap(**row) for row in rows]
-
-
-async def get_auto_reverse_submarine_swap(
- swap_id,
-) -> Optional[AutoReverseSubmarineSwap]:
- row = await db.fetchone(
- "SELECT * FROM boltz.auto_reverse_submarineswap WHERE id = ?", (swap_id,)
- )
- return AutoReverseSubmarineSwap(**row) if row else None
-
-
-async def get_auto_reverse_submarine_swap_by_wallet(
- wallet_id,
-) -> Optional[AutoReverseSubmarineSwap]:
- row = await db.fetchone(
- "SELECT * FROM boltz.auto_reverse_submarineswap WHERE wallet = ?", (wallet_id,)
- )
- return AutoReverseSubmarineSwap(**row) if row else None
-
-
-async def create_auto_reverse_submarine_swap(
- swap: CreateAutoReverseSubmarineSwap,
-) -> Optional[AutoReverseSubmarineSwap]:
-
- swap_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO boltz.auto_reverse_submarineswap (
- id,
- wallet,
- onchain_address,
- instant_settlement,
- balance,
- amount
- )
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (
- swap_id,
- swap.wallet,
- swap.onchain_address,
- swap.instant_settlement,
- swap.balance,
- swap.amount,
- ),
- )
- return await get_auto_reverse_submarine_swap(swap_id)
-
-
-async def delete_auto_reverse_submarine_swap(swap_id):
- await db.execute(
- "DELETE FROM boltz.auto_reverse_submarineswap WHERE id = ?", (swap_id,)
- )
-
-
-async def update_swap_status(swap_id: str, status: str):
-
- swap = await get_submarine_swap(swap_id)
- if swap:
- await db.execute(
- "UPDATE boltz.submarineswap SET status='"
- + status
- + "' WHERE id='"
- + swap.id
- + "'"
- )
- logger.info(
- f"Boltz - swap status change: {status}. boltz_id: {swap.boltz_id}, wallet: {swap.wallet}"
- )
- return swap
-
- reverse_swap = await get_reverse_submarine_swap(swap_id)
- if reverse_swap:
- await db.execute(
- "UPDATE boltz.reverse_submarineswap SET status='"
- + status
- + "' WHERE id='"
- + reverse_swap.id
- + "'"
- )
- logger.info(
- f"Boltz - reverse swap status change: {status}. boltz_id: {reverse_swap.boltz_id}, wallet: {reverse_swap.wallet}"
- )
- return reverse_swap
-
- return None
diff --git a/lnbits/extensions/boltz/migrations.py b/lnbits/extensions/boltz/migrations.py
deleted file mode 100644
index 66648fcc..00000000
--- a/lnbits/extensions/boltz/migrations.py
+++ /dev/null
@@ -1,64 +0,0 @@
-async def m001_initial(db):
- await db.execute(
- f"""
- CREATE TABLE boltz.submarineswap (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- payment_hash TEXT NOT NULL,
- amount {db.big_int} NOT NULL,
- status TEXT NOT NULL,
- boltz_id TEXT NOT NULL,
- refund_address TEXT NOT NULL,
- refund_privkey TEXT NOT NULL,
- expected_amount {db.big_int} NOT NULL,
- timeout_block_height INT NOT NULL,
- address TEXT NOT NULL,
- bip21 TEXT NOT NULL,
- redeem_script TEXT NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
- await db.execute(
- f"""
- CREATE TABLE boltz.reverse_submarineswap (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- onchain_address TEXT NOT NULL,
- amount {db.big_int} NOT NULL,
- instant_settlement BOOLEAN NOT NULL,
- status TEXT NOT NULL,
- boltz_id TEXT NOT NULL,
- timeout_block_height INT NOT NULL,
- redeem_script TEXT NOT NULL,
- preimage TEXT NOT NULL,
- claim_privkey TEXT NOT NULL,
- lockup_address TEXT NOT NULL,
- invoice TEXT NOT NULL,
- onchain_amount {db.big_int} NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
-
-
-async def m002_auto_swaps(db):
- await db.execute(
- """
- CREATE TABLE boltz.auto_reverse_submarineswap (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- onchain_address TEXT NOT NULL,
- amount INT NOT NULL,
- balance INT NOT NULL,
- instant_settlement BOOLEAN NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
diff --git a/lnbits/extensions/boltz/models.py b/lnbits/extensions/boltz/models.py
deleted file mode 100644
index 9500b678..00000000
--- a/lnbits/extensions/boltz/models.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class SubmarineSwap(BaseModel):
- id: str
- wallet: str
- amount: int
- payment_hash: str
- time: int
- status: str
- refund_privkey: str
- refund_address: str
- boltz_id: str
- expected_amount: int
- timeout_block_height: int
- address: str
- bip21: str
- redeem_script: str
-
-
-class CreateSubmarineSwap(BaseModel):
- wallet: str = Query(...)
- refund_address: str = Query(...)
- amount: int = Query(...)
-
-
-class ReverseSubmarineSwap(BaseModel):
- id: str
- wallet: str
- amount: int
- onchain_address: str
- instant_settlement: bool
- time: int
- status: str
- boltz_id: str
- preimage: str
- claim_privkey: str
- lockup_address: str
- invoice: str
- onchain_amount: int
- timeout_block_height: int
- redeem_script: str
-
-
-class CreateReverseSubmarineSwap(BaseModel):
- wallet: str = Query(...)
- amount: int = Query(...)
- instant_settlement: bool = Query(...)
- onchain_address: str = Query(...)
-
-
-class AutoReverseSubmarineSwap(BaseModel):
- id: str
- wallet: str
- amount: int
- balance: int
- onchain_address: str
- instant_settlement: bool
- time: int
-
-
-class CreateAutoReverseSubmarineSwap(BaseModel):
- wallet: str = Query(...)
- amount: int = Query(...)
- balance: int = Query(0)
- instant_settlement: bool = Query(...)
- onchain_address: str = Query(...)
diff --git a/lnbits/extensions/boltz/static/image/boltz.png b/lnbits/extensions/boltz/static/image/boltz.png
deleted file mode 100644
index 2dcefc94..00000000
Binary files a/lnbits/extensions/boltz/static/image/boltz.png and /dev/null differ
diff --git a/lnbits/extensions/boltz/tasks.py b/lnbits/extensions/boltz/tasks.py
deleted file mode 100644
index 63b981b1..00000000
--- a/lnbits/extensions/boltz/tasks.py
+++ /dev/null
@@ -1,180 +0,0 @@
-import asyncio
-
-from boltz_client.boltz import BoltzNotFoundException, BoltzSwapStatusException
-from boltz_client.mempool import MempoolBlockHeightException
-from loguru import logger
-
-from lnbits.core.crud import get_wallet
-from lnbits.core.models import Payment
-from lnbits.core.services import check_transaction_status, fee_reserve
-from lnbits.helpers import get_current_extension_name
-from lnbits.tasks import register_invoice_listener
-
-from .crud import (
- create_reverse_submarine_swap,
- get_all_pending_reverse_submarine_swaps,
- get_all_pending_submarine_swaps,
- get_auto_reverse_submarine_swap_by_wallet,
- get_submarine_swap,
- update_swap_status,
-)
-from .models import CreateReverseSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap
-from .utils import create_boltz_client, execute_reverse_swap
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue, get_current_extension_name())
-
- while True:
- payment = await invoice_queue.get()
- await on_invoice_paid(payment)
-
-
-async def on_invoice_paid(payment: Payment) -> None:
-
- await check_for_auto_swap(payment)
-
- if payment.extra.get("tag") != "boltz":
- # not a boltz invoice
- return
-
- await payment.set_pending(False)
-
- if payment.extra:
- swap_id = payment.extra.get("swap_id")
- if swap_id:
- swap = await get_submarine_swap(swap_id)
- if swap:
- await update_swap_status(swap_id, "complete")
-
-
-async def check_for_auto_swap(payment: Payment) -> None:
- auto_swap = await get_auto_reverse_submarine_swap_by_wallet(payment.wallet_id)
- if auto_swap:
- wallet = await get_wallet(payment.wallet_id)
- if wallet:
- reserve = fee_reserve(wallet.balance_msat) / 1000
- balance = wallet.balance_msat / 1000
- amount = balance - auto_swap.balance - reserve
- if amount >= auto_swap.amount:
-
- client = create_boltz_client()
- claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
- amount=int(amount)
- )
- new_swap = await create_reverse_submarine_swap(
- CreateReverseSubmarineSwap(
- wallet=auto_swap.wallet,
- amount=int(amount),
- instant_settlement=auto_swap.instant_settlement,
- onchain_address=auto_swap.onchain_address,
- ),
- claim_privkey_wif,
- preimage_hex,
- swap,
- )
- await execute_reverse_swap(client, new_swap)
-
- logger.info(
- f"Boltz: auto reverse swap created with amount: {amount}, boltz_id: {new_swap.boltz_id}"
- )
-
-
-"""
-testcases for boltz startup
-A. normal swaps
- 1. test: create -> kill -> start -> startup invoice listeners -> pay onchain funds -> should complete
- 2. test: create -> kill -> pay onchain funds -> mine block -> start -> startup check -> should complete
- 3. test: create -> kill -> mine blocks and hit timeout -> start -> should go timeout/failed
- 4. test: create -> kill -> pay to less onchain funds -> mine blocks hit timeout -> start lnbits -> should be refunded
-
-B. reverse swaps
- 1. test: create instant -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should claim/complete
- 2. test: create -> kill -> boltz does lockup -> not confirmed -> start lnbits -> mine blocks -> should claim/complete
- 3. test: create -> kill -> boltz does lockup -> confirmed -> start lnbits -> should claim/complete
-"""
-
-
-async def check_for_pending_swaps():
- try:
- swaps = await get_all_pending_submarine_swaps()
- reverse_swaps = await get_all_pending_reverse_submarine_swaps()
- if len(swaps) > 0 or len(reverse_swaps) > 0:
- logger.debug("Boltz - startup swap check")
- except:
- logger.error(
- "Boltz - startup swap check, database is not created yet, do nothing"
- )
- return
-
- client = create_boltz_client()
-
- if len(swaps) > 0:
- logger.debug(f"Boltz - {len(swaps)} pending swaps")
- for swap in swaps:
- await check_swap(swap, client)
-
- if len(reverse_swaps) > 0:
- logger.debug(f"Boltz - {len(reverse_swaps)} pending reverse swaps")
- for reverse_swap in reverse_swaps:
- await check_reverse_swap(reverse_swap, client)
-
-
-async def check_swap(swap: SubmarineSwap, client):
- try:
- payment_status = await check_transaction_status(swap.wallet, swap.payment_hash)
- if payment_status.paid:
- logger.debug(f"Boltz - swap: {swap.boltz_id} got paid while offline.")
- await update_swap_status(swap.id, "complete")
- else:
- try:
- _ = client.swap_status(swap.id)
- except:
- txs = client.mempool.get_txs_from_address(swap.address)
- if len(txs) == 0:
- await update_swap_status(swap.id, "timeout")
- else:
- await client.refund_swap(
- privkey_wif=swap.refund_privkey,
- lockup_address=swap.address,
- receive_address=swap.refund_address,
- redeem_script_hex=swap.redeem_script,
- timeout_block_height=swap.timeout_block_height,
- )
- await update_swap_status(swap.id, "refunded")
- except BoltzNotFoundException:
- logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
- await update_swap_status(swap.id, "failed")
- except MempoolBlockHeightException:
- logger.debug(
- f"Boltz - tried to refund swap: {swap.id}, but has not reached the timeout."
- )
- except Exception as exc:
- logger.error(f"Boltz - unhandled exception, swap: {swap.id} - {str(exc)}")
-
-
-async def check_reverse_swap(reverse_swap: ReverseSubmarineSwap, client):
- try:
- _ = client.swap_status(reverse_swap.boltz_id)
- await client.claim_reverse_swap(
- lockup_address=reverse_swap.lockup_address,
- receive_address=reverse_swap.onchain_address,
- privkey_wif=reverse_swap.claim_privkey,
- preimage_hex=reverse_swap.preimage,
- redeem_script_hex=reverse_swap.redeem_script,
- zeroconf=reverse_swap.instant_settlement,
- )
- await update_swap_status(reverse_swap.id, "complete")
-
- except BoltzSwapStatusException as exc:
- logger.debug(f"Boltz - swap_status: {str(exc)}")
- await update_swap_status(reverse_swap.id, "failed")
- # should only happen while development when regtest is reset
- except BoltzNotFoundException:
- logger.debug(f"Boltz - reverse swap: {reverse_swap.boltz_id} does not exist.")
- await update_swap_status(reverse_swap.id, "failed")
- except Exception as exc:
- logger.error(
- f"Boltz - unhandled exception, reverse swap: {reverse_swap.id} - {str(exc)}"
- )
diff --git a/lnbits/extensions/boltz/templates/boltz/_api_docs.html b/lnbits/extensions/boltz/templates/boltz/_api_docs.html
deleted file mode 100644
index f1be62a7..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_api_docs.html
+++ /dev/null
@@ -1,109 +0,0 @@
-
-
-
-
- NON CUSTODIAL atomic swap service
-
- Providing trustless and account-free swap services since 2018. Move IN and
- OUT of the lightning network and remain in control of your bitcoin, at all
- time.
-
-
- Link:
- https://boltz.exchange
-
-
- README:
- read more
-
-
- Extension created by,
- dni
-
-
-
-
-
-
- Fee Information
-
-
- {% raw %} Every swap consists of 2 onchain transactions, lockup and claim
- / refund, routing fees and a Boltz fee of
- {{ boltzConfig.fee_percentage }}% . {% endraw %}
-
-
-
-
- {% raw %} You want to swap out {{ boltzExample.amount }} sats, Lightning
- to Onchain:
-
- Onchain lockup tx fee: ~{{ boltzExample.onchain_boltz }} sats
-
- Onchain claim tx fee: {{ boltzExample.onchain_lnbits }} sats
- (hardcoded)
-
- Routing fees (paid by you): unknown
-
- Boltz fees: {{ boltzExample.boltz_fee }} sats ({{
- boltzConfig.fee_percentage }}%)
-
-
- Fees total: {{ boltzExample.reverse_fee_total }} sats + routing fees
-
- You receive: {{ boltzExample.reverse_receive }} sats
-
-
- onchain_amount_received = amount - (amount * boltz_fee / 100) -
- lockup_fee - claim_fee
-
- {% endraw %}
-
-
-
-
- {% raw %} You want to swap in {{ boltzExample.amount }} sats, Onchain to
- Lightning:
-
- Onchain lockup tx fee: whatever you choose when paying
- Onchain claim tx fee: ~{{ boltzExample.onchain_boltz }} sats
- Routing fees (paid by boltz): unknown
-
- Boltz fees: {{ boltzExample.boltz_fee }} sats ({{
- boltzConfig.fee_percentage }}%)
-
-
- Fees total: {{ boltzExample.normal_fee_total }} sats + lockup_fee
-
-
- You pay onchain: {{ boltzExample.normal_expected_amount }} sats +
- lockup_fee
-
- You receive lightning: {{ boltzExample.amount }} sats
-
- onchain_payment = amount + (amount * boltz_fee / 100) + claim_fee
- {% endraw %}
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html b/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html
deleted file mode 100644
index c9c682a8..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
-
-
- mininum balance kept in wallet after a swap + the fee_reserve
-
-
-
-
-
-
-
- Create Onchain TX when transaction is in mempool, but not
- confirmed yet.
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapList.html b/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapList.html
deleted file mode 100644
index b297524f..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapList.html
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
Auto Lightning -> Onchain
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
- delete the automatic reverse swap
-
-
-
- {{ col.value }}
-
-
-
- {% endraw %}
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_buttons.html b/lnbits/extensions/boltz/templates/boltz/_buttons.html
deleted file mode 100644
index 3817b076..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_buttons.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
- Send onchain funds offchain (BTC -> LN)
-
-
-
-
- Send offchain funds to onchain address (LN -> BTC)
-
-
-
-
- Automatically send offchain funds to onchain address (LN -> BTC) with a
- predefined threshold
-
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html b/lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html
deleted file mode 100644
index e59702d2..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
- pending swaps
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
- refund swap
-
-
- dowload refund file
-
-
- open tx on mempool.space
-
-
-
- {{ col.value }}
-
-
-
- {% endraw %}
-
- pending reverse swaps
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
- open tx on mempool.space
-
-
-
- {{ col.value }}
-
-
-
- {% endraw %}
-
-
- Close
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_qrDialog.html b/lnbits/extensions/boltz/templates/boltz/_qrDialog.html
deleted file mode 100644
index 053ef65e..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_qrDialog.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
- {% raw %}
- Bitcoin On-Chain TX
- Expected amount (sats): {{ qrCodeDialog.data.expected_amount }}
-
- Expected amount (btc): {{ qrCodeDialog.data.expected_amount_btc }}
-
- Onchain Address: {{ qrCodeDialog.data.address }}
- {% endraw %}
-
-
- Copy On-Chain Address
- Close
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapDialog.html b/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapDialog.html
deleted file mode 100644
index 5b3cf861..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapDialog.html
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- Create Onchain TX when transaction is in mempool, but not
- confirmed yet.
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapList.html b/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapList.html
deleted file mode 100644
index fc9668d0..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapList.html
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
Lightning -> Onchain
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
- open swap status info
-
-
- open tx on mempool.space
-
-
-
- {{ col.value }}
-
-
-
- {% endraw %}
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_statusDialog.html b/lnbits/extensions/boltz/templates/boltz/_statusDialog.html
deleted file mode 100644
index f6c14abc..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_statusDialog.html
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
- {% raw %}
- Status: {{ statusDialog.data.status }}
-
- {% endraw %}
-
-
- Refund
-
- Download refundfile
- Close
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_submarineSwapDialog.html b/lnbits/extensions/boltz/templates/boltz/_submarineSwapDialog.html
deleted file mode 100644
index bf6aaa18..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_submarineSwapDialog.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html b/lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html
deleted file mode 100644
index b42e1dee..00000000
--- a/lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
Onchain -> Lightning
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
- open swap onchain details
-
-
- open swap status info
-
-
- open tx on mempool.space
-
-
-
- {{ col.value }}
-
-
-
- {% endraw %}
-
-
-
diff --git a/lnbits/extensions/boltz/templates/boltz/index.html b/lnbits/extensions/boltz/templates/boltz/index.html
deleted file mode 100644
index d985a01f..00000000
--- a/lnbits/extensions/boltz/templates/boltz/index.html
+++ /dev/null
@@ -1,621 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
- {% include "boltz/_buttons.html" %} {% include
- "boltz/_submarineSwapList.html" %} {% include
- "boltz/_reverseSubmarineSwapList.html" %} {% include
- "boltz/_autoReverseSwapList.html" %}
-
-
- {% include "boltz/_api_docs.html" %}
-
- {% include "boltz/_submarineSwapDialog.html" %} {% include
- "boltz/_reverseSubmarineSwapDialog.html" %} {% include
- "boltz/_autoReverseSwapDialog.html" %} {% include "boltz/_qrDialog.html" %} {%
- include "boltz/_statusDialog.html" %}
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/boltz/utils.py b/lnbits/extensions/boltz/utils.py
deleted file mode 100644
index 7623fb6f..00000000
--- a/lnbits/extensions/boltz/utils.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import asyncio
-import calendar
-import datetime
-from typing import Awaitable
-
-from boltz_client.boltz import BoltzClient, BoltzConfig
-
-from lnbits.core.services import fee_reserve, get_wallet, pay_invoice
-from lnbits.settings import settings
-
-from .models import ReverseSubmarineSwap
-
-
-def create_boltz_client() -> BoltzClient:
- config = BoltzConfig(
- network=settings.boltz_network,
- api_url=settings.boltz_url,
- mempool_url=f"{settings.boltz_mempool_space_url}/api",
- mempool_ws_url=f"{settings.boltz_mempool_space_url_ws}/api/v1/ws",
- referral_id="lnbits",
- )
- return BoltzClient(config)
-
-
-async def check_balance(data) -> bool:
- # check if we can pay the invoice before we create the actual swap on boltz
- amount_msat = data.amount * 1000
- fee_reserve_msat = fee_reserve(amount_msat)
- wallet = await get_wallet(data.wallet)
- assert wallet
- if wallet.balance_msat - fee_reserve_msat < amount_msat:
- return False
- return True
-
-
-def get_timestamp():
- date = datetime.datetime.utcnow()
- return calendar.timegm(date.utctimetuple())
-
-
-async def execute_reverse_swap(client, swap: ReverseSubmarineSwap):
- # claim_task is watching onchain address for the lockup transaction to arrive / confirm
- # and if the lockup is there, claim the onchain revealing preimage for hold invoice
- claim_task = asyncio.create_task(
- client.claim_reverse_swap(
- privkey_wif=swap.claim_privkey,
- preimage_hex=swap.preimage,
- lockup_address=swap.lockup_address,
- receive_address=swap.onchain_address,
- redeem_script_hex=swap.redeem_script,
- )
- )
- # pay_task is paying the hold invoice which gets held until you reveal your preimage when claiming your onchain funds
- pay_task = pay_invoice_and_update_status(
- swap.id,
- claim_task,
- pay_invoice(
- wallet_id=swap.wallet,
- payment_request=swap.invoice,
- description=f"reverse swap for {swap.onchain_amount} sats on boltz.exchange",
- extra={"tag": "boltz", "swap_id": swap.id, "reverse": True},
- ),
- )
-
- # they need to run be concurrently, because else pay_task will lock the eventloop and claim_task will not be executed.
- # the lockup transaction can only happen after you pay the invoice, which cannot be redeemed immediatly -> hold invoice
- # after getting the lockup transaction, you can claim the onchain funds revealing the preimage for boltz to redeem the hold invoice
- asyncio.gather(claim_task, pay_task)
-
-
-def pay_invoice_and_update_status(
- swap_id: str, wstask: asyncio.Task, awaitable: Awaitable
-) -> asyncio.Task:
- async def _pay_invoice(awaitable):
- from .crud import update_swap_status
-
- try:
- awaited = await awaitable
- await update_swap_status(swap_id, "complete")
- return awaited
- except asyncio.exceptions.CancelledError:
- """lnbits process was exited, do nothing and handle it in startup script"""
- except:
- wstask.cancel()
- await update_swap_status(swap_id, "failed")
-
- return asyncio.create_task(_pay_invoice(awaitable))
diff --git a/lnbits/extensions/boltz/views.py b/lnbits/extensions/boltz/views.py
deleted file mode 100644
index 4b0e6d53..00000000
--- a/lnbits/extensions/boltz/views.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from urllib.parse import urlparse
-
-from fastapi import Depends, 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 boltz_ext, boltz_renderer
-
-templates = Jinja2Templates(directory="templates")
-
-
-@boltz_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- root_url = urlparse(str(request.url)).netloc
- return boltz_renderer().TemplateResponse(
- "boltz/index.html",
- {"request": request, "user": user.dict(), "root_url": root_url},
- )
diff --git a/lnbits/extensions/boltz/views_api.py b/lnbits/extensions/boltz/views_api.py
deleted file mode 100644
index ffec612c..00000000
--- a/lnbits/extensions/boltz/views_api.py
+++ /dev/null
@@ -1,332 +0,0 @@
-from http import HTTPStatus
-from typing import List
-
-from fastapi import Depends, Query, status
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_user
-from lnbits.core.services import create_invoice
-from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
-from lnbits.helpers import urlsafe_short_hash
-from lnbits.settings import settings
-
-from . import boltz_ext
-from .crud import (
- create_auto_reverse_submarine_swap,
- create_reverse_submarine_swap,
- create_submarine_swap,
- delete_auto_reverse_submarine_swap,
- get_auto_reverse_submarine_swap_by_wallet,
- get_auto_reverse_submarine_swaps,
- get_reverse_submarine_swap,
- get_reverse_submarine_swaps,
- get_submarine_swap,
- get_submarine_swaps,
- update_swap_status,
-)
-from .models import (
- AutoReverseSubmarineSwap,
- CreateAutoReverseSubmarineSwap,
- CreateReverseSubmarineSwap,
- CreateSubmarineSwap,
- ReverseSubmarineSwap,
- SubmarineSwap,
-)
-from .utils import check_balance, create_boltz_client, execute_reverse_swap
-
-
-@boltz_ext.get(
- "/api/v1/swap/mempool",
- name="boltz.get /swap/mempool",
- summary="get a the mempool url",
- description="""
- This endpoint gets the URL from mempool.space
- """,
- response_description="mempool.space url",
- response_model=str,
-)
-async def api_mempool_url():
- return settings.boltz_mempool_space_url
-
-
-# NORMAL SWAP
-@boltz_ext.get(
- "/api/v1/swap",
- name="boltz.get /swap",
- summary="get a list of swaps a swap",
- description="""
- This endpoint gets a list of normal swaps.
- """,
- response_description="list of normal swaps",
- dependencies=[Depends(get_key_type)],
- response_model=List[SubmarineSwap],
-)
-async def api_submarineswap(
- 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)
- wallet_ids = user.wallet_ids if user else []
- return [swap.dict() for swap in await get_submarine_swaps(wallet_ids)]
-
-
-@boltz_ext.post(
- "/api/v1/swap/refund",
- name="boltz.swap_refund",
- summary="refund of a swap",
- description="""
- This endpoint attempts to refund a normal swaps, creates onchain tx and sets swap status ro refunded.
- """,
- response_description="refunded swap with status set to refunded",
- dependencies=[Depends(require_admin_key)],
- response_model=SubmarineSwap,
- responses={
- 400: {"description": "when swap_id is missing"},
- 404: {"description": "when swap is not found"},
- 405: {"description": "when swap is not pending"},
- 500: {
- "description": "when something goes wrong creating the refund onchain tx"
- },
- },
-)
-async def api_submarineswap_refund(swap_id: str):
- if not swap_id:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="swap_id missing"
- )
- swap = await get_submarine_swap(swap_id)
- if not swap:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
- )
- if swap.status != "pending":
- raise HTTPException(
- status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="swap is not pending."
- )
-
- client = create_boltz_client()
- await client.refund_swap(
- privkey_wif=swap.refund_privkey,
- lockup_address=swap.address,
- receive_address=swap.refund_address,
- redeem_script_hex=swap.redeem_script,
- timeout_block_height=swap.timeout_block_height,
- )
-
- await update_swap_status(swap.id, "refunded")
- return swap
-
-
-@boltz_ext.post(
- "/api/v1/swap",
- status_code=status.HTTP_201_CREATED,
- name="boltz.post /swap",
- summary="create a submarine swap",
- description="""
- This endpoint creates a submarine swap
- """,
- response_description="create swap",
- response_model=SubmarineSwap,
- dependencies=[Depends(require_admin_key)],
- responses={
- 405: {
- "description": "auto reverse swap is active, a swap would immediatly be swapped out again."
- },
- 500: {"description": "boltz error"},
- },
-)
-async def api_submarineswap_create(data: CreateSubmarineSwap):
-
- auto_swap = await get_auto_reverse_submarine_swap_by_wallet(data.wallet)
- if auto_swap:
- raise HTTPException(
- status_code=HTTPStatus.METHOD_NOT_ALLOWED,
- detail="auto reverse swap is active, a swap would immediatly be swapped out again.",
- )
-
- client = create_boltz_client()
- swap_id = urlsafe_short_hash()
- payment_hash, payment_request = await create_invoice(
- wallet_id=data.wallet,
- amount=data.amount,
- memo=f"swap of {data.amount} sats on boltz.exchange",
- extra={"tag": "boltz", "swap_id": swap_id},
- )
- refund_privkey_wif, swap = client.create_swap(payment_request)
- new_swap = await create_submarine_swap(
- data, swap, swap_id, refund_privkey_wif, payment_hash
- )
- return new_swap.dict() if new_swap else None
-
-
-# REVERSE SWAP
-@boltz_ext.get(
- "/api/v1/swap/reverse",
- name="boltz.get /swap/reverse",
- summary="get a list of reverse swaps",
- description="""
- This endpoint gets a list of reverse swaps.
- """,
- response_description="list of reverse swaps",
- dependencies=[Depends(get_key_type)],
- response_model=List[ReverseSubmarineSwap],
-)
-async def api_reverse_submarineswap(
- 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)
- wallet_ids = user.wallet_ids if user else []
- return [swap for swap in await get_reverse_submarine_swaps(wallet_ids)]
-
-
-@boltz_ext.post(
- "/api/v1/swap/reverse",
- status_code=status.HTTP_201_CREATED,
- name="boltz.post /swap/reverse",
- summary="create a reverse submarine swap",
- description="""
- This endpoint creates a reverse submarine swap
- """,
- response_description="create reverse swap",
- response_model=ReverseSubmarineSwap,
- dependencies=[Depends(require_admin_key)],
- responses={
- 405: {"description": "not allowed method, insufficient balance"},
- 500: {"description": "boltz error"},
- },
-)
-async def api_reverse_submarineswap_create(
- data: CreateReverseSubmarineSwap,
-) -> ReverseSubmarineSwap:
-
- if not await check_balance(data):
- raise HTTPException(
- status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Insufficient balance."
- )
- client = create_boltz_client()
- claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
- amount=data.amount
- )
- new_swap = await create_reverse_submarine_swap(
- data, claim_privkey_wif, preimage_hex, swap
- )
- await execute_reverse_swap(client, new_swap)
- return new_swap
-
-
-@boltz_ext.get(
- "/api/v1/swap/reverse/auto",
- name="boltz.get /swap/reverse/auto",
- summary="get a list of auto reverse swaps",
- description="""
- This endpoint gets a list of auto reverse swaps.
- """,
- response_description="list of auto reverse swaps",
- dependencies=[Depends(get_key_type)],
- response_model=List[AutoReverseSubmarineSwap],
-)
-async def api_auto_reverse_submarineswap(
- 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)
- wallet_ids = user.wallet_ids if user else []
- return [swap.dict() for swap in await get_auto_reverse_submarine_swaps(wallet_ids)]
-
-
-@boltz_ext.post(
- "/api/v1/swap/reverse/auto",
- status_code=status.HTTP_201_CREATED,
- name="boltz.post /swap/reverse/auto",
- summary="create a auto reverse submarine swap",
- description="""
- This endpoint creates a auto reverse submarine swap
- """,
- response_description="create auto reverse swap",
- response_model=AutoReverseSubmarineSwap,
- dependencies=[Depends(require_admin_key)],
- responses={
- 405: {
- "description": "auto reverse swap is active, only 1 swap per wallet possible."
- },
- },
-)
-async def api_auto_reverse_submarineswap_create(data: CreateAutoReverseSubmarineSwap):
-
- auto_swap = await get_auto_reverse_submarine_swap_by_wallet(data.wallet)
- if auto_swap:
- raise HTTPException(
- status_code=HTTPStatus.METHOD_NOT_ALLOWED,
- detail="auto reverse swap is active, only 1 swap per wallet possible.",
- )
-
- swap = await create_auto_reverse_submarine_swap(data)
- return swap.dict() if swap else None
-
-
-@boltz_ext.delete(
- "/api/v1/swap/reverse/auto/{swap_id}",
- name="boltz.delete /swap/reverse/auto",
- summary="delete a auto reverse submarine swap",
- description="""
- This endpoint deletes a auto reverse submarine swap
- """,
- response_description="delete auto reverse swap",
- dependencies=[Depends(require_admin_key)],
-)
-async def api_auto_reverse_submarineswap_delete(swap_id: str):
- await delete_auto_reverse_submarine_swap(swap_id)
- return "OK"
-
-
-@boltz_ext.post(
- "/api/v1/swap/status",
- name="boltz.swap_status",
- summary="shows the status of a swap",
- description="""
- This endpoint attempts to get the status of the swap.
- """,
- response_description="status of swap json",
- dependencies=[Depends(require_admin_key)],
- responses={
- 404: {"description": "when swap_id is not found"},
- },
-)
-async def api_swap_status(swap_id: str):
- swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap(
- swap_id
- )
- if not swap:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
- )
-
- client = create_boltz_client()
- status = client.swap_status(swap.boltz_id)
- return status
-
-
-@boltz_ext.get(
- "/api/v1/swap/boltz",
- name="boltz.get /swap/boltz",
- summary="get a boltz configuration",
- description="""
- This endpoint gets configuration for boltz. (limits, fees...)
- """,
- response_description="dict of boltz config",
- response_model=dict,
-)
-async def api_boltz_config():
- client = create_boltz_client()
- return {
- "minimal": client.limit_minimal,
- "maximal": client.limit_maximal,
- "fee_percentage": client.fee_percentage,
- }
diff --git a/lnbits/extensions/cashu/README.md b/lnbits/extensions/cashu/README.md
deleted file mode 100644
index 8f53b474..00000000
--- a/lnbits/extensions/cashu/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Cashu
-
-## Create ecash mint for pegging in/out of ecash
-
-
-
-### Usage
-
-1. Enable extension
-2. Create a Mint
-3. Share wallet
diff --git a/lnbits/extensions/cashu/__init__.py b/lnbits/extensions/cashu/__init__.py
deleted file mode 100644
index ca831ce0..00000000
--- a/lnbits/extensions/cashu/__init__.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import asyncio
-
-from environs import Env
-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_cashu")
-
-
-cashu_static_files = [
- {
- "path": "/cashu/static",
- "app": StaticFiles(directory="lnbits/extensions/cashu/static"),
- "name": "cashu_static",
- }
-]
-from cashu.mint.ledger import Ledger
-
-env = Env()
-env.read_env()
-
-ledger = Ledger(
- db=db,
- seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"),
- derivation_path="0/0/0/1",
-)
-
-cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"])
-
-
-def cashu_renderer():
- return template_renderer(["lnbits/extensions/cashu/templates"])
-
-
-from .tasks import startup_cashu_mint, wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def cashu_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(startup_cashu_mint))
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/cashu/config.json b/lnbits/extensions/cashu/config.json
deleted file mode 100644
index 14ff1743..00000000
--- a/lnbits/extensions/cashu/config.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "Cashu",
- "short_description": "Ecash mint and wallet",
- "tile": "/cashu/static/image/cashu.png",
- "contributors": ["calle", "vlad", "arcbtc"],
- "hidden": false
-}
diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py
deleted file mode 100644
index e27cc98c..00000000
--- a/lnbits/extensions/cashu/crud.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from typing import List, Optional, Union
-
-from . import db
-from .models import Cashu
-
-
-async def create_cashu(
- cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
-) -> Cashu:
-
- await db.execute(
- """
- INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, keyset_id)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- cashu_id,
- wallet_id,
- data.name,
- data.tickershort,
- data.fraction,
- data.maxsats,
- data.coins,
- keyset_id,
- ),
- )
-
- cashu = await get_cashu(cashu_id)
- assert cashu, "Newly created cashu couldn't be retrieved"
- return cashu
-
-
-async def get_cashu(cashu_id) -> Optional[Cashu]:
- row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
- return Cashu(**row) if row else None
-
-
-async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM cashu.cashu WHERE wallet IN ({q})", (*wallet_ids,)
- )
-
- return [Cashu(**row) for row in rows]
-
-
-async def delete_cashu(cashu_id) -> None:
- await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,))
diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py
deleted file mode 100644
index b54c4108..00000000
--- a/lnbits/extensions/cashu/migrations.py
+++ /dev/null
@@ -1,33 +0,0 @@
-async def m001_initial(db):
- """
- Initial cashu table.
- """
- await db.execute(
- """
- CREATE TABLE cashu.cashu (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- name TEXT NOT NULL,
- tickershort TEXT DEFAULT 'sats',
- fraction BOOL,
- maxsats INT,
- coins INT,
- keyset_id TEXT NOT NULL,
- issued_sat INT
- );
- """
- )
-
- """
- Initial cashus table.
- """
- await db.execute(
- """
- CREATE TABLE cashu.pegs (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- inout BOOL NOT NULL,
- amount INT
- );
- """
- )
diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py
deleted file mode 100644
index aaff195f..00000000
--- a/lnbits/extensions/cashu/models.py
+++ /dev/null
@@ -1,148 +0,0 @@
-from sqlite3 import Row
-from typing import List
-
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class Cashu(BaseModel):
- id: str = Query(None)
- name: str = Query(None)
- wallet: str = Query(None)
- tickershort: str = Query(None)
- fraction: bool = Query(None)
- maxsats: int = Query(0)
- coins: int = Query(0)
- keyset_id: str = Query(None)
-
- @classmethod
- def from_row(cls, row: Row):
- return cls(**dict(row))
-
-
-class Pegs(BaseModel):
- id: str
- wallet: str
- inout: str
- amount: str
-
- @classmethod
- def from_row(cls, row: Row):
- return cls(**dict(row))
-
-
-class PayLnurlWData(BaseModel):
- lnurl: str
-
-
-class Promises(BaseModel):
- id: str
- amount: int
- B_b: str
- C_b: str
- cashu_id: str
-
-
-class Proof(BaseModel):
- amount: int
- secret: str
- C: str
- reserved: bool = False # whether this proof is reserved for sending
- send_id: str = "" # unique ID of send attempt
- time_created: str = ""
- time_reserved: str = ""
-
- @classmethod
- def from_row(cls, row: Row):
- return cls(
- amount=row[0],
- C=row[1],
- secret=row[2],
- reserved=row[3] or False,
- send_id=row[4] or "",
- time_created=row[5] or "",
- time_reserved=row[6] or "",
- )
-
- @classmethod
- def from_dict(cls, d: dict):
- assert "secret" in d, "no secret in proof"
- assert "amount" in d, "no amount in proof"
- assert "C" in d, "no C in proof"
- return cls(
- amount=d["amount"],
- C=d["C"],
- secret=d["secret"],
- reserved=d.get("reserved") or False,
- send_id=d.get("send_id") or "",
- time_created=d.get("time_created") or "",
- time_reserved=d.get("time_reserved") or "",
- )
-
- def to_dict(self):
- return dict(amount=self.amount, secret=self.secret, C=self.C)
-
- def __getitem__(self, key):
- return self.__getattribute__(key)
-
- def __setitem__(self, key, val):
- self.__setattr__(key, val)
-
-
-class Proofs(BaseModel):
- """TODO: Use this model"""
-
- proofs: List[Proof]
-
-
-class Invoice(BaseModel):
- amount: int
- pr: str
- hash: str
- issued: bool = False
-
- @classmethod
- def from_row(cls, row: Row):
- return cls(
- amount=int(row[0]),
- pr=str(row[1]),
- hash=str(row[2]),
- issued=bool(row[3]),
- )
-
-
-class BlindedMessage(BaseModel):
- amount: int
- B_: str
-
-
-class BlindedSignature(BaseModel):
- amount: int
- C_: str
-
- @classmethod
- def from_dict(cls, d: dict):
- return cls(
- amount=d["amount"],
- C_=d["C_"],
- )
-
-
-class MintPayloads(BaseModel):
- blinded_messages: List[BlindedMessage] = []
-
-
-class SplitPayload(BaseModel):
- proofs: List[Proof]
- amount: int
- output_data: MintPayloads
-
-
-class CheckPayload(BaseModel):
- proofs: List[Proof]
-
-
-class MeltPayload(BaseModel):
- proofs: List[Proof]
- amount: int
- invoice: str
diff --git a/lnbits/extensions/cashu/static/image/cashu.png b/lnbits/extensions/cashu/static/image/cashu.png
deleted file mode 100644
index e90611fc..00000000
Binary files a/lnbits/extensions/cashu/static/image/cashu.png and /dev/null differ
diff --git a/lnbits/extensions/cashu/static/js/base64.js b/lnbits/extensions/cashu/static/js/base64.js
deleted file mode 100644
index b150882f..00000000
--- a/lnbits/extensions/cashu/static/js/base64.js
+++ /dev/null
@@ -1,37 +0,0 @@
-function unescapeBase64Url(str) {
- return (str + '==='.slice((str.length + 3) % 4))
- .replace(/-/g, '+')
- .replace(/_/g, '/')
-}
-
-function escapeBase64Url(str) {
- return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
-}
-
-const uint8ToBase64 = (function (exports) {
- 'use strict'
-
- var fromCharCode = String.fromCharCode
- var encode = function encode(uint8array) {
- var output = []
-
- for (var i = 0, length = uint8array.length; i < length; i++) {
- output.push(fromCharCode(uint8array[i]))
- }
-
- return btoa(output.join(''))
- }
-
- var asCharCode = function asCharCode(c) {
- return c.charCodeAt(0)
- }
-
- var decode = function decode(chars) {
- return Uint8Array.from(atob(chars), asCharCode)
- }
-
- exports.decode = decode
- exports.encode = encode
-
- return exports
-})({})
diff --git a/lnbits/extensions/cashu/static/js/dhke.js b/lnbits/extensions/cashu/static/js/dhke.js
deleted file mode 100644
index cebef240..00000000
--- a/lnbits/extensions/cashu/static/js/dhke.js
+++ /dev/null
@@ -1,31 +0,0 @@
-async function hashToCurve(secretMessage) {
- let point
- while (!point) {
- const hash = await nobleSecp256k1.utils.sha256(secretMessage)
- const hashHex = nobleSecp256k1.utils.bytesToHex(hash)
- const pointX = '02' + hashHex
- try {
- point = nobleSecp256k1.Point.fromHex(pointX)
- } catch (error) {
- secretMessage = await nobleSecp256k1.utils.sha256(secretMessage)
- }
- }
- return point
-}
-
-async function step1Alice(secretMessage) {
- secretMessage = uint8ToBase64.encode(secretMessage)
- secretMessage = new TextEncoder().encode(secretMessage)
- const Y = await hashToCurve(secretMessage)
- const r_bytes = nobleSecp256k1.utils.randomPrivateKey()
- const r = bytesToNumber(r_bytes)
- const P = nobleSecp256k1.Point.fromPrivateKey(r)
- const B_ = Y.add(P)
- return {B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(r_bytes)}
-}
-
-function step3Alice(C_, r, A) {
- const rInt = bytesToNumber(r)
- const C = C_.subtract(A.multiply(rInt))
- return C
-}
diff --git a/lnbits/extensions/cashu/static/js/noble-secp256k1.js b/lnbits/extensions/cashu/static/js/noble-secp256k1.js
deleted file mode 100644
index 6a6bd441..00000000
--- a/lnbits/extensions/cashu/static/js/noble-secp256k1.js
+++ /dev/null
@@ -1,1178 +0,0 @@
-;(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined'
- ? factory(exports)
- : typeof define === 'function' && define.amd
- ? define(['exports'], factory)
- : ((global =
- typeof globalThis !== 'undefined' ? globalThis : global || self),
- factory((global.nobleSecp256k1 = {})))
-})(this, function (exports) {
- 'use strict'
-
- const _nodeResolve_empty = {}
-
- const nodeCrypto = /*#__PURE__*/ Object.freeze({
- __proto__: null,
- default: _nodeResolve_empty
- })
-
- /*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */
- const _0n = BigInt(0)
- const _1n = BigInt(1)
- const _2n = BigInt(2)
- const _3n = BigInt(3)
- const _8n = BigInt(8)
- const POW_2_256 = _2n ** BigInt(256)
- const CURVE = {
- a: _0n,
- b: BigInt(7),
- P: POW_2_256 - _2n ** BigInt(32) - BigInt(977),
- n: POW_2_256 - BigInt('432420386565659656852420866394968145599'),
- h: _1n,
- Gx: BigInt(
- '55066263022277343669578718895168534326250603453777594175500187360389116729240'
- ),
- Gy: BigInt(
- '32670510020758816978083085130507043184471273380659243275938904335757337482424'
- ),
- beta: BigInt(
- '0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'
- )
- }
- function weistrass(x) {
- const {a, b} = CURVE
- const x2 = mod(x * x)
- const x3 = mod(x2 * x)
- return mod(x3 + a * x + b)
- }
- const USE_ENDOMORPHISM = CURVE.a === _0n
- class JacobianPoint {
- constructor(x, y, z) {
- this.x = x
- this.y = y
- this.z = z
- }
- static fromAffine(p) {
- if (!(p instanceof Point)) {
- throw new TypeError('JacobianPoint#fromAffine: expected Point')
- }
- return new JacobianPoint(p.x, p.y, _1n)
- }
- static toAffineBatch(points) {
- const toInv = invertBatch(points.map(p => p.z))
- return points.map((p, i) => p.toAffine(toInv[i]))
- }
- static normalizeZ(points) {
- return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine)
- }
- equals(other) {
- if (!(other instanceof JacobianPoint))
- throw new TypeError('JacobianPoint expected')
- const {x: X1, y: Y1, z: Z1} = this
- const {x: X2, y: Y2, z: Z2} = other
- const Z1Z1 = mod(Z1 ** _2n)
- const Z2Z2 = mod(Z2 ** _2n)
- const U1 = mod(X1 * Z2Z2)
- const U2 = mod(X2 * Z1Z1)
- const S1 = mod(mod(Y1 * Z2) * Z2Z2)
- const S2 = mod(mod(Y2 * Z1) * Z1Z1)
- return U1 === U2 && S1 === S2
- }
- negate() {
- return new JacobianPoint(this.x, mod(-this.y), this.z)
- }
- double() {
- const {x: X1, y: Y1, z: Z1} = this
- const A = mod(X1 ** _2n)
- const B = mod(Y1 ** _2n)
- const C = mod(B ** _2n)
- const D = mod(_2n * (mod((X1 + B) ** _2n) - A - C))
- const E = mod(_3n * A)
- const F = mod(E ** _2n)
- const X3 = mod(F - _2n * D)
- const Y3 = mod(E * (D - X3) - _8n * C)
- const Z3 = mod(_2n * Y1 * Z1)
- return new JacobianPoint(X3, Y3, Z3)
- }
- add(other) {
- if (!(other instanceof JacobianPoint))
- throw new TypeError('JacobianPoint expected')
- const {x: X1, y: Y1, z: Z1} = this
- const {x: X2, y: Y2, z: Z2} = other
- if (X2 === _0n || Y2 === _0n) return this
- if (X1 === _0n || Y1 === _0n) return other
- const Z1Z1 = mod(Z1 ** _2n)
- const Z2Z2 = mod(Z2 ** _2n)
- const U1 = mod(X1 * Z2Z2)
- const U2 = mod(X2 * Z1Z1)
- const S1 = mod(mod(Y1 * Z2) * Z2Z2)
- const S2 = mod(mod(Y2 * Z1) * Z1Z1)
- const H = mod(U2 - U1)
- const r = mod(S2 - S1)
- if (H === _0n) {
- if (r === _0n) {
- return this.double()
- } else {
- return JacobianPoint.ZERO
- }
- }
- const HH = mod(H ** _2n)
- const HHH = mod(H * HH)
- const V = mod(U1 * HH)
- const X3 = mod(r ** _2n - HHH - _2n * V)
- const Y3 = mod(r * (V - X3) - S1 * HHH)
- const Z3 = mod(Z1 * Z2 * H)
- return new JacobianPoint(X3, Y3, Z3)
- }
- subtract(other) {
- return this.add(other.negate())
- }
- multiplyUnsafe(scalar) {
- const P0 = JacobianPoint.ZERO
- if (typeof scalar === 'bigint' && scalar === _0n) return P0
- let n = normalizeScalar(scalar)
- if (n === _1n) return this
- if (!USE_ENDOMORPHISM) {
- let p = P0
- let d = this
- while (n > _0n) {
- if (n & _1n) p = p.add(d)
- d = d.double()
- n >>= _1n
- }
- return p
- }
- let {k1neg, k1, k2neg, k2} = splitScalarEndo(n)
- let k1p = P0
- let k2p = P0
- let d = this
- while (k1 > _0n || k2 > _0n) {
- if (k1 & _1n) k1p = k1p.add(d)
- if (k2 & _1n) k2p = k2p.add(d)
- d = d.double()
- k1 >>= _1n
- k2 >>= _1n
- }
- if (k1neg) k1p = k1p.negate()
- if (k2neg) k2p = k2p.negate()
- k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z)
- return k1p.add(k2p)
- }
- precomputeWindow(W) {
- const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1
- const points = []
- let p = this
- let base = p
- for (let window = 0; window < windows; window++) {
- base = p
- points.push(base)
- for (let i = 1; i < 2 ** (W - 1); i++) {
- base = base.add(p)
- points.push(base)
- }
- p = base.double()
- }
- return points
- }
- wNAF(n, affinePoint) {
- if (!affinePoint && this.equals(JacobianPoint.BASE))
- affinePoint = Point.BASE
- const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1
- if (256 % W) {
- throw new Error(
- 'Point#wNAF: Invalid precomputation window, must be power of 2'
- )
- }
- let precomputes = affinePoint && pointPrecomputes.get(affinePoint)
- if (!precomputes) {
- precomputes = this.precomputeWindow(W)
- if (affinePoint && W !== 1) {
- precomputes = JacobianPoint.normalizeZ(precomputes)
- pointPrecomputes.set(affinePoint, precomputes)
- }
- }
- let p = JacobianPoint.ZERO
- let f = JacobianPoint.ZERO
- const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W)
- const windowSize = 2 ** (W - 1)
- const mask = BigInt(2 ** W - 1)
- const maxNumber = 2 ** W
- const shiftBy = BigInt(W)
- for (let window = 0; window < windows; window++) {
- const offset = window * windowSize
- let wbits = Number(n & mask)
- n >>= shiftBy
- if (wbits > windowSize) {
- wbits -= maxNumber
- n += _1n
- }
- if (wbits === 0) {
- let pr = precomputes[offset]
- if (window % 2) pr = pr.negate()
- f = f.add(pr)
- } else {
- let cached = precomputes[offset + Math.abs(wbits) - 1]
- if (wbits < 0) cached = cached.negate()
- p = p.add(cached)
- }
- }
- return {p, f}
- }
- multiply(scalar, affinePoint) {
- let n = normalizeScalar(scalar)
- let point
- let fake
- if (USE_ENDOMORPHISM) {
- const {k1neg, k1, k2neg, k2} = splitScalarEndo(n)
- let {p: k1p, f: f1p} = this.wNAF(k1, affinePoint)
- let {p: k2p, f: f2p} = this.wNAF(k2, affinePoint)
- if (k1neg) k1p = k1p.negate()
- if (k2neg) k2p = k2p.negate()
- k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z)
- point = k1p.add(k2p)
- fake = f1p.add(f2p)
- } else {
- const {p, f} = this.wNAF(n, affinePoint)
- point = p
- fake = f
- }
- return JacobianPoint.normalizeZ([point, fake])[0]
- }
- toAffine(invZ = invert(this.z)) {
- const {x, y, z} = this
- const iz1 = invZ
- const iz2 = mod(iz1 * iz1)
- const iz3 = mod(iz2 * iz1)
- const ax = mod(x * iz2)
- const ay = mod(y * iz3)
- const zz = mod(z * iz1)
- if (zz !== _1n) throw new Error('invZ was invalid')
- return new Point(ax, ay)
- }
- }
- JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n)
- JacobianPoint.ZERO = new JacobianPoint(_0n, _1n, _0n)
- const pointPrecomputes = new WeakMap()
- class Point {
- constructor(x, y) {
- this.x = x
- this.y = y
- }
- _setWindowSize(windowSize) {
- this._WINDOW_SIZE = windowSize
- pointPrecomputes.delete(this)
- }
- static fromCompressedHex(bytes) {
- const isShort = bytes.length === 32
- const x = bytesToNumber(isShort ? bytes : bytes.subarray(1))
- if (!isValidFieldElement(x)) throw new Error('Point is not on curve')
- const y2 = weistrass(x)
- let y = sqrtMod(y2)
- const isYOdd = (y & _1n) === _1n
- if (isShort) {
- if (isYOdd) y = mod(-y)
- } else {
- const isFirstByteOdd = (bytes[0] & 1) === 1
- if (isFirstByteOdd !== isYOdd) y = mod(-y)
- }
- const point = new Point(x, y)
- point.assertValidity()
- return point
- }
- static fromUncompressedHex(bytes) {
- const x = bytesToNumber(bytes.subarray(1, 33))
- const y = bytesToNumber(bytes.subarray(33, 65))
- const point = new Point(x, y)
- point.assertValidity()
- return point
- }
- static fromHex(hex) {
- const bytes = ensureBytes(hex)
- const len = bytes.length
- const header = bytes[0]
- if (len === 32 || (len === 33 && (header === 0x02 || header === 0x03))) {
- return this.fromCompressedHex(bytes)
- }
- if (len === 65 && header === 0x04) return this.fromUncompressedHex(bytes)
- throw new Error(
- `Point.fromHex: received invalid point. Expected 32-33 compressed bytes or 65 uncompressed bytes, not ${len}`
- )
- }
- static fromPrivateKey(privateKey) {
- return Point.BASE.multiply(normalizePrivateKey(privateKey))
- }
- static fromSignature(msgHash, signature, recovery) {
- msgHash = ensureBytes(msgHash)
- const h = truncateHash(msgHash)
- const {r, s} = normalizeSignature(signature)
- if (recovery !== 0 && recovery !== 1) {
- throw new Error('Cannot recover signature: invalid recovery bit')
- }
- const prefix = recovery & 1 ? '03' : '02'
- const R = Point.fromHex(prefix + numTo32bStr(r))
- const {n} = CURVE
- const rinv = invert(r, n)
- const u1 = mod(-h * rinv, n)
- const u2 = mod(s * rinv, n)
- const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2)
- if (!Q) throw new Error('Cannot recover signature: point at infinify')
- Q.assertValidity()
- return Q
- }
- toRawBytes(isCompressed = false) {
- return hexToBytes(this.toHex(isCompressed))
- }
- toHex(isCompressed = false) {
- const x = numTo32bStr(this.x)
- if (isCompressed) {
- const prefix = this.y & _1n ? '03' : '02'
- return `${prefix}${x}`
- } else {
- return `04${x}${numTo32bStr(this.y)}`
- }
- }
- toHexX() {
- return this.toHex(true).slice(2)
- }
- toRawX() {
- return this.toRawBytes(true).slice(1)
- }
- assertValidity() {
- const msg = 'Point is not on elliptic curve'
- const {x, y} = this
- if (!isValidFieldElement(x) || !isValidFieldElement(y))
- throw new Error(msg)
- const left = mod(y * y)
- const right = weistrass(x)
- if (mod(left - right) !== _0n) throw new Error(msg)
- }
- equals(other) {
- return this.x === other.x && this.y === other.y
- }
- negate() {
- return new Point(this.x, mod(-this.y))
- }
- double() {
- return JacobianPoint.fromAffine(this).double().toAffine()
- }
- add(other) {
- return JacobianPoint.fromAffine(this)
- .add(JacobianPoint.fromAffine(other))
- .toAffine()
- }
- subtract(other) {
- return this.add(other.negate())
- }
- multiply(scalar) {
- return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine()
- }
- multiplyAndAddUnsafe(Q, a, b) {
- const P = JacobianPoint.fromAffine(this)
- const aP =
- a === _0n || a === _1n || this !== Point.BASE
- ? P.multiplyUnsafe(a)
- : P.multiply(a)
- const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b)
- const sum = aP.add(bQ)
- return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine()
- }
- }
- Point.BASE = new Point(CURVE.Gx, CURVE.Gy)
- Point.ZERO = new Point(_0n, _0n)
- function sliceDER(s) {
- return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s
- }
- function parseDERInt(data) {
- if (data.length < 2 || data[0] !== 0x02) {
- throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`)
- }
- const len = data[1]
- const res = data.subarray(2, len + 2)
- if (!len || res.length !== len) {
- throw new Error(`Invalid signature integer: wrong length`)
- }
- if (res[0] === 0x00 && res[1] <= 0x7f) {
- throw new Error('Invalid signature integer: trailing length')
- }
- return {data: bytesToNumber(res), left: data.subarray(len + 2)}
- }
- function parseDERSignature(data) {
- if (data.length < 2 || data[0] != 0x30) {
- throw new Error(`Invalid signature tag: ${bytesToHex(data)}`)
- }
- if (data[1] !== data.length - 2) {
- throw new Error('Invalid signature: incorrect length')
- }
- const {data: r, left: sBytes} = parseDERInt(data.subarray(2))
- const {data: s, left: rBytesLeft} = parseDERInt(sBytes)
- if (rBytesLeft.length) {
- throw new Error(
- `Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`
- )
- }
- return {r, s}
- }
- class Signature {
- constructor(r, s) {
- this.r = r
- this.s = s
- this.assertValidity()
- }
- static fromCompact(hex) {
- const arr = isUint8a(hex)
- const name = 'Signature.fromCompact'
- if (typeof hex !== 'string' && !arr)
- throw new TypeError(`${name}: Expected string or Uint8Array`)
- const str = arr ? bytesToHex(hex) : hex
- if (str.length !== 128) throw new Error(`${name}: Expected 64-byte hex`)
- return new Signature(
- hexToNumber(str.slice(0, 64)),
- hexToNumber(str.slice(64, 128))
- )
- }
- static fromDER(hex) {
- const arr = isUint8a(hex)
- if (typeof hex !== 'string' && !arr)
- throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`)
- const {r, s} = parseDERSignature(arr ? hex : hexToBytes(hex))
- return new Signature(r, s)
- }
- static fromHex(hex) {
- return this.fromDER(hex)
- }
- assertValidity() {
- const {r, s} = this
- if (!isWithinCurveOrder(r))
- throw new Error('Invalid Signature: r must be 0 < r < n')
- if (!isWithinCurveOrder(s))
- throw new Error('Invalid Signature: s must be 0 < s < n')
- }
- hasHighS() {
- const HALF = CURVE.n >> _1n
- return this.s > HALF
- }
- normalizeS() {
- return this.hasHighS() ? new Signature(this.r, CURVE.n - this.s) : this
- }
- toDERRawBytes(isCompressed = false) {
- return hexToBytes(this.toDERHex(isCompressed))
- }
- toDERHex(isCompressed = false) {
- const sHex = sliceDER(numberToHexUnpadded(this.s))
- if (isCompressed) return sHex
- const rHex = sliceDER(numberToHexUnpadded(this.r))
- const rLen = numberToHexUnpadded(rHex.length / 2)
- const sLen = numberToHexUnpadded(sHex.length / 2)
- const length = numberToHexUnpadded(rHex.length / 2 + sHex.length / 2 + 4)
- return `30${length}02${rLen}${rHex}02${sLen}${sHex}`
- }
- toRawBytes() {
- return this.toDERRawBytes()
- }
- toHex() {
- return this.toDERHex()
- }
- toCompactRawBytes() {
- return hexToBytes(this.toCompactHex())
- }
- toCompactHex() {
- return numTo32bStr(this.r) + numTo32bStr(this.s)
- }
- }
- function concatBytes(...arrays) {
- if (!arrays.every(isUint8a)) throw new Error('Uint8Array list expected')
- if (arrays.length === 1) return arrays[0]
- const length = arrays.reduce((a, arr) => a + arr.length, 0)
- const result = new Uint8Array(length)
- for (let i = 0, pad = 0; i < arrays.length; i++) {
- const arr = arrays[i]
- result.set(arr, pad)
- pad += arr.length
- }
- return result
- }
- function isUint8a(bytes) {
- return bytes instanceof Uint8Array
- }
- const hexes = Array.from({length: 256}, (v, i) =>
- i.toString(16).padStart(2, '0')
- )
- function bytesToHex(uint8a) {
- if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array')
- let hex = ''
- for (let i = 0; i < uint8a.length; i++) {
- hex += hexes[uint8a[i]]
- }
- return hex
- }
- function numTo32bStr(num) {
- if (num > POW_2_256) throw new Error('Expected number < 2^256')
- return num.toString(16).padStart(64, '0')
- }
- function numTo32b(num) {
- return hexToBytes(numTo32bStr(num))
- }
- function numberToHexUnpadded(num) {
- const hex = num.toString(16)
- return hex.length & 1 ? `0${hex}` : hex
- }
- function hexToNumber(hex) {
- if (typeof hex !== 'string') {
- throw new TypeError('hexToNumber: expected string, got ' + typeof hex)
- }
- return BigInt(`0x${hex}`)
- }
- function hexToBytes(hex) {
- if (typeof hex !== 'string') {
- throw new TypeError('hexToBytes: expected string, got ' + typeof hex)
- }
- if (hex.length % 2)
- throw new Error('hexToBytes: received invalid unpadded hex' + hex.length)
- const array = new Uint8Array(hex.length / 2)
- for (let i = 0; i < array.length; i++) {
- const j = i * 2
- const hexByte = hex.slice(j, j + 2)
- const byte = Number.parseInt(hexByte, 16)
- if (Number.isNaN(byte) || byte < 0)
- throw new Error('Invalid byte sequence')
- array[i] = byte
- }
- return array
- }
- function bytesToNumber(bytes) {
- return hexToNumber(bytesToHex(bytes))
- }
- function ensureBytes(hex) {
- return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex)
- }
- function normalizeScalar(num) {
- if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0)
- return BigInt(num)
- if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num
- throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n')
- }
- function mod(a, b = CURVE.P) {
- const result = a % b
- return result >= _0n ? result : b + result
- }
- function pow2(x, power) {
- const {P} = CURVE
- let res = x
- while (power-- > _0n) {
- res *= res
- res %= P
- }
- return res
- }
- function sqrtMod(x) {
- const {P} = CURVE
- const _6n = BigInt(6)
- const _11n = BigInt(11)
- const _22n = BigInt(22)
- const _23n = BigInt(23)
- const _44n = BigInt(44)
- const _88n = BigInt(88)
- const b2 = (x * x * x) % P
- const b3 = (b2 * b2 * x) % P
- const b6 = (pow2(b3, _3n) * b3) % P
- const b9 = (pow2(b6, _3n) * b3) % P
- const b11 = (pow2(b9, _2n) * b2) % P
- const b22 = (pow2(b11, _11n) * b11) % P
- const b44 = (pow2(b22, _22n) * b22) % P
- const b88 = (pow2(b44, _44n) * b44) % P
- const b176 = (pow2(b88, _88n) * b88) % P
- const b220 = (pow2(b176, _44n) * b44) % P
- const b223 = (pow2(b220, _3n) * b3) % P
- const t1 = (pow2(b223, _23n) * b22) % P
- const t2 = (pow2(t1, _6n) * b2) % P
- return pow2(t2, _2n)
- }
- function invert(number, modulo = CURVE.P) {
- if (number === _0n || modulo <= _0n) {
- throw new Error(
- `invert: expected positive integers, got n=${number} mod=${modulo}`
- )
- }
- let a = mod(number, modulo)
- let b = modulo
- let x = _0n,
- u = _1n
- while (a !== _0n) {
- const q = b / a
- const r = b % a
- const m = x - u * q
- ;(b = a), (a = r), (x = u), (u = m)
- }
- const gcd = b
- if (gcd !== _1n) throw new Error('invert: does not exist')
- return mod(x, modulo)
- }
- function invertBatch(nums, p = CURVE.P) {
- const scratch = new Array(nums.length)
- const lastMultiplied = nums.reduce((acc, num, i) => {
- if (num === _0n) return acc
- scratch[i] = acc
- return mod(acc * num, p)
- }, _1n)
- const inverted = invert(lastMultiplied, p)
- nums.reduceRight((acc, num, i) => {
- if (num === _0n) return acc
- scratch[i] = mod(acc * scratch[i], p)
- return mod(acc * num, p)
- }, inverted)
- return scratch
- }
- const divNearest = (a, b) => (a + b / _2n) / b
- const POW_2_128 = _2n ** BigInt(128)
- function splitScalarEndo(k) {
- const {n} = CURVE
- const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15')
- const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3')
- const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8')
- const b2 = a1
- const c1 = divNearest(b2 * k, n)
- const c2 = divNearest(-b1 * k, n)
- let k1 = mod(k - c1 * a1 - c2 * a2, n)
- let k2 = mod(-c1 * b1 - c2 * b2, n)
- const k1neg = k1 > POW_2_128
- const k2neg = k2 > POW_2_128
- if (k1neg) k1 = n - k1
- if (k2neg) k2 = n - k2
- if (k1 > POW_2_128 || k2 > POW_2_128) {
- throw new Error('splitScalarEndo: Endomorphism failed, k=' + k)
- }
- return {k1neg, k1, k2neg, k2}
- }
- function truncateHash(hash) {
- const {n} = CURVE
- const byteLength = hash.length
- const delta = byteLength * 8 - 256
- let h = bytesToNumber(hash)
- if (delta > 0) h = h >> BigInt(delta)
- if (h >= n) h -= n
- return h
- }
- class HmacDrbg {
- constructor() {
- this.v = new Uint8Array(32).fill(1)
- this.k = new Uint8Array(32).fill(0)
- this.counter = 0
- }
- hmac(...values) {
- return utils.hmacSha256(this.k, ...values)
- }
- hmacSync(...values) {
- if (typeof utils.hmacSha256Sync !== 'function')
- throw new Error('utils.hmacSha256Sync is undefined, you need to set it')
- const res = utils.hmacSha256Sync(this.k, ...values)
- if (res instanceof Promise)
- throw new Error('To use sync sign(), ensure utils.hmacSha256 is sync')
- return res
- }
- incr() {
- if (this.counter >= 1000) {
- throw new Error('Tried 1,000 k values for sign(), all were invalid')
- }
- this.counter += 1
- }
- async reseed(seed = new Uint8Array()) {
- this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed)
- this.v = await this.hmac(this.v)
- if (seed.length === 0) return
- this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed)
- this.v = await this.hmac(this.v)
- }
- reseedSync(seed = new Uint8Array()) {
- this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed)
- this.v = this.hmacSync(this.v)
- if (seed.length === 0) return
- this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed)
- this.v = this.hmacSync(this.v)
- }
- async generate() {
- this.incr()
- this.v = await this.hmac(this.v)
- return this.v
- }
- generateSync() {
- this.incr()
- this.v = this.hmacSync(this.v)
- return this.v
- }
- }
- function isWithinCurveOrder(num) {
- return _0n < num && num < CURVE.n
- }
- function isValidFieldElement(num) {
- return _0n < num && num < CURVE.P
- }
- function kmdToSig(kBytes, m, d) {
- const k = bytesToNumber(kBytes)
- if (!isWithinCurveOrder(k)) return
- const {n} = CURVE
- const q = Point.BASE.multiply(k)
- const r = mod(q.x, n)
- if (r === _0n) return
- const s = mod(invert(k, n) * mod(m + d * r, n), n)
- if (s === _0n) return
- const sig = new Signature(r, s)
- const recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n)
- return {sig, recovery}
- }
- function normalizePrivateKey(key) {
- let num
- if (typeof key === 'bigint') {
- num = key
- } else if (
- typeof key === 'number' &&
- Number.isSafeInteger(key) &&
- key > 0
- ) {
- num = BigInt(key)
- } else if (typeof key === 'string') {
- if (key.length !== 64) throw new Error('Expected 32 bytes of private key')
- num = hexToNumber(key)
- } else if (isUint8a(key)) {
- if (key.length !== 32) throw new Error('Expected 32 bytes of private key')
- num = bytesToNumber(key)
- } else {
- throw new TypeError('Expected valid private key')
- }
- if (!isWithinCurveOrder(num))
- throw new Error('Expected private key: 0 < key < n')
- return num
- }
- function normalizePublicKey(publicKey) {
- if (publicKey instanceof Point) {
- publicKey.assertValidity()
- return publicKey
- } else {
- return Point.fromHex(publicKey)
- }
- }
- function normalizeSignature(signature) {
- if (signature instanceof Signature) {
- signature.assertValidity()
- return signature
- }
- try {
- return Signature.fromDER(signature)
- } catch (error) {
- return Signature.fromCompact(signature)
- }
- }
- function getPublicKey(privateKey, isCompressed = false) {
- return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed)
- }
- function recoverPublicKey(
- msgHash,
- signature,
- recovery,
- isCompressed = false
- ) {
- return Point.fromSignature(msgHash, signature, recovery).toRawBytes(
- isCompressed
- )
- }
- function isPub(item) {
- const arr = isUint8a(item)
- const str = typeof item === 'string'
- const len = (arr || str) && item.length
- if (arr) return len === 33 || len === 65
- if (str) return len === 66 || len === 130
- if (item instanceof Point) return true
- return false
- }
- function getSharedSecret(privateA, publicB, isCompressed = false) {
- if (isPub(privateA))
- throw new TypeError('getSharedSecret: first arg must be private key')
- if (!isPub(publicB))
- throw new TypeError('getSharedSecret: second arg must be public key')
- const b = normalizePublicKey(publicB)
- b.assertValidity()
- return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed)
- }
- function bits2int(bytes) {
- const slice = bytes.length > 32 ? bytes.slice(0, 32) : bytes
- return bytesToNumber(slice)
- }
- function bits2octets(bytes) {
- const z1 = bits2int(bytes)
- const z2 = mod(z1, CURVE.n)
- return int2octets(z2 < _0n ? z1 : z2)
- }
- function int2octets(num) {
- if (typeof num !== 'bigint') throw new Error('Expected bigint')
- const hex = numTo32bStr(num)
- return hexToBytes(hex)
- }
- function initSigArgs(msgHash, privateKey, extraEntropy) {
- if (msgHash == null)
- throw new Error(`sign: expected valid message hash, not "${msgHash}"`)
- const h1 = ensureBytes(msgHash)
- const d = normalizePrivateKey(privateKey)
- const seedArgs = [int2octets(d), bits2octets(h1)]
- if (extraEntropy != null) {
- if (extraEntropy === true) extraEntropy = utils.randomBytes(32)
- const e = ensureBytes(extraEntropy)
- if (e.length !== 32)
- throw new Error('sign: Expected 32 bytes of extra data')
- seedArgs.push(e)
- }
- const seed = concatBytes(...seedArgs)
- const m = bits2int(h1)
- return {seed, m, d}
- }
- function finalizeSig(recSig, opts) {
- let {sig, recovery} = recSig
- const {canonical, der, recovered} = Object.assign(
- {canonical: true, der: true},
- opts
- )
- if (canonical && sig.hasHighS()) {
- sig = sig.normalizeS()
- recovery ^= 1
- }
- const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes()
- return recovered ? [hashed, recovery] : hashed
- }
- async function sign(msgHash, privKey, opts = {}) {
- const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy)
- let sig
- const drbg = new HmacDrbg()
- await drbg.reseed(seed)
- while (!(sig = kmdToSig(await drbg.generate(), m, d))) await drbg.reseed()
- return finalizeSig(sig, opts)
- }
- function signSync(msgHash, privKey, opts = {}) {
- const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy)
- let sig
- const drbg = new HmacDrbg()
- drbg.reseedSync(seed)
- while (!(sig = kmdToSig(drbg.generateSync(), m, d))) drbg.reseedSync()
- return finalizeSig(sig, opts)
- }
- const vopts = {strict: true}
- function verify(signature, msgHash, publicKey, opts = vopts) {
- let sig
- try {
- sig = normalizeSignature(signature)
- msgHash = ensureBytes(msgHash)
- } catch (error) {
- return false
- }
- const {r, s} = sig
- if (opts.strict && sig.hasHighS()) return false
- const h = truncateHash(msgHash)
- let P
- try {
- P = normalizePublicKey(publicKey)
- } catch (error) {
- return false
- }
- const {n} = CURVE
- const sinv = invert(s, n)
- const u1 = mod(h * sinv, n)
- const u2 = mod(r * sinv, n)
- const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2)
- if (!R) return false
- const v = mod(R.x, n)
- return v === r
- }
- function finalizeSchnorrChallenge(ch) {
- return mod(bytesToNumber(ch), CURVE.n)
- }
- function hasEvenY(point) {
- return (point.y & _1n) === _0n
- }
- class SchnorrSignature {
- constructor(r, s) {
- this.r = r
- this.s = s
- this.assertValidity()
- }
- static fromHex(hex) {
- const bytes = ensureBytes(hex)
- if (bytes.length !== 64)
- throw new TypeError(
- `SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`
- )
- const r = bytesToNumber(bytes.subarray(0, 32))
- const s = bytesToNumber(bytes.subarray(32, 64))
- return new SchnorrSignature(r, s)
- }
- assertValidity() {
- const {r, s} = this
- if (!isValidFieldElement(r) || !isWithinCurveOrder(s))
- throw new Error('Invalid signature')
- }
- toHex() {
- return numTo32bStr(this.r) + numTo32bStr(this.s)
- }
- toRawBytes() {
- return hexToBytes(this.toHex())
- }
- }
- function schnorrGetPublicKey(privateKey) {
- return Point.fromPrivateKey(privateKey).toRawX()
- }
- function initSchnorrSigArgs(message, privateKey, auxRand) {
- if (message == null)
- throw new TypeError(`sign: Expected valid message, not "${message}"`)
- const m = ensureBytes(message)
- const d0 = normalizePrivateKey(privateKey)
- const rand = ensureBytes(auxRand)
- if (rand.length !== 32)
- throw new TypeError('sign: Expected 32 bytes of aux randomness')
- const P = Point.fromPrivateKey(d0)
- const px = P.toRawX()
- const d = hasEvenY(P) ? d0 : CURVE.n - d0
- return {m, P, px, d, rand}
- }
- function initSchnorrNonce(d, t0h) {
- return numTo32b(d ^ bytesToNumber(t0h))
- }
- function finalizeSchnorrNonce(k0h) {
- const k0 = mod(bytesToNumber(k0h), CURVE.n)
- if (k0 === _0n)
- throw new Error('sign: Creation of signature failed. k is zero')
- const R = Point.fromPrivateKey(k0)
- const rx = R.toRawX()
- const k = hasEvenY(R) ? k0 : CURVE.n - k0
- return {R, rx, k}
- }
- function finalizeSchnorrSig(R, k, e, d) {
- return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes()
- }
- async function schnorrSign(
- message,
- privateKey,
- auxRand = utils.randomBytes()
- ) {
- const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand)
- const t = initSchnorrNonce(d, await utils.taggedHash(TAGS.aux, rand))
- const {R, rx, k} = finalizeSchnorrNonce(
- await utils.taggedHash(TAGS.nonce, t, px, m)
- )
- const e = finalizeSchnorrChallenge(
- await utils.taggedHash(TAGS.challenge, rx, px, m)
- )
- const sig = finalizeSchnorrSig(R, k, e, d)
- const isValid = await schnorrVerify(sig, m, px)
- if (!isValid) throw new Error('sign: Invalid signature produced')
- return sig
- }
- function schnorrSignSync(message, privateKey, auxRand = utils.randomBytes()) {
- const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand)
- const t = initSchnorrNonce(d, utils.taggedHashSync(TAGS.aux, rand))
- const {R, rx, k} = finalizeSchnorrNonce(
- utils.taggedHashSync(TAGS.nonce, t, px, m)
- )
- const e = finalizeSchnorrChallenge(
- utils.taggedHashSync(TAGS.challenge, rx, px, m)
- )
- const sig = finalizeSchnorrSig(R, k, e, d)
- const isValid = schnorrVerifySync(sig, m, px)
- if (!isValid) throw new Error('sign: Invalid signature produced')
- return sig
- }
- function initSchnorrVerify(signature, message, publicKey) {
- const raw = signature instanceof SchnorrSignature
- const sig = raw ? signature : SchnorrSignature.fromHex(signature)
- if (raw) sig.assertValidity()
- return {
- ...sig,
- m: ensureBytes(message),
- P: normalizePublicKey(publicKey)
- }
- }
- function finalizeSchnorrVerify(r, P, s, e) {
- const R = Point.BASE.multiplyAndAddUnsafe(
- P,
- normalizePrivateKey(s),
- mod(-e, CURVE.n)
- )
- if (!R || !hasEvenY(R) || R.x !== r) return false
- return true
- }
- async function schnorrVerify(signature, message, publicKey) {
- try {
- const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey)
- const e = finalizeSchnorrChallenge(
- await utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m)
- )
- return finalizeSchnorrVerify(r, P, s, e)
- } catch (error) {
- return false
- }
- }
- function schnorrVerifySync(signature, message, publicKey) {
- try {
- const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey)
- const e = finalizeSchnorrChallenge(
- utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m)
- )
- return finalizeSchnorrVerify(r, P, s, e)
- } catch (error) {
- return false
- }
- }
- const schnorr = {
- Signature: SchnorrSignature,
- getPublicKey: schnorrGetPublicKey,
- sign: schnorrSign,
- verify: schnorrVerify,
- signSync: schnorrSignSync,
- verifySync: schnorrVerifySync
- }
- Point.BASE._setWindowSize(8)
- const crypto = {
- node: nodeCrypto,
- web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined
- }
- const TAGS = {
- challenge: 'BIP0340/challenge',
- aux: 'BIP0340/aux',
- nonce: 'BIP0340/nonce'
- }
- const TAGGED_HASH_PREFIXES = {}
- const utils = {
- isValidPrivateKey(privateKey) {
- try {
- normalizePrivateKey(privateKey)
- return true
- } catch (error) {
- return false
- }
- },
- privateAdd: (privateKey, tweak) => {
- const p = normalizePrivateKey(privateKey)
- const t = normalizePrivateKey(tweak)
- return numTo32b(mod(p + t, CURVE.n))
- },
- privateNegate: privateKey => {
- const p = normalizePrivateKey(privateKey)
- return numTo32b(CURVE.n - p)
- },
- pointAddScalar: (p, tweak, isCompressed) => {
- const P = Point.fromHex(p)
- const t = normalizePrivateKey(tweak)
- const Q = Point.BASE.multiplyAndAddUnsafe(P, t, _1n)
- if (!Q) throw new Error('Tweaked point at infinity')
- return Q.toRawBytes(isCompressed)
- },
- pointMultiply: (p, tweak, isCompressed) => {
- const P = Point.fromHex(p)
- const t = bytesToNumber(ensureBytes(tweak))
- return P.multiply(t).toRawBytes(isCompressed)
- },
- hashToPrivateKey: hash => {
- hash = ensureBytes(hash)
- if (hash.length < 40 || hash.length > 1024)
- throw new Error('Expected 40-1024 bytes of private key as per FIPS 186')
- const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n
- return numTo32b(num)
- },
- randomBytes: (bytesLength = 32) => {
- if (crypto.web) {
- return crypto.web.getRandomValues(new Uint8Array(bytesLength))
- } else if (crypto.node) {
- const {randomBytes} = crypto.node
- return Uint8Array.from(randomBytes(bytesLength))
- } else {
- throw new Error("The environment doesn't have randomBytes function")
- }
- },
- randomPrivateKey: () => {
- return utils.hashToPrivateKey(utils.randomBytes(40))
- },
- bytesToHex,
- hexToBytes,
- concatBytes,
- mod,
- invert,
- sha256: async (...messages) => {
- if (crypto.web) {
- const buffer = await crypto.web.subtle.digest(
- 'SHA-256',
- concatBytes(...messages)
- )
- return new Uint8Array(buffer)
- } else if (crypto.node) {
- const {createHash} = crypto.node
- const hash = createHash('sha256')
- messages.forEach(m => hash.update(m))
- return Uint8Array.from(hash.digest())
- } else {
- throw new Error("The environment doesn't have sha256 function")
- }
- },
- hmacSha256: async (key, ...messages) => {
- if (crypto.web) {
- const ckey = await crypto.web.subtle.importKey(
- 'raw',
- key,
- {name: 'HMAC', hash: {name: 'SHA-256'}},
- false,
- ['sign']
- )
- const message = concatBytes(...messages)
- const buffer = await crypto.web.subtle.sign('HMAC', ckey, message)
- return new Uint8Array(buffer)
- } else if (crypto.node) {
- const {createHmac} = crypto.node
- const hash = createHmac('sha256', key)
- messages.forEach(m => hash.update(m))
- return Uint8Array.from(hash.digest())
- } else {
- throw new Error("The environment doesn't have hmac-sha256 function")
- }
- },
- sha256Sync: undefined,
- hmacSha256Sync: undefined,
- taggedHash: async (tag, ...messages) => {
- let tagP = TAGGED_HASH_PREFIXES[tag]
- if (tagP === undefined) {
- const tagH = await utils.sha256(
- Uint8Array.from(tag, c => c.charCodeAt(0))
- )
- tagP = concatBytes(tagH, tagH)
- TAGGED_HASH_PREFIXES[tag] = tagP
- }
- return utils.sha256(tagP, ...messages)
- },
- taggedHashSync: (tag, ...messages) => {
- if (typeof utils.sha256Sync !== 'function')
- throw new Error('utils.sha256Sync is undefined, you need to set it')
- let tagP = TAGGED_HASH_PREFIXES[tag]
- if (tagP === undefined) {
- const tagH = utils.sha256Sync(
- Uint8Array.from(tag, c => c.charCodeAt(0))
- )
- tagP = concatBytes(tagH, tagH)
- TAGGED_HASH_PREFIXES[tag] = tagP
- }
- return utils.sha256Sync(tagP, ...messages)
- },
- precompute(windowSize = 8, point = Point.BASE) {
- const cached = point === Point.BASE ? point : new Point(point.x, point.y)
- cached._setWindowSize(windowSize)
- cached.multiply(_3n)
- return cached
- }
- }
-
- exports.CURVE = CURVE
- exports.Point = Point
- exports.Signature = Signature
- exports.getPublicKey = getPublicKey
- exports.getSharedSecret = getSharedSecret
- exports.recoverPublicKey = recoverPublicKey
- exports.schnorr = schnorr
- exports.sign = sign
- exports.signSync = signSync
- exports.utils = utils
- exports.verify = verify
-
- Object.defineProperty(exports, '__esModule', {value: true})
-})
diff --git a/lnbits/extensions/cashu/static/js/utils.js b/lnbits/extensions/cashu/static/js/utils.js
deleted file mode 100644
index cf852b58..00000000
--- a/lnbits/extensions/cashu/static/js/utils.js
+++ /dev/null
@@ -1,23 +0,0 @@
-function splitAmount(value) {
- const chunks = []
- for (let i = 0; i < 32; i++) {
- const mask = 1 << i
- if ((value & mask) !== 0) chunks.push(Math.pow(2, i))
- }
- return chunks
-}
-
-function bytesToNumber(bytes) {
- return hexToNumber(nobleSecp256k1.utils.bytesToHex(bytes))
-}
-
-function bigIntStringify(key, value) {
- return typeof value === 'bigint' ? value.toString() : value
-}
-
-function hexToNumber(hex) {
- if (typeof hex !== 'string') {
- throw new TypeError('hexToNumber: expected string, got ' + typeof hex)
- }
- return BigInt(`0x${hex}`)
-}
diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py
deleted file mode 100644
index 982d3ac1..00000000
--- a/lnbits/extensions/cashu/tasks.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import asyncio
-
-from cashu.core.migrations import migrate_databases
-from cashu.mint import migrations
-
-from lnbits.core.models import Payment
-from lnbits.tasks import register_invoice_listener
-
-from . import db, ledger
-
-
-async def startup_cashu_mint():
- await migrate_databases(db, migrations)
- await ledger.load_used_proofs()
- await ledger.init_keysets(autosave=False)
-
-
-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") != "cashu":
- return
-
- return
diff --git a/lnbits/extensions/cashu/templates/cashu/_api_docs.html b/lnbits/extensions/cashu/templates/cashu/_api_docs.html
deleted file mode 100644
index f7bb19f6..00000000
--- a/lnbits/extensions/cashu/templates/cashu/_api_docs.html
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
diff --git a/lnbits/extensions/cashu/templates/cashu/_cashu.html b/lnbits/extensions/cashu/templates/cashu/_cashu.html
deleted file mode 100644
index 370e9eb4..00000000
--- a/lnbits/extensions/cashu/templates/cashu/_cashu.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- Create Cashu ecash mints and wallets.
- Created by
- arcbtc ,
- vlad ,
- calle .
-
-
-
diff --git a/lnbits/extensions/cashu/templates/cashu/index.html b/lnbits/extensions/cashu/templates/cashu/index.html
deleted file mode 100644
index 2599669c..00000000
--- a/lnbits/extensions/cashu/templates/cashu/index.html
+++ /dev/null
@@ -1,367 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- Cashu mint and wallet
-
-
- Here you can create multiple cashu mints that you can share. Each mint
- can service many users but all ecash tokens of a mint are only valid
- inside that mint and not across different mints. To exchange funds
- between mints, use Lightning payments.
-
- Important
-
-
- If you are the operator of this LNbits instance, make sure to set
- CASHU_PRIVATE_KEY="randomkey" in your configuration file. Do not
- create mints before setting the key and do not change the key once
- set.
-
-
-
-
-
-
-
-
-
Mints
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
- Shareable wallet
-
- Shareable mint page
-
-
- {{ (col.name == 'tip_options' && col.value ?
- JSON.parse(col.value).join(", ") : col.value) }}
-
-
-
-
-
-
- {% endraw %}
-
- New Mint
-
-
-
-
-
-
-
- {{SITE_TITLE}} Cashu extension
-
-
-
-
- {% include "cashu/_api_docs.html" %}
-
- {% include "cashu/_cashu.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
- Create Mint
-
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/cashu/templates/cashu/mint.html b/lnbits/extensions/cashu/templates/cashu/mint.html
deleted file mode 100644
index a6959ec2..00000000
--- a/lnbits/extensions/cashu/templates/cashu/mint.html
+++ /dev/null
@@ -1,92 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
-
- {{ mint_name }}
-
- click to open wallet
-
-
-
-
-
- Read the following carefully!
-
- This is a
- Cashu
- mint. Cashu is an ecash system for Bitcoin.
-
-
- Open this page in your native browser
- Before you continue to the wallet, make sure to open this page in your
- device's native browser application (Safari for iOS, Chrome for
- Android). Do not use Cashu in an embedded browser that opens when you
- click a link in a messenger.
-
-
- Add wallet to home screen
- You can add Cashu to your home screen as a progressive web app (PWA).
- After opening the wallet in your browser (click the link above), on
- Android (Chrome), click the menu at the upper right. On iOS (Safari),
- click the share button. Now press the Add to Home screen button.
-
-
- Backup your wallet
- Ecash is a bearer asset. That means losing access to your wallet will
- make you lose your funds. The wallet stores ecash tokens on your
- device's database. If you lose the link or delete your your data
- without backing up, you will lose your tokens. Press the Backup button
- in the wallet to download a copy of your tokens.
-
-
- This service is in BETA
- Cashu is still experimental and in active development. There are
- likely bugs in this implementation so please use this with caution. We
- hold no responsibility for people losing access to funds. Use at your
- own risk!
-
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/cashu/templates/cashu/wallet.html b/lnbits/extensions/cashu/templates/cashu/wallet.html
deleted file mode 100644
index 9638f347..00000000
--- a/lnbits/extensions/cashu/templates/cashu/wallet.html
+++ /dev/null
@@ -1,2992 +0,0 @@
-{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu wallet {%
-endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
-page_container %}
-
-
-
-
-
-
-
-
-
-
-
- {% raw %} {{ getBalance() }}
- {{tickerShort}} {% endraw %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Get Ecash
-
-
-
-
- Send Ecash
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mints
- You can connect your wallet to multiple Cashu mints.
- Enter a mint URL and select the mint your want to
- use.
-
-
-
-
-
- {% raw %}
-
-
-
-
-
-
-
-
- {{mint.url}}
-
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Multimint Swaps
- Swap funds from one mint to another via Lightning.
- Warning: this feature is still experimental and could
- behave in unexpected ways!
-
-
-
-
-
-
-
-
-
-
- Swap
-
-
-
-
-
-
-
-
- {% raw %}
-
-
-
- {{props.row.value}}
-
-
- {{props.row.count}}
-
-
- {{props.row.sum}}
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
- {% raw %}
-
-
-
-
-
- Pending
-
-
- Check status
-
-
-
- Received
- Paid
-
-
-
-
- {{props.row.amount}}
-
-
-
- {{props.row.date}}
-
-
-
-
- {{shortenString(props.row.bolt11, 20, 10)}}
- Click to copy
-
-
-
-
- {{props.row.hash}}
-
-
-
- {{props.row.mint}}
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
- {% raw %}
-
-
-
-
-
- Pending
-
-
- Check status
-
-
-
- Received
- Paid
-
-
-
-
- {{props.row.amount}}
-
-
- {{props.row.date}}
-
-
-
-
- {{shortenString(props.row.token, 10, 40)}}
- Click to copy
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- Create invoice
-
-
-
-
-
- Warning
- Download wallet backup
- Install Install Cashu
-
-
-
- Pay invoice
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% raw %}
-
- Pay {{ payInvoiceData.invoice.fsat }}{% endraw %}
- {{LNBITS_DENOMINATION}} {% raw %}
-
-
-
- Description: {{
- payInvoiceData.invoice.description }}
- Expire date: {{ payInvoiceData.invoice.expireDate
- }}
- Hash: {{ payInvoiceData.invoice.hash }}
-
- {% endraw %}
-
-
- Cancel
-
-
- Not enough funds!
- Cancel
-
-
-
-
- {% raw %}
-
-
- {{ payInvoiceData.domain }} is requesting {{
- payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} {%
- endraw %} {{LNBITS_DENOMINATION}} {% raw %}
-
-
-
- {{ payInvoiceData.lnurlpay.targetUser ||
- payInvoiceData.domain }}
- is requesting
- between
- {{ payInvoiceData.lnurlpay.minSendable | msatoshiFormat }}
- and
- {{ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }}
- {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
-
-
-
-
-
- {{ payInvoiceData.lnurlpay.description }}
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
- Send
- Cancel
-
-
-
-
-
-
-
-
- Enter
-
-
- Close
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
- Cashu
- wallet
-
-
- Please take a moment to read the following information.
-
-
- Open this wallet on your device's native browser
- Cashu stores your ecash on your device locally. For the best
- experience, use this wallet with your device's native web browser
- (for example Safari for iOS, Chrome for Android).
-
-
- Add to home screen.
- Add Cashu to your home screen as a progressive web app (PWA). On
- Android Chrome, click the hamburger menu at the upper right. On
- iOS Safari, click the share button. Now press the Add to Home
- screen button.
-
-
- This software is in BETA! We hold no
- responsibility for people losing access to funds. Use at your own
- risk! Ecash is a bearer asset, meaning losing access to this
- wallet will mean you will lose the funds. This wallet stores ecash
- tokens in its database. If you lose the link or delete your your
- data without backing up, you will lose your tokens. Press the
- Backup button to download a copy of your tokens.
-
-
- Install Cashu
- Copy URL
- Continue
-
-
-
-
-
-
-
- Warning
-
- Bookmark this page and backup your tokens!
- Ecash is a bearer asset, meaning losing access to this wallet will
- mean you will lose the funds. This wallet stores ecash tokens in its
- database. If you lose the link or delete your your data without
- backing up, you will lose your tokens. Press the Backup button to
- download a copy of your tokens.
-
-
- Add to home screen.
- You can add Cashu to your home screen as a progressive web app
- (PWA). On Android Chrome, click the hamburger menu at the upper
- right. On iOS Safari, click the share button. Now press the Add to
- Home screen button.
-
-
- This software is in BETA! We hold no responsibility
- for people losing access to funds. Use at your own risk!
-
-
- Copy wallet URL
- I understand
-
-
-
-
-
-
-
-
-
- Create a Lightning invoice
-
-
-
-
-
-
-
- Copy invoice
- Create Invoice
- Close
-
-
-
-
-
-
-
-
-
- How much would you like to send?
-
-
-
-
-
-
-
-
Send Tokens
-
-
- Copy token
- Copy link
-
-
-
Close
-
-
-
-
-
-
-
-
-
- Receive Cashu tokens
-
-
-
-
-
-
- Receive
-
- Close
-
-
-
-
-
- Do you trust this mint?
-
- A Cashu mint does not know about your financial activity but it
- controls your funds. Make sure that you trust the operator of this
- mint.
-
-
-
- Add mint
- Cancel
-
-
-
-
-
-
-{% endblock %} {% block styles %}
-
-{% endblock %} {% block scripts %}
-
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/cashu/views.py b/lnbits/extensions/cashu/views.py
deleted file mode 100644
index 09d314c3..00000000
--- a/lnbits/extensions/cashu/views.py
+++ /dev/null
@@ -1,253 +0,0 @@
-from http import HTTPStatus
-from typing import Optional
-
-from fastapi import Depends, Request
-from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
-from starlette.responses import HTMLResponse
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import cashu_ext, cashu_renderer
-from .crud import get_cashu
-
-templates = Jinja2Templates(directory="templates")
-
-
-@cashu_ext.get("/", response_class=HTMLResponse)
-async def index(
- request: Request,
- user: User = Depends(check_user_exists),
-):
- return cashu_renderer().TemplateResponse(
- "cashu/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@cashu_ext.get("/wallet")
-async def wallet(request: Request, mint_id: Optional[str] = None):
- if mint_id is not None:
- cashu = await get_cashu(mint_id)
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
- manifest_url = f"/cashu/manifest/{mint_id}.webmanifest"
- mint_name = cashu.name
- else:
- manifest_url = "/cashu/cashu.webmanifest"
- mint_name = "Cashu mint"
-
- return cashu_renderer().TemplateResponse(
- "cashu/wallet.html",
- {
- "request": request,
- "web_manifest": manifest_url,
- "mint_name": mint_name,
- },
- )
-
-
-@cashu_ext.get("/mint/{mintID}")
-async def cashu(request: Request, mintID):
- cashu = await get_cashu(mintID)
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
- return cashu_renderer().TemplateResponse(
- "cashu/mint.html",
- {"request": request, "mint_id": mintID},
- )
-
-
-@cashu_ext.get("/manifest/{cashu_id}.webmanifest")
-async def manifest_lnbits(cashu_id: str):
- cashu = await get_cashu(cashu_id)
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
-
- return get_manifest(cashu_id, cashu.name)
-
-
-@cashu_ext.get("/cashu.webmanifest")
-async def manifest():
- return get_manifest()
-
-
-def get_manifest(mint_id: Optional[str] = None, mint_name: Optional[str] = None):
- manifest_name = "Cashu"
- if mint_name:
- manifest_name += " - " + mint_name
- manifest_url = "/cashu/wallet"
- if mint_id:
- manifest_url += "?mint_id=" + mint_id
-
- return {
- "short_name": "Cashu",
- "name": manifest_name,
- "icons": [
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png",
- "type": "image/png",
- "sizes": "512x512",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96x96.png",
- "type": "image/png",
- "sizes": "96x96",
- },
- ],
- "id": manifest_url,
- "start_url": manifest_url,
- "background_color": "#1F2234",
- "description": "Cashu ecash wallet",
- "display": "standalone",
- "scope": "/cashu/",
- "theme_color": "#1F2234",
- "protocol_handlers": [
- {"protocol": "web+cashu", "url": "&recv_token=%s"},
- {"protocol": "web+lightning", "url": "&lightning=%s"},
- ],
- "shortcuts": [
- {
- "name": manifest_name,
- "short_name": "Cashu",
- "description": manifest_name,
- "url": manifest_url,
- "icons": [
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png",
- "sizes": "512x512",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png",
- "sizes": "192x192",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png",
- "sizes": "144x144",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96x96.png",
- "sizes": "96x96",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png",
- "sizes": "72x72",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/48x48.png",
- "sizes": "48x48",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/16x16.png",
- "sizes": "16x16",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/20x20.png",
- "sizes": "20x20",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/29x29.png",
- "sizes": "29x29",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/32x32.png",
- "sizes": "32x32",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/40x40.png",
- "sizes": "40x40",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/50x50.png",
- "sizes": "50x50",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/57x57.png",
- "sizes": "57x57",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/58x58.png",
- "sizes": "58x58",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/60x60.png",
- "sizes": "60x60",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/64x64.png",
- "sizes": "64x64",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png",
- "sizes": "72x72",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/76x76.png",
- "sizes": "76x76",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/80x80.png",
- "sizes": "80x80",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/87x87.png",
- "sizes": "87x87",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/100x100.png",
- "sizes": "100x100",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/114x114.png",
- "sizes": "114x114",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/120x120.png",
- "sizes": "120x120",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/128x128.png",
- "sizes": "128x128",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png",
- "sizes": "144x144",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/152x152.png",
- "sizes": "152x152",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/167x167.png",
- "sizes": "167x167",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/180x180.png",
- "sizes": "180x180",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png",
- "sizes": "192x192",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/256x256.png",
- "sizes": "256x256",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png",
- "sizes": "512x512",
- },
- {
- "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/1024x1024.png",
- "sizes": "1024x1024",
- },
- ],
- }
- ],
- }
diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py
deleted file mode 100644
index 682412a4..00000000
--- a/lnbits/extensions/cashu/views_api.py
+++ /dev/null
@@ -1,440 +0,0 @@
-import math
-from http import HTTPStatus
-from typing import Dict, Union
-
-# -------- cashu imports
-from cashu.core.base import (
- CheckFeesRequest,
- CheckFeesResponse,
- CheckSpendableRequest,
- CheckSpendableResponse,
- GetMeltResponse,
- GetMintResponse,
- Invoice,
- PostMeltRequest,
- PostMintRequest,
- PostMintResponse,
- PostSplitRequest,
- PostSplitResponse,
-)
-from fastapi import Depends, Query
-from loguru import logger
-from starlette.exceptions import HTTPException
-
-from lnbits import bolt11
-from lnbits.core.crud import check_internal, get_user
-from lnbits.core.services import (
- check_transaction_status,
- create_invoice,
- fee_reserve,
- pay_invoice,
-)
-from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
-from lnbits.helpers import urlsafe_short_hash
-from lnbits.wallets.base import PaymentStatus
-
-from . import cashu_ext, ledger
-from .crud import create_cashu, delete_cashu, get_cashu, get_cashus
-from .models import Cashu
-
-# --------- extension imports
-
-# WARNING: Do not set this to False in production! This will create
-# tokens for free otherwise. This is for testing purposes only!
-
-LIGHTNING = True
-
-if not LIGHTNING:
- logger.warning(
- "Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
- )
-
-########################################
-############### LNBITS MINTS ###########
-########################################
-
-
-@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
-async def api_cashus(
- all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
-):
- """
- Get all mints of this wallet.
- """
- wallet_ids = [wallet.wallet.id]
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- if user:
- wallet_ids = user.wallet_ids
-
- return [cashu.dict() for cashu in await get_cashus(wallet_ids)]
-
-
-@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
-async def api_cashu_create(
- data: Cashu,
- wallet: WalletTypeInfo = Depends(get_key_type),
-):
- """
- Create a new mint for this wallet.
- """
- cashu_id = urlsafe_short_hash()
- # generate a new keyset in cashu
- keyset = await ledger.load_keyset(cashu_id)
-
- cashu = await create_cashu(
- cashu_id=cashu_id, keyset_id=keyset.id, wallet_id=wallet.wallet.id, data=data
- )
- logger.debug(cashu)
- return cashu.dict()
-
-
-@cashu_ext.delete("/api/v1/mints/{cashu_id}")
-async def api_cashu_delete(
- cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- """
- Delete an existing cashu mint.
- """
- cashu = await get_cashu(cashu_id)
-
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Cashu mint does not exist."
- )
-
- if cashu.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your Cashu mint."
- )
-
- await delete_cashu(cashu_id)
- raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
-
-
-#######################################
-########### CASHU ENDPOINTS ###########
-#######################################
-
-
-@cashu_ext.get("/api/v1/{cashu_id}/keys", status_code=HTTPStatus.OK)
-async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
- """Get the public keys of the mint"""
- cashu: Union[Cashu, None] = await get_cashu(cashu_id)
-
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
-
- return ledger.get_keyset(keyset_id=cashu.keyset_id)
-
-
-@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}")
-async def keyset_keys(
- cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None)
-) -> dict[int, str]:
- """
- Get the public keys of the mint of a specificy keyset id.
- The id is encoded in base64_urlsafe and needs to be converted back to
- normal base64 before it can be processed.
- """
-
- cashu: Union[Cashu, None] = await get_cashu(cashu_id)
-
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
-
- id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
- keyset = ledger.get_keyset(keyset_id=id)
- return keyset
-
-
-@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
-async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
- """Get the public keys of the mint"""
- cashu: Union[Cashu, None] = await get_cashu(cashu_id)
-
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
-
- return {"keysets": [cashu.keyset_id]}
-
-
-@cashu_ext.get("/api/v1/{cashu_id}/mint")
-async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintResponse:
- """
- Request minting of new tokens. The mint responds with a Lightning invoice.
- This endpoint can be used for a Lightning invoice UX flow.
-
- Call `POST /mint` after paying the invoice.
- """
- cashu: Union[Cashu, None] = await get_cashu(cashu_id)
-
- if not cashu:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
-
- # create an invoice that the wallet needs to pay
- try:
- payment_hash, payment_request = await create_invoice(
- wallet_id=cashu.wallet,
- amount=amount,
- memo=f"{cashu.name}",
- extra={"tag": "cashu"},
- )
- invoice = Invoice(
- amount=amount, pr=payment_request, hash=payment_hash, issued=False
- )
- # await store_lightning_invoice(cashu_id, invoice)
- await ledger.crud.store_lightning_invoice(invoice=invoice, db=ledger.db)
- except Exception as e:
- logger.error(e)
- raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
-
- print(f"Lightning invoice: {payment_request}")
- resp = GetMintResponse(pr=payment_request, hash=payment_hash)
- # return {"pr": payment_request, "hash": payment_hash}
- return resp
-
-
-@cashu_ext.post("/api/v1/{cashu_id}/mint")
-async def mint(
- data: PostMintRequest,
- cashu_id: str = Query(None),
- payment_hash: str = Query(None),
-) -> PostMintResponse:
- """
- Requests the minting of tokens belonging to a paid payment request.
- Call this endpoint after `GET /mint`.
- """
- cashu: Union[Cashu, None] = await get_cashu(cashu_id)
- if cashu is None:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
-
- keyset = ledger.keysets.keysets[cashu.keyset_id]
-
- if LIGHTNING:
- invoice: Invoice = await ledger.crud.get_lightning_invoice(
- db=ledger.db, hash=payment_hash
- )
- if invoice is None:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="Mint does not know this invoice.",
- )
- if invoice.issued:
- raise HTTPException(
- status_code=HTTPStatus.PAYMENT_REQUIRED,
- detail="Tokens already issued for this invoice.",
- )
-
- # set this invoice as issued
- await ledger.crud.update_lightning_invoice(
- db=ledger.db, hash=payment_hash, issued=True
- )
-
- status: PaymentStatus = await check_transaction_status(
- cashu.wallet, payment_hash
- )
-
- try:
- total_requested = sum([bm.amount for bm in data.outputs])
- if total_requested > invoice.amount:
- raise HTTPException(
- status_code=HTTPStatus.PAYMENT_REQUIRED,
- detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
- )
-
- if not status.paid:
- raise HTTPException(
- status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
- )
-
- promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
- return PostMintResponse(promises=promises)
- except (Exception, HTTPException) as e:
- logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
- # unset issued flag because something went wrong
- await ledger.crud.update_lightning_invoice(
- db=ledger.db, hash=payment_hash, issued=False
- )
- raise HTTPException(
- status_code=getattr(e, "status_code")
- or HTTPStatus.INTERNAL_SERVER_ERROR,
- detail=str(e) or getattr(e, "detail"),
- )
- else:
- # only used for testing when LIGHTNING=false
- promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
- return PostMintResponse(promises=promises)
-
-
-@cashu_ext.post("/api/v1/{cashu_id}/melt")
-async def melt_coins(
- payload: PostMeltRequest, cashu_id: str = Query(None)
-) -> GetMeltResponse:
- """Invalidates proofs and pays a Lightning invoice."""
- cashu: Union[None, Cashu] = await get_cashu(cashu_id)
- if cashu is None:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
- proofs = payload.proofs
- invoice = payload.pr
-
- # !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
- # THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
- # TOKENS
- assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException(
- status_code=HTTPStatus.METHOD_NOT_ALLOWED,
- detail="Error: Tokens are from another mint.",
- )
-
- # set proofs as pending
- await ledger._set_proofs_pending(proofs)
-
- try:
- await ledger._verify_proofs(proofs)
-
- total_provided = sum([p["amount"] for p in proofs])
- invoice_obj = bolt11.decode(invoice)
- amount = math.ceil(invoice_obj.amount_msat / 1000)
-
- internal_checking_id = await check_internal(invoice_obj.payment_hash)
-
- if not internal_checking_id:
- fees_msat = fee_reserve(invoice_obj.amount_msat)
- else:
- fees_msat = 0
- assert total_provided >= amount + math.ceil(fees_msat / 1000), Exception(
- f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
- )
- logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
- try:
- await pay_invoice(
- wallet_id=cashu.wallet,
- payment_request=invoice,
- description="Pay cashu invoice",
- extra={"tag": "cashu", "cashu_name": cashu.name},
- )
- except Exception as e:
- logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
- raise e
- finally:
- logger.debug(
- f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
- )
- status: PaymentStatus = await check_transaction_status(
- cashu.wallet, invoice_obj.payment_hash
- )
- if status.paid is True:
- logger.debug(
- f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
- )
- await ledger._invalidate_proofs(proofs)
- else:
- logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
- except Exception as e:
- logger.debug(f"Cashu: Exception: {str(e)}")
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=f"Cashu: {str(e)}",
- )
- finally:
- logger.debug("Cashu: Unset pending")
- # delete proofs from pending list
- await ledger._unset_proofs_pending(proofs)
-
- return GetMeltResponse(paid=status.paid, preimage=status.preimage)
-
-
-@cashu_ext.post("/api/v1/{cashu_id}/check")
-async def check_spendable(
- payload: CheckSpendableRequest, cashu_id: str = Query(None)
-) -> Dict[int, bool]:
- """Check whether a secret has been spent already or not."""
- cashu: Union[None, Cashu] = await get_cashu(cashu_id)
- if cashu is None:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
- spendableList = await ledger.check_spendable(payload.proofs)
- return CheckSpendableResponse(spendable=spendableList)
-
-
-@cashu_ext.post("/api/v1/{cashu_id}/checkfees")
-async def check_fees(
- payload: CheckFeesRequest, cashu_id: str = Query(None)
-) -> CheckFeesResponse:
- """
- Responds with the fees necessary to pay a Lightning invoice.
- Used by wallets for figuring out the fees they need to supply.
- This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
- """
- cashu: Union[None, Cashu] = await get_cashu(cashu_id)
- if cashu is None:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
- invoice_obj = bolt11.decode(payload.pr)
- internal_checking_id = await check_internal(invoice_obj.payment_hash)
-
- if not internal_checking_id:
- fees_msat = fee_reserve(invoice_obj.amount_msat)
- else:
- fees_msat = 0
- return CheckFeesResponse(fee=math.ceil(fees_msat / 1000))
-
-
-@cashu_ext.post("/api/v1/{cashu_id}/split")
-async def split(
- payload: PostSplitRequest, cashu_id: str = Query(None)
-) -> PostSplitResponse:
- """
- Requetst a set of tokens with amount "total" to be split into two
- newly minted sets with amount "split" and "total-split".
- """
- cashu: Union[None, Cashu] = await get_cashu(cashu_id)
- if cashu is None:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
- )
- proofs = payload.proofs
-
- # !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
- # THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
- # TOKENS
- if not all([p.id == cashu.keyset_id for p in proofs]):
- raise HTTPException(
- status_code=HTTPStatus.METHOD_NOT_ALLOWED,
- detail="Error: Tokens are from another mint.",
- )
-
- amount = payload.amount
- outputs = payload.outputs
- assert outputs, Exception("no outputs provided.")
- split_return = None
- try:
- keyset = ledger.keysets.keysets[cashu.keyset_id]
- split_return = await ledger.split(proofs, amount, outputs, keyset)
- except Exception as exc:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=str(exc),
- )
- if not split_return:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail="there was an error with the split",
- )
- frst_promises, scnd_promises = split_return
- resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
- return resp
diff --git a/lnbits/extensions/deezy/README.md b/lnbits/extensions/deezy/README.md
deleted file mode 100644
index c8c0678a..00000000
--- a/lnbits/extensions/deezy/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Deezy: Home for Lightning Liquidity
-Swap lightning bitcoin for on-chain bitcoin to get inbound liquidity. Or get an on-chain deposit address for your lightning address.
-* [Website](https://deezy.io)
-* [Lightning Node](https://amboss.space/node/024bfaf0cabe7f874fd33ebf7c6f4e5385971fc504ef3f492432e9e3ec77e1b5cf)
-* [Documentation](https://docs.deezy.io)
-* [Discord](https://discord.gg/nEBbrUAvPy)
-
-# Usage
-This extension lets you swap lightning btc for on-chain btc and vice versa.
-* Swap Lightning -> BTC to get inbound liquidity
-* Swap BTC -> Lightning to generate an on-chain deposit address for your lightning address
\ No newline at end of file
diff --git a/lnbits/extensions/deezy/__init__.py b/lnbits/extensions/deezy/__init__.py
deleted file mode 100644
index 60596445..00000000
--- a/lnbits/extensions/deezy/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-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: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/deezy/config.json b/lnbits/extensions/deezy/config.json
deleted file mode 100644
index 4f945a79..00000000
--- a/lnbits/extensions/deezy/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Deezy",
- "short_description": "LN to onchain, onchain to LN swaps",
- "tile": "/deezy/static/deezy.png",
- "contributors": ["Uthpala"]
-}
diff --git a/lnbits/extensions/deezy/crud.py b/lnbits/extensions/deezy/crud.py
deleted file mode 100644
index 38adc266..00000000
--- a/lnbits/extensions/deezy/crud.py
+++ /dev/null
@@ -1,114 +0,0 @@
-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(
- "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(
- "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(
- "SELECT * FROM deezy.token ORDER BY created_at DESC",
- )
-
- return Token(**row) if row else None
-
-
-async def save_token(
- data: Token,
-) -> Token:
-
- await db.execute(
- """
- INSERT INTO deezy.token (
- deezy_token
- )
- VALUES (?)
- """,
- (data.deezy_token,),
- )
- return data
-
-
-async def save_ln_to_btc(
- data: LnToBtcSwap,
-) -> LnToBtcSwap:
-
- return await db.execute(
- """
- INSERT INTO deezy.ln_to_btc_swap (
- amount_sats,
- on_chain_address,
- on_chain_sats_per_vbyte,
- bolt11_invoice,
- fee_sats,
- txid,
- tx_hex
- )
- VALUES (?,?,?,?,?,?,?)
- """,
- (
- data.amount_sats,
- data.on_chain_address,
- data.on_chain_sats_per_vbyte,
- data.bolt11_invoice,
- data.fee_sats,
- data.txid,
- data.tx_hex,
- ),
- )
-
-
-async def update_ln_to_btc(data: UpdateLnToBtcSwap) -> str:
- await db.execute(
- """
- UPDATE deezy.ln_to_btc_swap
- SET txid = ?, tx_hex = ?
- WHERE bolt11_invoice = ?
- """,
- (data.txid, data.tx_hex, data.bolt11_invoice),
- )
-
- return data.txid
-
-
-async def save_btc_to_ln(
- data: BtcToLnSwap,
-) -> BtcToLnSwap:
-
- return await db.execute(
- """
- INSERT INTO deezy.btc_to_ln_swap (
- ln_address,
- on_chain_address,
- secret_access_key,
- commitment,
- signature
- )
- VALUES (?,?,?,?,?)
- """,
- (
- data.ln_address,
- data.on_chain_address,
- data.secret_access_key,
- data.commitment,
- data.signature,
- ),
- )
diff --git a/lnbits/extensions/deezy/migrations.py b/lnbits/extensions/deezy/migrations.py
deleted file mode 100644
index 42c3973e..00000000
--- a/lnbits/extensions/deezy/migrations.py
+++ /dev/null
@@ -1,37 +0,0 @@
-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(
- """
- 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(
- """
- CREATE TABLE deezy.token (
- deezy_token TEXT NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
- );
- """
- )
diff --git a/lnbits/extensions/deezy/models.py b/lnbits/extensions/deezy/models.py
deleted file mode 100644
index ad0c03cb..00000000
--- a/lnbits/extensions/deezy/models.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from pydantic.main import BaseModel
-
-
-class Token(BaseModel):
- deezy_token: str
-
-
-class LnToBtcSwap(BaseModel):
- amount_sats: int
- on_chain_address: str
- on_chain_sats_per_vbyte: int
- bolt11_invoice: str
- fee_sats: int
- txid: str = ""
- tx_hex: str = ""
- created_at: str = ""
-
-
-class UpdateLnToBtcSwap(BaseModel):
- txid: str
- tx_hex: str
- bolt11_invoice: str
-
-
-class BtcToLnSwap(BaseModel):
- ln_address: str
- on_chain_address: str
- secret_access_key: str
- commitment: str
- signature: str
- created_at: str = ""
diff --git a/lnbits/extensions/deezy/static/deezy.png b/lnbits/extensions/deezy/static/deezy.png
deleted file mode 100644
index cb526705..00000000
Binary files a/lnbits/extensions/deezy/static/deezy.png and /dev/null differ
diff --git a/lnbits/extensions/deezy/templates/deezy/_api_docs.html b/lnbits/extensions/deezy/templates/deezy/_api_docs.html
deleted file mode 100644
index 4a4e9e30..00000000
--- a/lnbits/extensions/deezy/templates/deezy/_api_docs.html
+++ /dev/null
@@ -1,253 +0,0 @@
-
-
-
-
-
- Deezy.io: Do onchain to offchain and vice-versa swaps
-
-
- Link :
-
- https://deezy.io/
-
-
-
- API DOCS
-
-
- Created by,
- Uthpala
-
-
-
-
-
-
-
-
-
-
- Get the current info about the swap service for converting LN btc to
- on-chain BTC.
-
-
- GET (mainnet)
- https://api.deezy.io/v1/swap/info
-
-
-
- GET (testnet)
- https://api-testnet.deezy.io/v1/swap/info
-
- Response
-
- {
- "liquidity_fee_ppm": 2000,
- "on_chain_bytes_estimate": 300,
- "max_swap_amount_sats": 100000000,
- "min_swap_amount_sats": 100000,
- "available": true
- }
-
-
-
-
-
-
-
-
- Initiate a new swap to send lightning btc in exchange for on-chain
- btc
-
-
- POST (mainnet)
- https://api.deezy.io/v1/swap
-
-
-
- POST (testnet)
- https://api-testnet.deezy.io/v1/swap
-
- Payload
-
- {
- "amount_sats": 500000,
- "on_chain_address": "tb1qrcdhlm0m...",
- "on_chain_sats_per_vbyte": 2
- }
-
- Response
-
- {
- "bolt11_invoice": "lntb603u1p3vmxj7p...",
- "fee_sats": 600
- }
-
-
-
-
-
-
-
-
- Lookup the on-chain transaction information for an existing swap
-
-
- GET (mainnet)
- https://api.deezy.io/v1/swap/lookup
-
-
-
- GET (testnet)
- https://api-testnet.deezy.io/v1/swap/lookup
-
- Query Parameter
-
- "bolt11_invoice": "lntb603u1p3vmxj7pp54...",
-
- Response
-
- {
- "on_chain_txid": "string",
- "tx_hex": "string"
- }
-
-
-
-
-
-
-
-
-
-
- Generate an on-chain deposit address for your lnurl or lightning
- address.
-
-
- POST (mainnet)
- https://api.deezy.io/v1/source
-
-
-
- POST (testnet)
- https://api-testnet.deezy.io/v1/source
-
- Payload
-
- {
- "lnurl_or_lnaddress": "LNURL1DP68GURN8GHJ...",
- "secret_access_key": "b3c6056d2845867fa7..",
- "webhook_url": "https://your.website.com/dee.."
- }
-
- Response
-
- {
- "address": "bc1qkceyc5...",
- "secret_access_key": "b3c6056d28458...",
- "commitment": "for any satoshis sent to bc1..",
- "signature": "d69j6aj1ssz5egmsr..",
- "webhook_url": "https://your.website.com/deez.."
- }
-
-
-
-
-
-
-
-
- Lookup (BTC to LN) swaps
-
-
- GET (mainnet)
- https://api.deezy.io/v1/source/lookup
-
-
-
- GET (testnet)
- https://api-testnet.deezy.io/v1/source/lookup
-
- Response
-
- {
- "swaps": [
- {
- "lnurl_or_lnaddress": "string",
- "deposit_address": "string",
- "utxo_key": "string",
- "deposit_amount_sats": 0,
- "target_payout_amount_sats": 0,
- "paid_amount_sats": 0,
- "deezy_fee_sats": 0,
- "status": "string"
- }
- ],
- "total_sent_sats": 0,
- "total_received_sats": 0,
- "total_pending_payout_sats": 0,
- "total_deezy_fees_sats": 0
- }
-
-
-
-
-
-
diff --git a/lnbits/extensions/deezy/templates/deezy/index.html b/lnbits/extensions/deezy/templates/deezy/index.html
deleted file mode 100644
index 9d112ef1..00000000
--- a/lnbits/extensions/deezy/templates/deezy/index.html
+++ /dev/null
@@ -1,588 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- Deezy
-
- An access token is required to use the swap service. Email
- support@deezy.io or contact @dannydeezy on telegram to get one.
-
-
-
- Deezy token
- Add or Update token
-
-
-
-
-
-
-
-
-
-
-
-
- Send lightning btc and receive on-chain btc
-
-
-
-
- Send on-chain btc and receive via lightning
-
-
-
-
-
-
LIGHTNING BTC -> BTC
-
-
-
-
-
-
- Cancel
-
-
-
-
-
-
Pay invoice to complete swap
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
BTC -> LIGHTNING BTC
-
-
-
- Cancel
-
-
-
-
-
-
Onchain Address
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% raw %}
-
-
-
- Success Bitcoin is on its way
-
-
-
- Onchain tx id {{ swapLnToBtc.onchainTxId }}
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
- {{SITE_TITLE}} Deezy extension
-
-
-
- {% include "deezy/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/deezy/views.py b/lnbits/extensions/deezy/views.py
deleted file mode 100644
index c4cc4b22..00000000
--- a/lnbits/extensions/deezy/views.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from fastapi import Request
-from fastapi.params import Depends
-from fastapi.templating import Jinja2Templates
-from starlette.responses import HTMLResponse
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import deezy_ext, deezy_renderer
-
-templates = Jinja2Templates(directory="templates")
-
-
-@deezy_ext.get("/", response_class=HTMLResponse)
-async def index(
- request: Request,
- user: User = Depends(check_user_exists), # type: ignore
-):
- return deezy_renderer().TemplateResponse(
- "deezy/index.html", {"request": request, "user": user.dict()}
- )
diff --git a/lnbits/extensions/deezy/views_api.py b/lnbits/extensions/deezy/views_api.py
deleted file mode 100644
index 1006edeb..00000000
--- a/lnbits/extensions/deezy/views_api.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# views_api.py is for you API endpoints that could be hit by another service
-
-# add your dependencies here
-
-# import httpx
-# (use httpx just like requests, except instead of response.ok there's only the
-# response.is_error that is its inverse)
-
-from . import deezy_ext
-from .crud import (
- get_btc_to_ln,
- get_ln_to_btc,
- get_token,
- save_btc_to_ln,
- save_ln_to_btc,
- save_token,
- update_ln_to_btc,
-)
-from .models import BtcToLnSwap, LnToBtcSwap, Token, UpdateLnToBtcSwap
-
-
-@deezy_ext.get("/api/v1/token")
-async def api_deezy_get_token():
- rows = await get_token()
- return rows
-
-
-@deezy_ext.get("/api/v1/ln-to-btc")
-async def api_deezy_get_ln_to_btc():
- rows = await get_ln_to_btc()
- return rows
-
-
-@deezy_ext.get("/api/v1/btc-to-ln")
-async def api_deezy_get_btc_to_ln():
- rows = await get_btc_to_ln()
- return rows
-
-
-@deezy_ext.post("/api/v1/store-token")
-async def api_deezy_save_toke(data: Token):
- await save_token(data)
-
- return data.deezy_token
-
-
-@deezy_ext.post("/api/v1/store-ln-to-btc")
-async def api_deezy_save_ln_to_btc(data: LnToBtcSwap):
- response = await save_ln_to_btc(data)
-
- return response
-
-
-@deezy_ext.post("/api/v1/update-ln-to-btc")
-async def api_deezy_update_ln_to_btc(data: UpdateLnToBtcSwap):
- response = await update_ln_to_btc(data)
-
- return response
-
-
-@deezy_ext.post("/api/v1/store-btc-to-ln")
-async def api_deezy_save_btc_to_ln(data: BtcToLnSwap):
- response = await save_btc_to_ln(data)
-
- return response
diff --git a/lnbits/extensions/invoices/README.md b/lnbits/extensions/invoices/README.md
deleted file mode 100644
index 2b5bd538..00000000
--- a/lnbits/extensions/invoices/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Invoices
-
-## Create invoices that you can send to your client to pay online over Lightning.
-
-This extension allows users to create "traditional" invoices (not in the lightning sense) that contain one or more line items. Line items are denominated in a user-configurable fiat currency. Each invoice contains one or more payments up to the total of the invoice. Each invoice creates a public link that can be shared with a customer that they can use to (partially or in full) pay the invoice.
-
-## Usage
-
-1. Create an invoice by clicking "NEW INVOICE"\
- 
-2. Fill the options for your INVOICE
- - select the wallet
- - select the fiat currency the invoice will be denominated in
- - select a status for the invoice (default is draft)
- - enter a company name, first name, last name, email, phone & address (optional)
- - add one or more line items
- - enter a name & price for each line item
-3. You can then use share your invoice link with your customer to receive payment\
- 
\ No newline at end of file
diff --git a/lnbits/extensions/invoices/__init__.py b/lnbits/extensions/invoices/__init__.py
deleted file mode 100644
index 735e95d8..00000000
--- a/lnbits/extensions/invoices/__init__.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import asyncio
-
-from fastapi import APIRouter
-from starlette.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_invoices")
-
-invoices_static_files = [
- {
- "path": "/invoices/static",
- "app": StaticFiles(directory="lnbits/extensions/invoices/static"),
- "name": "invoices_static",
- }
-]
-
-invoices_ext: APIRouter = APIRouter(prefix="/invoices", tags=["invoices"])
-
-
-def invoices_renderer():
- return template_renderer(["lnbits/extensions/invoices/templates"])
-
-
-from .tasks import wait_for_paid_invoices
-
-
-def invoices_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
-
-
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/invoices/config.json b/lnbits/extensions/invoices/config.json
deleted file mode 100644
index a604fec5..00000000
--- a/lnbits/extensions/invoices/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Invoices",
- "short_description": "Create invoices for your clients.",
- "tile": "/invoices/static/image/invoices.png",
- "contributors": ["leesalminen"]
-}
diff --git a/lnbits/extensions/invoices/crud.py b/lnbits/extensions/invoices/crud.py
deleted file mode 100644
index 39652802..00000000
--- a/lnbits/extensions/invoices/crud.py
+++ /dev/null
@@ -1,210 +0,0 @@
-from typing import List, Optional, Union
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import (
- CreateInvoiceData,
- CreateInvoiceItemData,
- Invoice,
- InvoiceItem,
- Payment,
- UpdateInvoiceData,
- UpdateInvoiceItemData,
-)
-
-
-async def get_invoice(invoice_id: str) -> Optional[Invoice]:
- row = await db.fetchone(
- "SELECT * FROM invoices.invoices WHERE id = ?", (invoice_id,)
- )
- return Invoice.from_row(row) if row else None
-
-
-async def get_invoice_items(invoice_id: str) -> List[InvoiceItem]:
- rows = await db.fetchall(
- "SELECT * FROM invoices.invoice_items WHERE invoice_id = ?", (invoice_id,)
- )
-
- return [InvoiceItem.from_row(row) for row in rows]
-
-
-async def get_invoice_item(item_id: str) -> Optional[InvoiceItem]:
- row = await db.fetchone(
- "SELECT * FROM invoices.invoice_items WHERE id = ?", (item_id,)
- )
- return InvoiceItem.from_row(row) if row else None
-
-
-async def get_invoice_total(items: List[InvoiceItem]) -> int:
- return sum(item.amount for item in items)
-
-
-async def get_invoices(wallet_ids: Union[str, List[str]]) -> List[Invoice]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM invoices.invoices WHERE wallet IN ({q})", (*wallet_ids,)
- )
-
- return [Invoice.from_row(row) for row in rows]
-
-
-async def get_invoice_payments(invoice_id: str) -> List[Payment]:
- rows = await db.fetchall(
- "SELECT * FROM invoices.payments WHERE invoice_id = ?", (invoice_id,)
- )
-
- return [Payment.from_row(row) for row in rows]
-
-
-async def get_invoice_payment(payment_id: str) -> Optional[Payment]:
- row = await db.fetchone(
- "SELECT * FROM invoices.payments WHERE id = ?", (payment_id,)
- )
- return Payment.from_row(row) if row else None
-
-
-async def get_payments_total(payments: List[Payment]) -> int:
- return sum(item.amount for item in payments)
-
-
-async def create_invoice_internal(wallet_id: str, data: CreateInvoiceData) -> Invoice:
- invoice_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO invoices.invoices (id, wallet, status, currency, company_name, first_name, last_name, email, phone, address)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- invoice_id,
- wallet_id,
- data.status,
- data.currency,
- data.company_name,
- data.first_name,
- data.last_name,
- data.email,
- data.phone,
- data.address,
- ),
- )
-
- invoice = await get_invoice(invoice_id)
- assert invoice, "Newly created invoice couldn't be retrieved"
- return invoice
-
-
-async def create_invoice_items(
- invoice_id: str, data: List[CreateInvoiceItemData]
-) -> List[InvoiceItem]:
- for item in data:
- item_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO invoices.invoice_items (id, invoice_id, description, amount)
- VALUES (?, ?, ?, ?)
- """,
- (
- item_id,
- invoice_id,
- item.description,
- int(item.amount * 100),
- ),
- )
-
- invoice_items = await get_invoice_items(invoice_id)
- return invoice_items
-
-
-async def update_invoice_internal(
- wallet_id: str, data: Union[UpdateInvoiceData, Invoice]
-) -> Invoice:
- await db.execute(
- """
- UPDATE invoices.invoices
- SET wallet = ?, currency = ?, status = ?, company_name = ?, first_name = ?, last_name = ?, email = ?, phone = ?, address = ?
- WHERE id = ?
- """,
- (
- wallet_id,
- data.currency,
- data.status,
- data.company_name,
- data.first_name,
- data.last_name,
- data.email,
- data.phone,
- data.address,
- data.id,
- ),
- )
-
- invoice = await get_invoice(data.id)
- assert invoice, "Newly updated invoice couldn't be retrieved"
- return invoice
-
-
-async def update_invoice_items(
- invoice_id: str, data: List[UpdateInvoiceItemData]
-) -> List[InvoiceItem]:
- updated_items = []
- for item in data:
- if item.id:
- updated_items.append(item.id)
- await db.execute(
- """
- UPDATE invoices.invoice_items
- SET description = ?, amount = ?
- WHERE id = ?
- """,
- (item.description, int(item.amount * 100), item.id),
- )
-
- placeholders = ",".join("?" for _ in range(len(updated_items)))
- if not placeholders:
- placeholders = "?"
- updated_items = ["skip"]
-
- await db.execute(
- f"""
- DELETE FROM invoices.invoice_items
- WHERE invoice_id = ?
- AND id NOT IN ({placeholders})
- """,
- (
- invoice_id,
- *tuple(updated_items),
- ),
- )
-
- for item in data:
- if not item:
- await create_invoice_items(
- invoice_id=invoice_id,
- data=[CreateInvoiceItemData(description=item.description)],
- )
-
- invoice_items = await get_invoice_items(invoice_id)
- return invoice_items
-
-
-async def create_invoice_payment(invoice_id: str, amount: int) -> Payment:
- payment_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO invoices.payments (id, invoice_id, amount)
- VALUES (?, ?, ?)
- """,
- (
- payment_id,
- invoice_id,
- amount,
- ),
- )
-
- payment = await get_invoice_payment(payment_id)
- assert payment, "Newly created payment couldn't be retrieved"
- return payment
diff --git a/lnbits/extensions/invoices/migrations.py b/lnbits/extensions/invoices/migrations.py
deleted file mode 100644
index 74a0fdba..00000000
--- a/lnbits/extensions/invoices/migrations.py
+++ /dev/null
@@ -1,55 +0,0 @@
-async def m001_initial_invoices(db):
-
- # STATUS COLUMN OPTIONS: 'draft', 'open', 'paid', 'canceled'
-
- await db.execute(
- f"""
- CREATE TABLE invoices.invoices (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
-
- status TEXT NOT NULL DEFAULT 'draft',
-
- currency TEXT NOT NULL,
-
- company_name TEXT DEFAULT NULL,
- first_name TEXT DEFAULT NULL,
- last_name TEXT DEFAULT NULL,
- email TEXT DEFAULT NULL,
- phone TEXT DEFAULT NULL,
- address TEXT DEFAULT NULL,
-
-
- time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
- );
- """
- )
-
- await db.execute(
- f"""
- CREATE TABLE invoices.invoice_items (
- id TEXT PRIMARY KEY,
- invoice_id TEXT NOT NULL,
-
- description TEXT NOT NULL,
- amount INTEGER NOT NULL,
-
- FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
- );
- """
- )
-
- await db.execute(
- f"""
- CREATE TABLE invoices.payments (
- id TEXT PRIMARY KEY,
- invoice_id TEXT NOT NULL,
-
- amount {db.big_int} NOT NULL,
-
- time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
-
- FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
- );
- """
- )
diff --git a/lnbits/extensions/invoices/models.py b/lnbits/extensions/invoices/models.py
deleted file mode 100644
index 6f0e63cb..00000000
--- a/lnbits/extensions/invoices/models.py
+++ /dev/null
@@ -1,104 +0,0 @@
-from enum import Enum
-from sqlite3 import Row
-from typing import List, Optional
-
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class InvoiceStatusEnum(str, Enum):
- draft = "draft"
- open = "open"
- paid = "paid"
- canceled = "canceled"
-
-
-class CreateInvoiceItemData(BaseModel):
- description: str
- amount: float = Query(..., ge=0.01)
-
-
-class CreateInvoiceData(BaseModel):
- status: InvoiceStatusEnum = InvoiceStatusEnum.draft
- currency: str
- company_name: Optional[str]
- first_name: Optional[str]
- last_name: Optional[str]
- email: Optional[str]
- phone: Optional[str]
- address: Optional[str]
- items: List[CreateInvoiceItemData]
-
- class Config:
- use_enum_values = True
-
-
-class UpdateInvoiceItemData(BaseModel):
- id: Optional[str]
- description: str
- amount: float = Query(..., ge=0.01)
-
-
-class UpdateInvoiceData(BaseModel):
- id: str
- wallet: str
- status: InvoiceStatusEnum = InvoiceStatusEnum.draft
- currency: str
- company_name: Optional[str]
- first_name: Optional[str]
- last_name: Optional[str]
- email: Optional[str]
- phone: Optional[str]
- address: Optional[str]
- items: List[UpdateInvoiceItemData]
-
-
-class Invoice(BaseModel):
- id: str
- wallet: str
- status: InvoiceStatusEnum = InvoiceStatusEnum.draft
- currency: str
- company_name: Optional[str]
- first_name: Optional[str]
- last_name: Optional[str]
- email: Optional[str]
- phone: Optional[str]
- address: Optional[str]
- time: int
-
- class Config:
- use_enum_values = True
-
- @classmethod
- def from_row(cls, row: Row) -> "Invoice":
- return cls(**dict(row))
-
-
-class InvoiceItem(BaseModel):
- id: str
- invoice_id: str
- description: str
- amount: int
-
- class Config:
- orm_mode = True
-
- @classmethod
- def from_row(cls, row: Row) -> "InvoiceItem":
- return cls(**dict(row))
-
-
-class Payment(BaseModel):
- id: str
- invoice_id: str
- amount: int
- time: int
-
- @classmethod
- def from_row(cls, row: Row) -> "Payment":
- return cls(**dict(row))
-
-
-class CreatePaymentData(BaseModel):
- invoice_id: str
- amount: int
diff --git a/lnbits/extensions/invoices/static/css/pay.css b/lnbits/extensions/invoices/static/css/pay.css
deleted file mode 100644
index ad7ce914..00000000
--- a/lnbits/extensions/invoices/static/css/pay.css
+++ /dev/null
@@ -1,65 +0,0 @@
-#invoicePage>.row:first-child>.col-md-6 {
- display: flex;
-}
-
-#invoicePage>.row:first-child>.col-md-6>.q-card {
- flex: 1;
-}
-
-#invoicePage .clear {
- margin-bottom: 25px;
-}
-
-#printQrCode {
- display: none;
-}
-
-@media (min-width: 1024px) {
- #invoicePage>.row:first-child>.col-md-6:first-child>div {
- margin-right: 5px;
- }
-
- #invoicePage>.row:first-child>.col-md-6:nth-child(2)>div {
- margin-left: 5px;
- }
-}
-
-
-@media print {
- * {
- color: black !important;
- }
-
- header, button, #payButtonContainer {
- display: none !important;
- }
-
- main, .q-page-container {
- padding-top: 0px !important;
- }
-
- .q-card {
- box-shadow: none !important;
- border: 1px solid black;
- }
-
- .q-item {
- padding: 5px;
- }
-
- .q-card__section {
- padding: 5px;
- }
-
- #printQrCode {
- display: block;
- }
-
- p {
- margin-bottom: 0px !important;
- }
-
- #invoicePage .clear {
- margin-bottom: 10px !important;
- }
-}
\ No newline at end of file
diff --git a/lnbits/extensions/invoices/static/image/invoices.png b/lnbits/extensions/invoices/static/image/invoices.png
deleted file mode 100644
index 823f9dee..00000000
Binary files a/lnbits/extensions/invoices/static/image/invoices.png and /dev/null differ
diff --git a/lnbits/extensions/invoices/tasks.py b/lnbits/extensions/invoices/tasks.py
deleted file mode 100644
index c8a829db..00000000
--- a/lnbits/extensions/invoices/tasks.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import asyncio
-
-from lnbits.core.models import Payment
-from lnbits.tasks import register_invoice_listener
-
-from .crud import (
- create_invoice_payment,
- get_invoice,
- get_invoice_items,
- get_invoice_payments,
- get_invoice_total,
- get_payments_total,
- update_invoice_internal,
-)
-from .models import InvoiceStatusEnum
-
-
-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") != "invoices":
- return
-
- invoice_id = payment.extra.get("invoice_id")
- assert invoice_id
-
- amount = payment.extra.get("famount")
- assert amount
-
- await create_invoice_payment(invoice_id=invoice_id, amount=amount)
-
- invoice = await get_invoice(invoice_id)
- assert invoice
-
- invoice_items = await get_invoice_items(invoice_id)
- invoice_total = await get_invoice_total(invoice_items)
-
- invoice_payments = await get_invoice_payments(invoice_id)
- payments_total = await get_payments_total(invoice_payments)
-
- if payments_total >= invoice_total:
- invoice.status = InvoiceStatusEnum.paid
- await update_invoice_internal(invoice.wallet, invoice)
-
- return
diff --git a/lnbits/extensions/invoices/templates/invoices/_api_docs.html b/lnbits/extensions/invoices/templates/invoices/_api_docs.html
deleted file mode 100644
index 6e2a6355..00000000
--- a/lnbits/extensions/invoices/templates/invoices/_api_docs.html
+++ /dev/null
@@ -1,153 +0,0 @@
-
-
-
-
- GET /invoices/api/v1/invoices
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<invoice_object>, ...]
- Curl example
- curl -X GET {{ request.base_url }}invoices/api/v1/invoices -H
- "X-Api-Key: <invoice_key>"
-
-
-
-
-
-
-
-
- GET
- /invoices/api/v1/invoice/{invoice_id}
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {invoice_object}
- Curl example
- curl -X GET {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
- <invoice_key>"
-
-
-
-
-
-
-
-
- POST /invoices/api/v1/invoice
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {invoice_object}
- Curl example
- curl -X POST {{ request.base_url }}invoices/api/v1/invoice -H
- "X-Api-Key: <invoice_key>"
-
-
-
-
-
-
-
-
- POST
- /invoices/api/v1/invoice/{invoice_id}
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {invoice_object}
- Curl example
- curl -X POST {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
- <invoice_key>"
-
-
-
-
-
-
-
-
- POST
- /invoices/api/v1/invoice/{invoice_id}/payments
- Headers
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {payment_object}
- Curl example
- curl -X POST {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id}/payments -H "X-Api-Key:
- <invoice_key>"
-
-
-
-
-
-
-
-
- GET
- /invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}
- Headers
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- Curl example
- curl -X GET {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash} -H
- "X-Api-Key: <invoice_key>"
-
-
-
-
-
diff --git a/lnbits/extensions/invoices/templates/invoices/index.html b/lnbits/extensions/invoices/templates/invoices/index.html
deleted file mode 100644
index 4ef3b7f1..00000000
--- a/lnbits/extensions/invoices/templates/invoices/index.html
+++ /dev/null
@@ -1,571 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New Invoice
-
-
-
-
-
-
-
-
Invoices
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} Invoices extension
-
-
-
-
- {% include "invoices/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Line Item
-
-
-
-
-
- Create Invoice
- Save Invoice
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/invoices/templates/invoices/pay.html b/lnbits/extensions/invoices/templates/invoices/pay.html
deleted file mode 100644
index 82f1765e..00000000
--- a/lnbits/extensions/invoices/templates/invoices/pay.html
+++ /dev/null
@@ -1,433 +0,0 @@
-{% extends "public.html" %} {% block toolbar_title %} Invoice
-
-
-{% endblock %} {% from "macros.jinja" import window_vars with context %} {%
-block page %}
-
-
-
-
-
-
-
- Invoice
-
-
-
-
- ID
- {{ invoice_id }}
-
-
-
- Created At
- {{ datetime.utcfromtimestamp(invoice.time).strftime('%Y-%m-%d
- %H:%M') }}
-
-
-
- Status
-
-
- {{ invoice.status }}
-
-
-
-
-
- Total
-
- {{ "{:0,.2f}".format(invoice_total / 100) }} {{ invoice.currency
- }}
-
-
-
-
- Paid
-
-
-
- {{ "{:0,.2f}".format(payments_total / 100) }} {{
- invoice.currency }}
-
-
- {% if payments_total < invoice_total %}
-
- Pay Invoice
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Bill To
-
-
-
-
- Company Name
- {{ invoice.company_name }}
-
-
-
- Name
- {{ invoice.first_name }} {{ invoice.last_name
- }}
-
-
-
- Address
- {{ invoice.address }}
-
-
-
- Email
- {{ invoice.email }}
-
-
-
- Phone
- {{ invoice.phone }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Items
-
-
-
- {% if invoice_items %}
-
- Item
- Amount
-
- {% endif %} {% for item in invoice_items %}
-
- {{item.description}}
-
- {{ "{:0,.2f}".format(item.amount / 100) }} {{ invoice.currency
- }}
-
-
- {% endfor %} {% if not invoice_items %} No Invoice Items {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Payments
-
-
-
- {% if invoice_payments %}
-
- Date
- Amount
-
- {% endif %} {% for item in invoice_payments %}
-
- {{ datetime.utcfromtimestamp(item.time).strftime('%Y-%m-%d
- %H:%M') }}
-
- {{ "{:0,.2f}".format(item.amount / 100) }} {{ invoice.currency
- }}
-
-
- {% endfor %} {% if not invoice_payments %} No Invoice Payments {%
- endif %}
-
-
-
-
-
-
-
-
-
-
-
-
Scan to View & Pay Online!
-
-
-
-
-
-
-
-
-
-
- {{ invoice.currency }}
-
-
-
-
- Create Payment
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
- Copy Invoice
-
-
-
-
-
-
-
-
-
-
- Copy URL
- Close
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/invoices/views.py b/lnbits/extensions/invoices/views.py
deleted file mode 100644
index cc35b351..00000000
--- a/lnbits/extensions/invoices/views.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from datetime import datetime
-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 invoices_ext, invoices_renderer
-from .crud import (
- get_invoice,
- get_invoice_items,
- get_invoice_payments,
- get_invoice_total,
- get_payments_total,
-)
-
-templates = Jinja2Templates(directory="templates")
-
-
-@invoices_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return invoices_renderer().TemplateResponse(
- "invoices/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@invoices_ext.get("/pay/{invoice_id}", response_class=HTMLResponse)
-async def pay(request: Request, invoice_id: str):
- invoice = await get_invoice(invoice_id)
-
- if not invoice:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
- )
-
- invoice_items = await get_invoice_items(invoice_id)
- invoice_total = await get_invoice_total(invoice_items)
-
- invoice_payments = await get_invoice_payments(invoice_id)
- payments_total = await get_payments_total(invoice_payments)
-
- return invoices_renderer().TemplateResponse(
- "invoices/pay.html",
- {
- "request": request,
- "invoice_id": invoice_id,
- "invoice": invoice.dict(),
- "invoice_items": invoice_items,
- "invoice_total": invoice_total,
- "invoice_payments": invoice_payments,
- "payments_total": payments_total,
- "datetime": datetime,
- },
- )
diff --git a/lnbits/extensions/invoices/views_api.py b/lnbits/extensions/invoices/views_api.py
deleted file mode 100644
index 1a7762a8..00000000
--- a/lnbits/extensions/invoices/views_api.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, HTTPException, Query
-from loguru import logger
-
-from lnbits.core.crud import get_user
-from lnbits.core.services import create_invoice
-from lnbits.core.views.api import api_payment
-from lnbits.decorators import WalletTypeInfo, get_key_type
-from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
-
-from . import invoices_ext
-from .crud import (
- create_invoice_internal,
- create_invoice_items,
- get_invoice,
- get_invoice_items,
- get_invoice_payments,
- get_invoice_total,
- get_invoices,
- get_payments_total,
- update_invoice_internal,
- update_invoice_items,
-)
-from .models import CreateInvoiceData, UpdateInvoiceData
-
-
-@invoices_ext.get("/api/v1/invoices", status_code=HTTPStatus.OK)
-async def api_invoices(
- all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
-):
- wallet_ids = [wallet.wallet.id]
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- return [invoice.dict() for invoice in await get_invoices(wallet_ids)]
-
-
-@invoices_ext.get("/api/v1/invoice/{invoice_id}", status_code=HTTPStatus.OK)
-async def api_invoice(invoice_id: str):
- invoice = await get_invoice(invoice_id)
- if not invoice:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
- )
- invoice_items = await get_invoice_items(invoice_id)
-
- invoice_payments = await get_invoice_payments(invoice_id)
- payments_total = await get_payments_total(invoice_payments)
-
- invoice_dict = invoice.dict()
- invoice_dict["items"] = invoice_items
- invoice_dict["payments"] = payments_total
- return invoice_dict
-
-
-@invoices_ext.post("/api/v1/invoice", status_code=HTTPStatus.CREATED)
-async def api_invoice_create(
- data: CreateInvoiceData, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- invoice = await create_invoice_internal(wallet_id=wallet.wallet.id, data=data)
- items = await create_invoice_items(invoice_id=invoice.id, data=data.items)
- invoice_dict = invoice.dict()
- invoice_dict["items"] = items
- return invoice_dict
-
-
-@invoices_ext.post("/api/v1/invoice/{invoice_id}", status_code=HTTPStatus.OK)
-async def api_invoice_update(
- data: UpdateInvoiceData,
- invoice_id: str,
- wallet: WalletTypeInfo = Depends(get_key_type),
-):
- invoice = await update_invoice_internal(wallet_id=wallet.wallet.id, data=data)
- items = await update_invoice_items(invoice_id=invoice.id, data=data.items)
- invoice_dict = invoice.dict()
- invoice_dict["items"] = items
- return invoice_dict
-
-
-@invoices_ext.post(
- "/api/v1/invoice/{invoice_id}/payments", status_code=HTTPStatus.CREATED
-)
-async def api_invoices_create_payment(invoice_id: str, famount: int = Query(..., ge=1)):
- invoice = await get_invoice(invoice_id)
- invoice_items = await get_invoice_items(invoice_id)
- invoice_total = await get_invoice_total(invoice_items)
-
- invoice_payments = await get_invoice_payments(invoice_id)
- payments_total = await get_payments_total(invoice_payments)
-
- if payments_total + famount > invoice_total:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="Amount exceeds invoice due."
- )
-
- if not invoice:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
- )
-
- price_in_sats = await fiat_amount_as_satoshis(famount / 100, invoice.currency)
-
- try:
- payment_hash, payment_request = await create_invoice(
- wallet_id=invoice.wallet,
- amount=price_in_sats,
- memo=f"Payment for invoice {invoice_id}",
- extra={"tag": "invoices", "invoice_id": invoice_id, "famount": famount},
- )
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
-
- return {"payment_hash": payment_hash, "payment_request": payment_request}
-
-
-@invoices_ext.get(
- "/api/v1/invoice/{invoice_id}/payments/{payment_hash}", status_code=HTTPStatus.OK
-)
-async def api_invoices_check_payment(invoice_id: str, payment_hash: str):
- invoice = await get_invoice(invoice_id)
- if not invoice:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Invoice does not exist."
- )
- try:
- status = await api_payment(payment_hash)
-
- except Exception as exc:
- logger.error(exc)
- return {"paid": False}
- return status
diff --git a/lnbits/extensions/lnaddress/README.md b/lnbits/extensions/lnaddress/README.md
deleted file mode 100644
index d7e40503..00000000
--- a/lnbits/extensions/lnaddress/README.md
+++ /dev/null
@@ -1,68 +0,0 @@
-Lightning Address
-Rent Lightning Addresses on your domain
-LNAddress extension allows for someone to rent users lightning addresses on their domain.
-
-The extension is muted by default on the .env file and needs the admin of the LNbits instance to meet a few requirements on the server.
-
-## Requirements
-
-- Free Cloudflare account
-- Cloudflare as a DNS server provider
-- Cloudflare TOKEN and Cloudflare zone-ID where the domain is parked
-
-The server must provide SSL/TLS certificates to domain owners. If using caddy, this can be easily achieved with the Caddyfife snippet:
-
-```
-:443 {
- reverse_proxy localhost:5000
-
- tls @example.com {
- on_demand
- }
-}
-```
-
-fill in with your email.
-
-Certbot is also a possibity.
-
-## Usage
-
-1. Before adding a domain, you need to add the domain to Cloudflare and get an API key and Secret key\
- \
- You can use the _Edit zone DNS_ template Cloudflare provides.\
- \
- Edit the template as you like, if only using one domain you can narrow the scope of the template\
- 
-
-2. Back on LNbits, click "ADD DOMAIN"\
- 
-
-3. Fill the form with the domain information\
- 
-
- - select your wallet - add your domain
- - cloudflare keys
- - an optional webhook to get notified
- - the amount, in sats, you'll rent the addresses, per day
-
-4. Your domains will show up on the _Domains_ section\
- \
- On the left side, is the link to share with users so they can rent an address on your domain. When someone creates an address, after pay, they will be shown on the _Addresses_ section\
- 
-
-5. Addresses get automatically purged if expired or unpaid, after 24 hours. After expiration date, users will be granted a 24 hours period to renew their address!
-
-6. On the user/buyer side, the webpage will present the _Create_ or _Renew_ address tabs. On the Create tab:\
- 
- - optional email
- - the alias or username they want on your domain
- - the LNbits URL, if not the same instance (for example the user has an LNbits wallet on https://s.lnbits.com and is renting an address from https://lnbits.com)
- - the _Admin key_ for the wallet
- - how many days to rent a username for - bellow shows the per day cost and total cost the user will have to pay
-7. On the Renew tab:\
- 
- - enter the Alias/username
- - enter the wallet key
- - press the _GET INFO_ button to retrieve your address data
- - an expiration date will appear and the option to extend the duration of your address
diff --git a/lnbits/extensions/lnaddress/__init__.py b/lnbits/extensions/lnaddress/__init__.py
deleted file mode 100644
index dcc4a951..00000000
--- a/lnbits/extensions/lnaddress/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import asyncio
-
-from fastapi import APIRouter
-from starlette.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_lnaddress")
-
-lnaddress_ext: APIRouter = APIRouter(prefix="/lnaddress", tags=["lnaddress"])
-
-lnaddress_static_files = [
- {
- "path": "/lnaddress/static",
- "app": StaticFiles(directory="lnbits/extensions/lnaddress/static"),
- "name": "lnaddress_static",
- }
-]
-
-
-def lnaddress_renderer():
- return template_renderer(["lnbits/extensions/lnaddress/templates"])
-
-
-from .lnurl import * # noqa: F401,F403
-from .tasks import wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def lnaddress_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/lnaddress/cloudflare.py b/lnbits/extensions/lnaddress/cloudflare.py
deleted file mode 100644
index 558bca7d..00000000
--- a/lnbits/extensions/lnaddress/cloudflare.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import httpx
-
-from .models import Domains
-
-
-async def cloudflare_create_record(domain: Domains, ip: str):
- url = (
- "https://api.cloudflare.com/client/v4/zones/"
- + domain.cf_zone_id
- + "/dns_records"
- )
- header = {
- "Authorization": "Bearer " + domain.cf_token,
- "Content-Type": "application/json",
- }
-
- cf_response = {}
- async with httpx.AsyncClient() as client:
- try:
- r = await client.post(
- url,
- headers=header,
- json={
- "type": "CNAME",
- "name": domain.domain,
- "content": ip,
- "ttl": 0,
- "proxied": False,
- },
- timeout=40,
- )
- cf_response = r.json()
- except AssertionError:
- cf_response = {"error": "Error occured"}
- return cf_response
-
-
-async def cloudflare_deleterecord(domain: Domains, domain_id: str):
- url = (
- "https://api.cloudflare.com/client/v4/zones/"
- + domain.cf_zone_id
- + "/dns_records"
- )
- header = {
- "Authorization": "Bearer " + domain.cf_token,
- "Content-Type": "application/json",
- }
- async with httpx.AsyncClient() as client:
- try:
- r = await client.delete(url + "/" + domain_id, headers=header, timeout=40)
- cf_response = r.text
- except AssertionError:
- cf_response = "Error occured"
- return cf_response
diff --git a/lnbits/extensions/lnaddress/config.json b/lnbits/extensions/lnaddress/config.json
deleted file mode 100644
index 5eaa4948..00000000
--- a/lnbits/extensions/lnaddress/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Lightning Address",
- "short_description": "Sell LN addresses for your domain",
- "tile": "/lnaddress/static/image/lnaddress.png",
- "contributors": ["talvasconcelos"]
-}
diff --git a/lnbits/extensions/lnaddress/crud.py b/lnbits/extensions/lnaddress/crud.py
deleted file mode 100644
index a0201ee6..00000000
--- a/lnbits/extensions/lnaddress/crud.py
+++ /dev/null
@@ -1,198 +0,0 @@
-from datetime import datetime, timedelta
-from typing import List, Optional, Union
-
-from loguru import logger
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import Addresses, CreateAddress, CreateDomain, Domains
-
-
-async def create_domain(data: CreateDomain) -> Domains:
- domain_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO lnaddress.domain (id, wallet, domain, webhook, cf_token, cf_zone_id, cost)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- """,
- (
- domain_id,
- data.wallet,
- data.domain,
- data.webhook,
- data.cf_token,
- data.cf_zone_id,
- data.cost,
- ),
- )
-
- new_domain = await get_domain(domain_id)
- assert new_domain, "Newly created domain couldn't be retrieved"
- return new_domain
-
-
-async def update_domain(domain_id: str, **kwargs) -> Domains:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE lnaddress.domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id)
- )
- row = await db.fetchone("SELECT * FROM lnaddress.domain WHERE id = ?", (domain_id,))
- assert row, "Newly updated domain couldn't be retrieved"
- return Domains(**row)
-
-
-async def delete_domain(domain_id: str) -> None:
-
- await db.execute("DELETE FROM lnaddress.domain WHERE id = ?", (domain_id,))
-
-
-async def get_domain(domain_id: str) -> Optional[Domains]:
- row = await db.fetchone("SELECT * FROM lnaddress.domain WHERE id = ?", (domain_id,))
- return Domains(**row) if row else None
-
-
-async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM lnaddress.domain WHERE wallet IN ({q})", (*wallet_ids,)
- )
-
- return [Domains(**row) for row in rows]
-
-
-## ADRESSES
-
-
-async def create_address(
- payment_hash: str, wallet: str, data: CreateAddress
-) -> Addresses:
- await db.execute(
- """
- INSERT INTO lnaddress.address (id, wallet, domain, email, username, wallet_key, wallet_endpoint, sats, duration, paid)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- payment_hash,
- wallet,
- data.domain,
- data.email,
- data.username,
- data.wallet_key,
- data.wallet_endpoint,
- data.sats,
- data.duration,
- False,
- ),
- )
-
- new_address = await get_address(payment_hash)
- assert new_address, "Newly created address couldn't be retrieved"
- return new_address
-
-
-async def get_address(address_id: str) -> Optional[Addresses]:
- row = await db.fetchone(
- "SELECT a.* FROM lnaddress.address AS a INNER JOIN lnaddress.domain AS d ON a.id = ? AND a.domain = d.id",
- (address_id,),
- )
- return Addresses(**row) if row else None
-
-
-async def get_address_by_username(username: str, domain: str) -> Optional[Addresses]:
- row = await db.fetchone(
- "SELECT a.* FROM lnaddress.address AS a INNER JOIN lnaddress.domain AS d ON a.username = ? AND d.domain = ?",
- (username, domain),
- )
-
- return Addresses(**row) if row else None
-
-
-async def delete_address(address_id: str) -> None:
- await db.execute("DELETE FROM lnaddress.address WHERE id = ?", (address_id,))
-
-
-async def get_addresses(wallet_ids: Union[str, List[str]]) -> List[Addresses]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM lnaddress.address WHERE wallet IN ({q})", (*wallet_ids,)
- )
- return [Addresses(**row) for row in rows]
-
-
-async def set_address_paid(payment_hash: str) -> Addresses:
- address = await get_address(payment_hash)
- assert address
-
- if address.paid is False:
- await db.execute(
- """
- UPDATE lnaddress.address
- SET paid = true
- WHERE id = ?
- """,
- (payment_hash,),
- )
-
- new_address = await get_address(payment_hash)
- assert new_address, "Newly paid address couldn't be retrieved"
- return new_address
-
-
-async def set_address_renewed(address_id: str, duration: int):
- address = await get_address(address_id)
- assert address
-
- extend_duration = int(address.duration) + duration
- await db.execute(
- """
- UPDATE lnaddress.address
- SET duration = ?
- WHERE id = ?
- """,
- (extend_duration, address_id),
- )
- updated_address = await get_address(address_id)
- assert updated_address, "Renewed address couldn't be retrieved"
- return updated_address
-
-
-async def check_address_available(username: str, domain: str):
- (row,) = await db.fetchone(
- "SELECT COUNT(username) FROM lnaddress.address WHERE username = ? AND domain = ?",
- (username, domain),
- )
- return row
-
-
-async def purge_addresses(domain_id: str):
-
- rows = await db.fetchall(
- "SELECT * FROM lnaddress.address WHERE domain = ?", (domain_id,)
- )
-
- now = datetime.now().timestamp()
-
- for row in rows:
- r = Addresses(**row).dict()
-
- start = datetime.fromtimestamp(r["time"])
- paid = r["paid"]
- pay_expire = now > start.timestamp() + 86400 # if payment wasn't made in 1 day
- expired = (
- now > (start + timedelta(days=r["duration"] + 1)).timestamp()
- ) # give user 1 day to topup is address
-
- if not paid and pay_expire:
- logger.debug("DELETE UNP_PAY_EXP", r["username"])
- await delete_address(r["id"])
-
- if paid and expired:
- logger.debug("DELETE PAID_EXP", r["username"])
- await delete_address(r["id"])
diff --git a/lnbits/extensions/lnaddress/lnurl.py b/lnbits/extensions/lnaddress/lnurl.py
deleted file mode 100644
index b38da954..00000000
--- a/lnbits/extensions/lnaddress/lnurl.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from datetime import datetime, timedelta
-
-import httpx
-from fastapi import Query, Request
-from lnurl import LnurlErrorResponse
-from loguru import logger
-
-from . import lnaddress_ext
-from .crud import get_address, get_address_by_username, get_domain
-
-
-async def lnurl_response(username: str, domain: str, request: Request):
- address = await get_address_by_username(username, domain)
-
- if not address:
- return {"status": "ERROR", "reason": "Address not found."}
-
- ## CHECK IF USER IS STILL VALID/PAYING
- now = datetime.now().timestamp()
- start = datetime.fromtimestamp(address.time)
- expiration = (start + timedelta(days=address.duration)).timestamp()
-
- if now > expiration:
- return LnurlErrorResponse(reason="Address has expired.").dict()
-
- resp = {
- "tag": "payRequest",
- "callback": request.url_for("lnaddress.lnurl_callback", address_id=address.id),
- "metadata": await address.lnurlpay_metadata(domain=domain),
- "minSendable": 1000,
- "maxSendable": 1000000000,
- }
-
- logger.debug("RESP", resp)
- return resp
-
-
-@lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback")
-async def lnurl_callback(address_id, amount: int = Query(...)):
- address = await get_address(address_id)
- if not address:
- return LnurlErrorResponse(reason="Address not found").dict()
-
- amount_received = amount
-
- domain = await get_domain(address.domain)
- assert domain
-
- base_url = (
- address.wallet_endpoint[:-1]
- if address.wallet_endpoint.endswith("/")
- else address.wallet_endpoint
- )
-
- async with httpx.AsyncClient() as client:
- try:
- call = await client.post(
- base_url + "/api/v1/payments",
- headers={
- "X-Api-Key": address.wallet_key,
- "Content-Type": "application/json",
- },
- json={
- "out": False,
- "amount": int(amount_received / 1000),
- "description_hash": (
- await address.lnurlpay_metadata(domain=domain.domain)
- ).encode(),
- "extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
- },
- timeout=40,
- )
-
- r = call.json()
- except Exception:
- return LnurlErrorResponse(reason="ERROR")
-
- # resp = LnurlPayActionResponse(pr=r["payment_request"], routes=[])
- resp = {"pr": r["payment_request"], "routes": []}
-
- return resp
diff --git a/lnbits/extensions/lnaddress/migrations.py b/lnbits/extensions/lnaddress/migrations.py
deleted file mode 100644
index 1724e186..00000000
--- a/lnbits/extensions/lnaddress/migrations.py
+++ /dev/null
@@ -1,39 +0,0 @@
-async def m001_initial(db):
- await db.execute(
- """
- CREATE TABLE lnaddress.domain (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- domain TEXT NOT NULL,
- webhook TEXT,
- cf_token TEXT NOT NULL,
- cf_zone_id TEXT NOT NULL,
- cost INTEGER NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
-
-
-async def m002_addresses(db):
- await db.execute(
- """
- CREATE TABLE lnaddress.address (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- domain TEXT NOT NULL,
- email TEXT,
- username TEXT NOT NULL,
- wallet_key TEXT NOT NULL,
- wallet_endpoint TEXT NOT NULL,
- sats INTEGER NOT NULL,
- duration INTEGER NOT NULL,
- paid BOOLEAN NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
diff --git a/lnbits/extensions/lnaddress/models.py b/lnbits/extensions/lnaddress/models.py
deleted file mode 100644
index 77eb3cd3..00000000
--- a/lnbits/extensions/lnaddress/models.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import json
-from typing import Optional
-
-from fastapi import Query
-from lnurl.types import LnurlPayMetadata
-from pydantic import BaseModel
-
-
-class CreateDomain(BaseModel):
- wallet: str = Query(...)
- domain: str = Query(...)
- cf_token: str = Query(...)
- cf_zone_id: str = Query(...)
- webhook: str = Query(None)
- cost: int = Query(..., ge=0)
-
-
-class Domains(BaseModel):
- id: str
- wallet: str
- domain: str
- cf_token: str
- cf_zone_id: str
- webhook: Optional[str]
- cost: int
- time: int
-
-
-class CreateAddress(BaseModel):
- domain: str = Query(...)
- username: str = Query(...)
- email: str = Query(None)
- wallet_endpoint: str = Query(...)
- wallet_key: str = Query(...)
- sats: int = Query(..., ge=0)
- duration: int = Query(..., ge=1)
-
-
-class Addresses(BaseModel):
- id: str
- wallet: str
- domain: str
- email: Optional[str]
- username: str
- wallet_key: str
- wallet_endpoint: str
- sats: int
- duration: int
- paid: bool
- time: int
-
- async def lnurlpay_metadata(self, domain) -> LnurlPayMetadata:
- text = f"Payment to {self.username}"
- identifier = f"{self.username}@{domain}"
- metadata = [["text/plain", text], ["text/identifier", identifier]]
-
- return LnurlPayMetadata(json.dumps(metadata))
diff --git a/lnbits/extensions/lnaddress/static/image/lnaddress.png b/lnbits/extensions/lnaddress/static/image/lnaddress.png
deleted file mode 100644
index c94dedbc..00000000
Binary files a/lnbits/extensions/lnaddress/static/image/lnaddress.png and /dev/null differ
diff --git a/lnbits/extensions/lnaddress/tasks.py b/lnbits/extensions/lnaddress/tasks.py
deleted file mode 100644
index 3699c463..00000000
--- a/lnbits/extensions/lnaddress/tasks.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import asyncio
-
-import httpx
-from loguru import logger
-
-from lnbits.core.models import Payment
-from lnbits.helpers import get_current_extension_name
-from lnbits.tasks import register_invoice_listener
-
-from .crud import get_address, get_domain, set_address_paid, set_address_renewed
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue, get_current_extension_name())
-
- while True:
- payment = await invoice_queue.get()
- await on_invoice_paid(payment)
-
-
-async def call_webhook_on_paid(payment_hash):
- ### Use webhook to notify about cloudflare registration
- address = await get_address(payment_hash)
- assert address
- domain = await get_domain(address.domain)
- assert domain
-
- if not domain.webhook:
- return
-
- async with httpx.AsyncClient() as client:
- try:
- r = await client.post(
- domain.webhook,
- json={
- "domain": domain.domain,
- "address": address.username,
- "email": address.email,
- "cost": str(address.sats) + " sats",
- "duration": str(address.duration) + " days",
- },
- timeout=40,
- )
- r.raise_for_status()
- except Exception as e:
- logger.error(f"lnaddress: error calling webhook on paid: {str(e)}")
-
-
-async def on_invoice_paid(payment: Payment) -> None:
-
- if payment.extra.get("tag") == "lnaddress":
- await payment.set_pending(False)
- await set_address_paid(payment_hash=payment.payment_hash)
- await call_webhook_on_paid(payment_hash=payment.payment_hash)
-
- elif payment.extra.get("tag") == "renew lnaddress":
- await payment.set_pending(False)
- await set_address_renewed(
- address_id=payment.extra["id"], duration=payment.extra["duration"]
- )
- await call_webhook_on_paid(payment_hash=payment.payment_hash)
- else:
- return
diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html b/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html
deleted file mode 100644
index f4d1f651..00000000
--- a/lnbits/extensions/lnaddress/templates/lnaddress/_api_docs.html
+++ /dev/null
@@ -1,184 +0,0 @@
-
-
-
-
- lnAddress: Get paid sats to sell lightning addresses on your domains
-
-
- Charge people for using your domain name...
-
- More details
-
-
- Created by,
- talvasconcelos
-
-
-
-
-
-
-
-
-
- GET
- lnaddress/api/v1/domains
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- JSON list of users
- Curl example
- curl -X GET {{ request.base_url }}lnaddress/api/v1/domains -H
- "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- POST
- /lnAddress/api/v1/domains
- Headers
- {"X-Api-Key": <string>, "Content-type":
- "application/json"}
-
- Body (application/json) - "wallet" is a YOUR wallet ID
-
- {"wallet": <string>, "domain": <string>, "cf_token":
- <string>,"cf_zone_id": <string>,"webhook": <Optional
- string> ,"cost": <integer>}
-
- Returns 201 CREATED (application/json)
-
- {"id": <string>, "wallet": <string>, "domain":
- <string>, "webhook": <string>, "cf_token": <string>,
- "cf_zone_id": <string>, "cost": <integer>}
- Curl example
- curl -X POST {{ request.base_url }}lnaddress/api/v1/domains -d
- '{"wallet": "{{ user.wallets[0].id }}", "domain": <string>,
- "cf_token": <string>,"cf_zone_id": <string>,"webhook":
- <Optional string> ,"cost": <integer>}' -H "X-Api-Key: {{
- user.wallets[0].inkey }}" -H "Content-type: application/json"
-
-
-
-
-
-
-
- DELETE
- /lnaddress/api/v1/domains/<domain_id>
- Headers
- {"X-Api-Key": <string>}
- Curl example
- curl -X DELETE {{ request.base_url
- }}lnaddress/api/v1/domains/<domain_id> -H "X-Api-Key: {{
- user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- GET
- lnaddress/api/v1/addresses
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- JSON list of addresses
- Curl example
- curl -X GET {{ request.base_url }}lnaddress/api/v1/addresses -H
- "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- GET
- lnaddress/api/v1/address/<domain>/<username>/<wallet_key>
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- JSON list of addresses
- Curl example
- curl -X GET {{ request.base_url
- }}lnaddress/api/v1/address/<domain>/<username>/<wallet_key>
- -H "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- POST
- /lnaddress/api/v1/address/<domain_id>
- Headers
- {"X-Api-Key": <string>}
- Curl example
- curl -X POST {{ request.base_url
- }}lnaddress/api/v1/address/<domain_id> -d '{"domain":
- <string>, "username": <string>,"email": <Optional
- string>, "wallet_endpoint": <string>, "wallet_key":
- <string>, "sats": <integer> "duration": <integer>,}'
- -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
- application/json"
-
-
-
-
-
diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/display.html b/lnbits/extensions/lnaddress/templates/lnaddress/display.html
deleted file mode 100644
index 2dca8f48..00000000
--- a/lnbits/extensions/lnaddress/templates/lnaddress/display.html
+++ /dev/null
@@ -1,435 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
-
- tab = val.name"
- >
- tab = val.name"
- >
-
-
-
-
-
-
-
- {{ domain_domain }}
-
-
- Your Lightning Address: {% raw
- %}{{this.formDialog.data.username}}{% endraw %}@{{domain_domain}}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cost per day: {{ domain_cost }} sats
- {% raw %} Total cost: {{amountSats}} sats {% endraw %}
-
-
- Submit
- Cancel
-
-
-
-
-
-
- {{ domain_domain }}
-
-
- Renew your Lightning Address: {% raw
- %}{{this.formDialog.data.username}}{% endraw %}@{{domain_domain}}
-
-
-
-
-
-
-
-
-
- {% raw %}
-
- LN Address:
- {{renewDialog.data.username}}@{{renewDialog.data.domain}}
-
- Expires at: {{renewDialog.data.expiration}}
-
- {% endraw %}
-
-
Get Info
-
-
-
-
- Cost per day: {{ domain_cost }} sats
- {% raw %} Total cost: {{amountSats}} sats {% endraw %}
-
-
- Submit
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Copy invoice
- Close
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/lnaddress/templates/lnaddress/index.html b/lnbits/extensions/lnaddress/templates/lnaddress/index.html
deleted file mode 100644
index 41602581..00000000
--- a/lnbits/extensions/lnaddress/templates/lnaddress/index.html
+++ /dev/null
@@ -1,504 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- Add Domain
-
-
-
-
-
-
-
Domains
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
Addresses
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} LN Address extension
-
-
-
-
- {% include "lnaddress/_api_docs.html" %}
-
-
-
-
-
-
-
-
- The domain to use ex: "example.com"
-
-
- Check extension
- documentation!
-
- Your API key in cloudflare
-
-
- Create a "Edit zone DNS" API token in cloudflare
-
-
- How much to charge per day
-
- Update Form
- Create Domain
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/lnaddress/views.py b/lnbits/extensions/lnaddress/views.py
deleted file mode 100644
index d1a7be83..00000000
--- a/lnbits/extensions/lnaddress/views.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from http import HTTPStatus
-from urllib.parse import urlparse
-
-from fastapi import Depends, HTTPException, Request
-from fastapi.templating import Jinja2Templates
-from starlette.responses import HTMLResponse
-
-from lnbits.core.crud import get_wallet
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import lnaddress_ext, lnaddress_renderer
-from .crud import get_domain, purge_addresses
-
-templates = Jinja2Templates(directory="templates")
-
-
-@lnaddress_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return lnaddress_renderer().TemplateResponse(
- "lnaddress/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@lnaddress_ext.get("/{domain_id}", response_class=HTMLResponse)
-async def display(domain_id, request: Request):
- domain = await get_domain(domain_id)
- if not domain:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
- )
-
- await purge_addresses(domain_id)
-
- wallet = await get_wallet(domain.wallet)
- assert wallet
- url = urlparse(str(request.url))
-
- return lnaddress_renderer().TemplateResponse(
- "lnaddress/display.html",
- {
- "request": request,
- "domain_id": domain.id,
- "domain_domain": domain.domain,
- "domain_cost": domain.cost,
- "domain_wallet_inkey": wallet.inkey,
- "root_url": f"{url.scheme}://{url.netloc}",
- },
- )
diff --git a/lnbits/extensions/lnaddress/views_api.py b/lnbits/extensions/lnaddress/views_api.py
deleted file mode 100644
index cdf5b91f..00000000
--- a/lnbits/extensions/lnaddress/views_api.py
+++ /dev/null
@@ -1,258 +0,0 @@
-from http import HTTPStatus
-from urllib.parse import urlparse
-
-from fastapi import Depends, HTTPException, Query, Request
-from loguru import logger
-
-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 lnaddress_ext
-from .cloudflare import cloudflare_create_record
-from .crud import (
- check_address_available,
- create_address,
- create_domain,
- delete_address,
- delete_domain,
- get_address,
- get_address_by_username,
- get_addresses,
- get_domain,
- get_domains,
- update_domain,
-)
-from .models import CreateAddress, CreateDomain
-
-
-# DOMAINS
-@lnaddress_ext.get("/api/v1/domains")
-async def api_domains(
- 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)
- wallet_ids = user.wallet_ids if user else []
-
- return [domain.dict() for domain in await get_domains(wallet_ids)]
-
-
-@lnaddress_ext.post("/api/v1/domains")
-@lnaddress_ext.put("/api/v1/domains/{domain_id}")
-async def api_domain_create(
- request: Request,
- data: CreateDomain,
- domain_id=None,
- g: WalletTypeInfo = Depends(get_key_type),
-):
- if domain_id:
- domain = await get_domain(domain_id)
-
- if not domain:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
- )
-
- if domain.wallet != g.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your domain"
- )
-
- domain = await update_domain(domain_id, **data.dict())
- else:
-
- domain = await create_domain(data=data)
- root_url = urlparse(str(request.url)).netloc
-
- cf_response = await cloudflare_create_record(domain=domain, ip=root_url)
-
- if not cf_response or not cf_response["success"]:
- await delete_domain(domain.id)
- logger.error("Cloudflare failed with: " + cf_response["errors"][0]["message"]) # type: ignore
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="Problem with cloudflare."
- )
-
- return domain.dict()
-
-
-@lnaddress_ext.delete("/api/v1/domains/{domain_id}")
-async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
- domain = await get_domain(domain_id)
-
- if not domain:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Domain does not exist."
- )
-
- if domain.wallet != g.wallet.id:
- raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain")
-
- await delete_domain(domain_id)
- return "", HTTPStatus.NO_CONTENT
-
-
-# ADDRESSES
-
-
-@lnaddress_ext.get("/api/v1/addresses")
-async def api_addresses(
- 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)
- wallet_ids = user.wallet_ids if user else []
-
- return [address.dict() for address in await get_addresses(wallet_ids)]
-
-
-@lnaddress_ext.get("/api/v1/address/availabity/{domain_id}/{username}")
-async def api_check_available_username(domain_id, username):
- used_username = await check_address_available(username, domain_id)
-
- return used_username
-
-
-@lnaddress_ext.get("/api/v1/address/{domain}/{username}/{wallet_key}")
-async def api_get_user_info(username, wallet_key, domain):
- address = await get_address_by_username(username, domain)
-
- if not address:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Address does not exist."
- )
-
- if address.wallet_key != wallet_key:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Incorrect user/wallet information.",
- )
-
- return address.dict()
-
-
-@lnaddress_ext.post("/api/v1/address/{domain_id}")
-@lnaddress_ext.put("/api/v1/address/{domain_id}/{user}/{wallet_key}")
-async def api_lnaddress_make_address(
- domain_id, data: CreateAddress, user=None, wallet_key=None
-):
- domain = await get_domain(domain_id)
-
- # If the request is coming for the non-existant domain
- if not domain:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="The domain does not exist."
- )
-
- domain_cost = domain.cost
- sats = data.sats
-
- ## FAILSAFE FOR CREATING ADDRESSES BY API
- if domain_cost * data.duration != data.sats:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="The amount is not correct. Either 'duration', or 'sats' are wrong.",
- )
-
- if user:
- address = await get_address_by_username(user, domain.domain)
-
- if not address:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="The address does not exist."
- )
-
- if address.wallet_key != wallet_key:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your address."
- )
-
- try:
- payment_hash, payment_request = await create_invoice(
- wallet_id=domain.wallet,
- amount=data.sats,
- memo=f"Renew {data.username}@{domain.domain} for {sats} sats for {data.duration} more days",
- extra={
- "tag": "renew lnaddress",
- "id": address.id,
- "duration": data.duration,
- },
- )
-
- except Exception as e:
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
- )
- else:
- used_username = await check_address_available(data.username, data.domain)
- # If username is already taken
- if used_username:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail="Alias/username already taken.",
- )
-
- ## ALL OK - create an invoice and return it to the user
-
- try:
- payment_hash, payment_request = await create_invoice(
- wallet_id=domain.wallet,
- amount=sats,
- memo=f"LNAddress {data.username}@{domain.domain} for {sats} sats for {data.duration} days",
- extra={"tag": "lnaddress"},
- )
- except Exception as e:
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
- )
-
- address = await create_address(
- payment_hash=payment_hash, wallet=domain.wallet, data=data
- )
-
- if not address:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="LNAddress could not be fetched.",
- )
-
- return {"payment_hash": payment_hash, "payment_request": payment_request}
-
-
-@lnaddress_ext.get("/api/v1/addresses/{payment_hash}")
-async def api_address_send_address(payment_hash):
- address = await get_address(payment_hash)
- assert address
- domain = await get_domain(address.domain)
- assert domain
- try:
- status = await check_transaction_status(domain.wallet, payment_hash)
- is_paid = not status.pending
- except Exception as e:
- return {"paid": False, "error": str(e)}
-
- if is_paid:
- return {"paid": True}
-
- return {"paid": False}
-
-
-@lnaddress_ext.delete("/api/v1/addresses/{address_id}")
-async def api_address_delete(address_id, g: WalletTypeInfo = Depends(get_key_type)):
- address = await get_address(address_id)
- if not address:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Address does not exist."
- )
- if address.wallet != g.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your address."
- )
-
- await delete_address(address_id)
- return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/lndhub/README.md b/lnbits/extensions/lndhub/README.md
deleted file mode 100644
index ddd2020a..00000000
--- a/lnbits/extensions/lndhub/README.md
+++ /dev/null
@@ -1,6 +0,0 @@
-lndhub Extension
-*connect to your lnbits wallet from BlueWallet or Zeus*
-
-Lndhub has nothing to do with lnd, it is just the name of the HTTP/JSON protocol https://bluewallet.io/ uses to talk to their Lightning custodian server at https://lndhub.io/.
-
-Despite not having been planned to this, Lndhub because somewhat a standard for custodian wallet communication when https://t.me/lntxbot and https://zeusln.app/ implemented the same interface. And with this extension LNbits joins the same club.
diff --git a/lnbits/extensions/lndhub/__init__.py b/lnbits/extensions/lndhub/__init__.py
deleted file mode 100644
index 344e91c6..00000000
--- a/lnbits/extensions/lndhub/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from fastapi import APIRouter
-from starlette.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_lndhub")
-
-lndhub_ext: APIRouter = APIRouter(prefix="/lndhub", tags=["lndhub"])
-
-lndhub_static_files = [
- {
- "path": "/lndhub/static",
- "app": StaticFiles(directory="lnbits/extensions/lndhub/static"),
- "name": "lndhub_static",
- }
-]
-
-
-def lndhub_renderer():
- return template_renderer(["lnbits/extensions/lndhub/templates"])
-
-
-from .decorators import * # noqa: F401,F403
-from .utils import * # noqa: F401,F403
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/lndhub/config.json b/lnbits/extensions/lndhub/config.json
deleted file mode 100644
index 30a2ce59..00000000
--- a/lnbits/extensions/lndhub/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "LndHub",
- "short_description": "Access lnbits from BlueWallet or Zeus",
- "tile": "/lndhub/static/image/lndhub.png",
- "contributors": ["fiatjaf"]
-}
diff --git a/lnbits/extensions/lndhub/decorators.py b/lnbits/extensions/lndhub/decorators.py
deleted file mode 100644
index 48118087..00000000
--- a/lnbits/extensions/lndhub/decorators.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from base64 import b64decode
-
-from fastapi import Request, status
-from fastapi.param_functions import Security
-from fastapi.security.api_key import APIKeyHeader
-from starlette.exceptions import HTTPException
-
-from lnbits.decorators import WalletTypeInfo, get_key_type
-
-api_key_header_auth = APIKeyHeader(
- name="AUTHORIZATION",
- auto_error=False,
- description="Admin or Invoice key for LNDHub API's",
-)
-
-
-async def check_wallet(
- r: Request, api_key_header_auth: str = Security(api_key_header_auth)
-) -> WalletTypeInfo:
- if not api_key_header_auth:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth key"
- )
-
- t = api_key_header_auth.split(" ")[1]
- _, token = b64decode(t).decode().split(":")
-
- return await get_key_type(r, api_key_header=token)
-
-
-async def require_admin_key(
- r: Request, api_key_header_auth: str = Security(api_key_header_auth)
-):
- wallet = await check_wallet(r, api_key_header_auth)
- if wallet.wallet_type != 0:
- # If wallet type is not admin then return the unauthorized status
- # This also covers when the user passes an invalid key type
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED, detail="Admin key required."
- )
- else:
- return wallet
diff --git a/lnbits/extensions/lndhub/migrations.py b/lnbits/extensions/lndhub/migrations.py
deleted file mode 100644
index d6ea5fde..00000000
--- a/lnbits/extensions/lndhub/migrations.py
+++ /dev/null
@@ -1,2 +0,0 @@
-async def migrate():
- pass
diff --git a/lnbits/extensions/lndhub/static/image/lndhub.png b/lnbits/extensions/lndhub/static/image/lndhub.png
deleted file mode 100644
index f5e95a6e..00000000
Binary files a/lnbits/extensions/lndhub/static/image/lndhub.png and /dev/null differ
diff --git a/lnbits/extensions/lndhub/templates/lndhub/_instructions.html b/lnbits/extensions/lndhub/templates/lndhub/_instructions.html
deleted file mode 100644
index a5eba8a2..00000000
--- a/lnbits/extensions/lndhub/templates/lndhub/_instructions.html
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
- To access an LNbits wallet from a mobile phone,
-
-
- Install either
- Zeus or
- BlueWallet ;
-
-
- Go to Add a wallet / Import wallet on BlueWallet or
- Settings / Add a new node on Zeus.
-
- Select the desired wallet on this page;
- Scan one of the two QR codes from the mobile wallet.
-
-
-
- Invoice URLs mean the mobile wallet will only have the
- authorization to read your payments and invoices and generate new
- invoices.
-
-
- Admin URLs mean the mobile wallet will be able to pay
- invoices..
-
-
-
-
-
-
diff --git a/lnbits/extensions/lndhub/templates/lndhub/_lndhub.html b/lnbits/extensions/lndhub/templates/lndhub/_lndhub.html
deleted file mode 100644
index 73097dbf..00000000
--- a/lnbits/extensions/lndhub/templates/lndhub/_lndhub.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
- LndHub is a protocol invented by
- BlueWallet
- that allows mobile wallets to query payments and balances, generate
- invoices and make payments from accounts that exist on a server. The
- protocol is a collection of HTTP endpoints exposed through the internet.
-
-
- For a wallet that supports it, reading a QR code that contains the URL
- along with secret access credentials should enable access. Currently it
- is supported by
- Zeus and
- BlueWallet .
-
-
-
-
diff --git a/lnbits/extensions/lndhub/templates/lndhub/index.html b/lnbits/extensions/lndhub/templates/lndhub/index.html
deleted file mode 100644
index fc666da9..00000000
--- a/lnbits/extensions/lndhub/templates/lndhub/index.html
+++ /dev/null
@@ -1,94 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %} {% raw %}
-
-
-
-
-
-
-
- Copy LndHub {{type}} URL
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
- {{SITE_TITLE}} LndHub extension
-
-
-
-
-
- {% include "lndhub/_instructions.html" %}
-
- {% include "lndhub/_lndhub.html" %}
-
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/lndhub/utils.py b/lnbits/extensions/lndhub/utils.py
deleted file mode 100644
index 00865080..00000000
--- a/lnbits/extensions/lndhub/utils.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from lnbits.bolt11 import Invoice
-
-
-def to_buffer(payment_hash: str):
- return {"type": "Buffer", "data": [b for b in bytes.fromhex(payment_hash)]}
-
-
-def decoded_as_lndhub(invoice: Invoice):
- return {
- "destination": invoice.payee,
- "payment_hash": invoice.payment_hash,
- "num_satoshis": invoice.amount_msat / 1000,
- "timestamp": str(invoice.date),
- "expiry": str(invoice.expiry),
- "description": invoice.description,
- "fallback_addr": "",
- "cltv_expiry": invoice.min_final_cltv_expiry,
- "route_hints": "",
- }
diff --git a/lnbits/extensions/lndhub/views.py b/lnbits/extensions/lndhub/views.py
deleted file mode 100644
index b216f8b1..00000000
--- a/lnbits/extensions/lndhub/views.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from fastapi import Depends, Request
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import lndhub_ext, lndhub_renderer
-
-
-@lndhub_ext.get("/")
-async def lndhub_index(request: Request, user: User = Depends(check_user_exists)):
- return lndhub_renderer().TemplateResponse(
- "lndhub/index.html", {"request": request, "user": user.dict()}
- )
diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py
deleted file mode 100644
index 059604f2..00000000
--- a/lnbits/extensions/lndhub/views_api.py
+++ /dev/null
@@ -1,230 +0,0 @@
-import time
-from base64 import urlsafe_b64encode
-from http import HTTPStatus
-
-from fastapi import Depends, Query
-from pydantic import BaseModel
-from starlette.exceptions import HTTPException
-
-from lnbits import bolt11
-from lnbits.core.crud import get_payments
-from lnbits.core.services import create_invoice, pay_invoice
-from lnbits.decorators import WalletTypeInfo
-from lnbits.settings import get_wallet_class, settings
-
-from . import lndhub_ext
-from .decorators import check_wallet, require_admin_key
-from .utils import decoded_as_lndhub, to_buffer
-
-
-@lndhub_ext.get("/ext/getinfo")
-async def lndhub_getinfo():
- return {"alias": settings.lnbits_site_title}
-
-
-class AuthData(BaseModel):
- login: str = Query(None)
- password: str = Query(None)
- refresh_token: str = Query(None)
-
-
-@lndhub_ext.post("/ext/auth")
-async def lndhub_auth(data: AuthData):
- token = (
- data.refresh_token
- if data.refresh_token
- else urlsafe_b64encode((data.login + ":" + data.password).encode()).decode(
- "ascii"
- )
- )
- return {"refresh_token": token, "access_token": token}
-
-
-class AddInvoice(BaseModel):
- amt: str = Query(...)
- memo: str = Query(...)
- preimage: str = Query(None)
-
-
-@lndhub_ext.post("/ext/addinvoice")
-async def lndhub_addinvoice(
- data: AddInvoice, wallet: WalletTypeInfo = Depends(check_wallet)
-):
- try:
- _, pr = await create_invoice(
- wallet_id=wallet.wallet.id,
- amount=int(data.amt),
- memo=data.memo or settings.lnbits_site_title,
- extra={"tag": "lndhub"},
- )
- except:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice"
- )
- invoice = bolt11.decode(pr)
- return {
- "pay_req": pr,
- "payment_request": pr,
- "add_index": "500",
- "r_hash": to_buffer(invoice.payment_hash),
- "hash": invoice.payment_hash,
- }
-
-
-class CreateInvoice(BaseModel):
- invoice: str = Query(...)
-
-
-@lndhub_ext.post("/ext/payinvoice")
-async def lndhub_payinvoice(
- r_invoice: CreateInvoice, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- try:
- await pay_invoice(
- wallet_id=wallet.wallet.id,
- payment_request=r_invoice.invoice,
- extra={"tag": "lndhub"},
- )
- except:
- raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Payment failed")
-
- invoice: bolt11.Invoice = bolt11.decode(r_invoice.invoice)
-
- return {
- "payment_error": "",
- "payment_preimage": "0" * 64,
- "route": {},
- "payment_hash": invoice.payment_hash,
- "decoded": decoded_as_lndhub(invoice),
- "fee_msat": 0,
- "type": "paid_invoice",
- "fee": 0,
- "value": invoice.amount_msat / 1000,
- "timestamp": int(time.time()),
- "memo": invoice.description,
- }
-
-
-@lndhub_ext.get("/ext/balance")
-async def lndhub_balance(
- wallet: WalletTypeInfo = Depends(check_wallet),
-):
- return {"BTC": {"AvailableBalance": wallet.wallet.balance}}
-
-
-@lndhub_ext.get("/ext/gettxs")
-async def lndhub_gettxs(
- wallet: WalletTypeInfo = Depends(check_wallet),
- limit: int = Query(20, ge=1, le=20),
- offset: int = Query(0, ge=0),
-):
- for payment in await get_payments(
- wallet_id=wallet.wallet.id,
- complete=False,
- pending=True,
- outgoing=True,
- incoming=False,
- limit=limit,
- offset=offset,
- exclude_uncheckable=True,
- ):
- await payment.check_status()
-
- return [
- {
- "payment_preimage": payment.preimage,
- "payment_hash": payment.payment_hash,
- "fee_msat": payment.fee * 1000,
- "type": "paid_invoice",
- "fee": payment.fee,
- "value": int(payment.amount / 1000),
- "timestamp": payment.time,
- "memo": payment.memo if not payment.pending else "Payment in transition",
- }
- for payment in reversed(
- (
- await get_payments(
- wallet_id=wallet.wallet.id,
- pending=True,
- complete=True,
- outgoing=True,
- incoming=False,
- limit=limit,
- offset=offset,
- )
- )
- )
- ]
-
-
-@lndhub_ext.get("/ext/getuserinvoices")
-async def lndhub_getuserinvoices(
- wallet: WalletTypeInfo = Depends(check_wallet),
- limit: int = Query(20, ge=1, le=20),
- offset: int = Query(0, ge=0),
-):
- WALLET = get_wallet_class()
- for invoice in await get_payments(
- wallet_id=wallet.wallet.id,
- complete=False,
- pending=True,
- outgoing=False,
- incoming=True,
- limit=limit,
- offset=offset,
- exclude_uncheckable=True,
- ):
- await invoice.set_pending(
- (await WALLET.get_invoice_status(invoice.checking_id)).pending
- )
-
- return [
- {
- "r_hash": to_buffer(invoice.payment_hash),
- "payment_request": invoice.bolt11,
- "add_index": "500",
- "description": invoice.memo,
- "payment_hash": invoice.payment_hash,
- "ispaid": not invoice.pending,
- "amt": int(invoice.amount / 1000),
- "expire_time": int(time.time() + 1800),
- "timestamp": invoice.time,
- "type": "user_invoice",
- }
- for invoice in reversed(
- (
- await get_payments(
- wallet_id=wallet.wallet.id,
- pending=True,
- complete=True,
- incoming=True,
- outgoing=False,
- limit=limit,
- offset=offset,
- )
- )
- )
- ]
-
-
-@lndhub_ext.get("/ext/getbtc")
-async def lndhub_getbtc(wallet: WalletTypeInfo = Depends(check_wallet)):
- "load an address for incoming onchain btc"
- return []
-
-
-@lndhub_ext.get("/ext/getpending")
-async def lndhub_getpending(wallet: WalletTypeInfo = Depends(check_wallet)):
- "pending onchain transactions"
- return []
-
-
-@lndhub_ext.get("/ext/decodeinvoice")
-async def lndhub_decodeinvoice(invoice: str = Query(None)):
- inv = bolt11.decode(invoice)
- return decoded_as_lndhub(inv)
-
-
-@lndhub_ext.get("/ext/checkrouteinvoice")
-async def lndhub_checkrouteinvoice():
- "not implemented on canonical lndhub"
diff --git a/lnbits/extensions/lnurlp/README.md b/lnbits/extensions/lnurlp/README.md
deleted file mode 100644
index 0832bfb7..00000000
--- a/lnbits/extensions/lnurlp/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# LNURLp
-
-## Create a static QR code people can use to pay over Lightning Network
-
-LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL-pay is a link that wallets use to fetch an invoice from a server on-demand. The link or QR code is fixed, but each time it is read by a compatible wallet a new invoice is issued by the service and sent to the wallet.
-
-[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
-
-## Usage
-
-1. Create an LNURLp (New Pay link)\
- 
-
- - select your wallets
- - make a small description
- - enter amount
- - if _Fixed amount_ is unchecked you'll have the option to configure a Max and Min amount
- - you can set the currency to something different than sats. For example if you choose EUR, the satoshi amount will be calculated when a user scans the LNURLp
- - You can ask the user to send a comment that will be sent along with the payment (for example a comment to a blog post)
- - Webhook URL allows to call an URL when the LNURLp is paid
- - Success mesage, will send a message back to the user after a successful payment, for example a thank you note
- - Success URL, will send back a clickable link to the user. Access to some hidden content, or a download link
-
-2. Use the shareable link or view the LNURLp you just created\
- 
- - you can now open your LNURLp and copy the LNURL, get the shareable link or print it\
- 
diff --git a/lnbits/extensions/lnurlp/__init__.py b/lnbits/extensions/lnurlp/__init__.py
deleted file mode 100644
index f5ea0cd2..00000000
--- a/lnbits/extensions/lnurlp/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-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_lnurlp")
-
-lnurlp_static_files = [
- {
- "path": "/lnurlp/static",
- "app": StaticFiles(packages=[("lnbits", "extensions/lnurlp/static")]),
- "name": "lnurlp_static",
- }
-]
-
-lnurlp_ext: APIRouter = APIRouter(prefix="/lnurlp", tags=["lnurlp"])
-
-
-def lnurlp_renderer():
- return template_renderer(["lnbits/extensions/lnurlp/templates"])
-
-
-from .lnurl import * # noqa: F401,F403
-from .tasks import wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def lnurlp_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/lnurlp/config.json b/lnbits/extensions/lnurlp/config.json
deleted file mode 100644
index d3e046de..00000000
--- a/lnbits/extensions/lnurlp/config.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "LNURLp",
- "short_description": "Make reusable LNURL pay links",
- "tile": "/lnurlp/static/image/lnurl-pay.png",
- "contributors": [
- "arcbtc",
- "eillarra",
- "fiatjaf"
- ]
-}
diff --git a/lnbits/extensions/lnurlp/crud.py b/lnbits/extensions/lnurlp/crud.py
deleted file mode 100644
index 4acb4a41..00000000
--- a/lnbits/extensions/lnurlp/crud.py
+++ /dev/null
@@ -1,95 +0,0 @@
-from typing import List, Optional, Union
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import CreatePayLinkData, PayLink
-
-
-async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
- link_id = urlsafe_short_hash()[:6]
-
- result = await db.execute(
- """
- INSERT INTO lnurlp.pay_links (
- id,
- wallet,
- description,
- min,
- max,
- served_meta,
- served_pr,
- webhook_url,
- webhook_headers,
- webhook_body,
- success_text,
- success_url,
- comment_chars,
- currency,
- fiat_base_multiplier
- )
- VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- link_id,
- wallet_id,
- data.description,
- data.min,
- data.max,
- data.webhook_url,
- data.webhook_headers,
- data.webhook_body,
- data.success_text,
- data.success_url,
- data.comment_chars,
- data.currency,
- data.fiat_base_multiplier,
- ),
- )
- assert result
-
- link = await get_pay_link(link_id)
- assert link, "Newly created link couldn't be retrieved"
- return link
-
-
-async def get_pay_link(link_id: str) -> Optional[PayLink]:
- row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
- return PayLink.from_row(row) if row else None
-
-
-async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"""
- SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q})
- ORDER BY Id
- """,
- (*wallet_ids,),
- )
- return [PayLink.from_row(row) for row in rows]
-
-
-async def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
- )
- row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
- return PayLink.from_row(row) if row else None
-
-
-async def increment_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
- q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
- )
- row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
- return PayLink.from_row(row) if row else None
-
-
-async def delete_pay_link(link_id: int) -> None:
- await db.execute("DELETE FROM lnurlp.pay_links WHERE id = ?", (link_id,))
diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py
deleted file mode 100644
index 918a5bd3..00000000
--- a/lnbits/extensions/lnurlp/lnurl.py
+++ /dev/null
@@ -1,106 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Request
-from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse
-from starlette.exceptions import HTTPException
-
-from lnbits.core.services import create_invoice
-from lnbits.utils.exchange_rates import get_fiat_rate_satoshis
-
-from . import lnurlp_ext
-from .crud import increment_pay_link
-
-
-@lnurlp_ext.get(
- "/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes (with long URL)
- status_code=HTTPStatus.OK,
- name="lnurlp.api_lnurl_response.deprecated",
-)
-@lnurlp_ext.get(
- "/{link_id}",
- status_code=HTTPStatus.OK,
- name="lnurlp.api_lnurl_response",
-)
-async def api_lnurl_response(request: Request, link_id):
- link = await increment_pay_link(link_id, served_meta=1)
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
- )
-
- rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
-
- resp = LnurlPayResponse(
- callback=request.url_for("lnurlp.api_lnurl_callback", link_id=link.id),
- min_sendable=round(link.min * rate) * 1000,
- max_sendable=round(link.max * rate) * 1000,
- metadata=link.lnurlpay_metadata,
- )
- params = resp.dict()
-
- if link.comment_chars > 0:
- params["commentAllowed"] = link.comment_chars
-
- return params
-
-
-@lnurlp_ext.get(
- "/api/v1/lnurl/cb/{link_id}",
- status_code=HTTPStatus.OK,
- name="lnurlp.api_lnurl_callback",
-)
-async def api_lnurl_callback(request: Request, link_id):
- link = await increment_pay_link(link_id, served_pr=1)
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
- )
- min, max = link.min, link.max
- rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
- if link.currency:
- # allow some fluctuation (as the fiat price may have changed between the calls)
- min = rate * 995 * link.min
- max = rate * 1010 * link.max
- else:
- min = link.min * 1000
- max = link.max * 1000
-
- amount_received = int(request.query_params.get("amount") or 0)
- if amount_received < min:
- return LnurlErrorResponse(
- reason=f"Amount {amount_received} is smaller than minimum {min}."
- ).dict()
-
- elif amount_received > max:
- return LnurlErrorResponse(
- reason=f"Amount {amount_received} is greater than maximum {max}."
- ).dict()
-
- comment = request.query_params.get("comment")
- if len(comment or "") > link.comment_chars:
- return LnurlErrorResponse(
- reason=f"Got a comment with {len(comment)} characters, but can only accept {link.comment_chars}"
- ).dict()
-
- payment_hash, payment_request = await create_invoice(
- wallet_id=link.wallet,
- amount=int(amount_received / 1000),
- memo=link.description,
- unhashed_description=link.lnurlpay_metadata.encode(),
- extra={
- "tag": "lnurlp",
- "link": link.id,
- "comment": comment,
- "extra": request.query_params.get("amount"),
- },
- )
-
- success_action = link.success_action(payment_hash)
- if success_action:
- resp = LnurlPayActionResponse(
- pr=payment_request, success_action=success_action, routes=[]
- )
- else:
- resp = LnurlPayActionResponse(pr=payment_request, routes=[])
-
- return resp.dict()
diff --git a/lnbits/extensions/lnurlp/migrations.py b/lnbits/extensions/lnurlp/migrations.py
deleted file mode 100644
index 1ec85eb0..00000000
--- a/lnbits/extensions/lnurlp/migrations.py
+++ /dev/null
@@ -1,148 +0,0 @@
-async def m001_initial(db):
- """
- Initial pay table.
- """
- await db.execute(
- f"""
- CREATE TABLE lnurlp.pay_links (
- id {db.serial_primary_key},
- wallet TEXT NOT NULL,
- description TEXT NOT NULL,
- amount {db.big_int} NOT NULL,
- served_meta INTEGER NOT NULL,
- served_pr INTEGER NOT NULL
- );
- """
- )
-
-
-async def m002_webhooks_and_success_actions(db):
- """
- Webhooks and success actions.
- """
- await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_url TEXT;")
- await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_text TEXT;")
- await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_url TEXT;")
- await db.execute(
- f"""
- CREATE TABLE lnurlp.invoices (
- pay_link INTEGER NOT NULL REFERENCES {db.references_schema}pay_links (id),
- payment_hash TEXT NOT NULL,
- webhook_sent INT, -- null means not sent, otherwise store status
- expiry INT
- );
- """
- )
-
-
-async def m003_min_max_comment_fiat(db):
- """
- Support for min/max amounts, comments and fiat prices that get
- converted automatically to satoshis based on some API.
- """
- await db.execute(
- "ALTER TABLE lnurlp.pay_links ADD COLUMN currency TEXT;"
- ) # null = satoshis
- await db.execute(
- "ALTER TABLE lnurlp.pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;"
- )
- await db.execute("ALTER TABLE lnurlp.pay_links RENAME COLUMN amount TO min;")
- await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN max INTEGER;")
- await db.execute("UPDATE lnurlp.pay_links SET max = min;")
- await db.execute("DROP TABLE lnurlp.invoices")
-
-
-async def m004_fiat_base_multiplier(db):
- """
- Store the multiplier for fiat prices. We store the price in cents and
- remember to multiply by 100 when we use it to convert to Dollars.
- """
- await db.execute(
- "ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;"
- )
-
-
-async def m005_webhook_headers_and_body(db):
- """
- Add headers and body to webhooks
- """
- await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
- await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
-
-
-async def m006_redux(db):
- """
- Migrate ID column type to string for UUIDs and migrate existing data
- """
- # we can simply change the column type for postgres
- if db.type != "SQLITE":
- await db.execute("ALTER TABLE lnurlp.pay_links ALTER COLUMN id TYPE TEXT;")
- else:
- # but we have to do this for sqlite
- await db.execute("ALTER TABLE lnurlp.pay_links RENAME TO pay_links_old")
- await db.execute(
- f"""
- CREATE TABLE lnurlp.pay_links (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- description TEXT NOT NULL,
- min {db.big_int} NOT NULL,
- max {db.big_int},
- currency TEXT,
- fiat_base_multiplier INTEGER DEFAULT 1,
- served_meta INTEGER NOT NULL,
- served_pr INTEGER NOT NULL,
- webhook_url TEXT,
- success_text TEXT,
- success_url TEXT,
- comment_chars INTEGER DEFAULT 0,
- webhook_headers TEXT,
- webhook_body TEXT
- );
- """
- )
-
- for row in [
- list(row) for row in await db.fetchall("SELECT * FROM lnurlp.pay_links_old")
- ]:
- await db.execute(
- """
- INSERT INTO lnurlp.pay_links (
- id,
- wallet,
- description,
- min,
- served_meta,
- served_pr,
- webhook_url,
- success_text,
- success_url,
- currency,
- comment_chars,
- max,
- fiat_base_multiplier,
- webhook_headers,
- webhook_body
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- row[0],
- row[1],
- row[2],
- row[3],
- row[4],
- row[5],
- row[6],
- row[7],
- row[8],
- row[9],
- row[10],
- row[11],
- row[12],
- row[13],
- row[14],
- ),
- )
-
- await db.execute("DROP TABLE lnurlp.pay_links_old")
diff --git a/lnbits/extensions/lnurlp/models.py b/lnbits/extensions/lnurlp/models.py
deleted file mode 100644
index 4ee82aad..00000000
--- a/lnbits/extensions/lnurlp/models.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import json
-from sqlite3 import Row
-from typing import Dict, Optional
-from urllib.parse import ParseResult, urlparse, urlunparse
-
-from fastapi.param_functions import Query
-from lnurl.types import LnurlPayMetadata
-from pydantic import BaseModel
-from starlette.requests import Request
-
-from lnbits.lnurl import encode as lnurl_encode
-
-
-class CreatePayLinkData(BaseModel):
- description: str
- min: float = Query(1, ge=0.01)
- max: float = Query(1, ge=0.01)
- currency: str = Query(None)
- comment_chars: int = Query(0, ge=0, lt=800)
- webhook_url: str = Query(None)
- webhook_headers: str = Query(None)
- webhook_body: str = Query(None)
- success_text: str = Query(None)
- success_url: str = Query(None)
- fiat_base_multiplier: int = Query(100, ge=1)
-
-
-class PayLink(BaseModel):
- id: str
- wallet: str
- description: str
- min: float
- served_meta: int
- served_pr: int
- webhook_url: Optional[str]
- webhook_headers: Optional[str]
- webhook_body: Optional[str]
- success_text: Optional[str]
- success_url: Optional[str]
- currency: Optional[str]
- comment_chars: int
- max: float
- fiat_base_multiplier: int
-
- @classmethod
- def from_row(cls, row: Row) -> "PayLink":
- data = dict(row)
- if data["currency"] and data["fiat_base_multiplier"]:
- data["min"] /= data["fiat_base_multiplier"]
- data["max"] /= data["fiat_base_multiplier"]
- return cls(**data)
-
- def lnurl(self, req: Request) -> str:
- url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id)
- return lnurl_encode(url)
-
- @property
- def lnurlpay_metadata(self) -> LnurlPayMetadata:
- return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))
-
- def success_action(self, payment_hash: str) -> Optional[Dict]:
- if self.success_url:
- url: ParseResult = urlparse(self.success_url)
- #qs = parse_qs(url.query)
- #setattr(qs, "payment_hash", payment_hash)
- #url = url._replace(query=urlencode(qs, doseq=True))
- return {
- "tag": "url",
- "description": self.success_text or "~",
- "url": urlunparse(url),
- }
- elif self.success_text:
- return {"tag": "message", "message": self.success_text}
- else:
- return None
diff --git a/lnbits/extensions/lnurlp/static/image/lnurl-pay.png b/lnbits/extensions/lnurlp/static/image/lnurl-pay.png
deleted file mode 100644
index 36af81a7..00000000
Binary files a/lnbits/extensions/lnurlp/static/image/lnurl-pay.png and /dev/null differ
diff --git a/lnbits/extensions/lnurlp/static/js/index.js b/lnbits/extensions/lnurlp/static/js/index.js
deleted file mode 100644
index c1372bec..00000000
--- a/lnbits/extensions/lnurlp/static/js/index.js
+++ /dev/null
@@ -1,264 +0,0 @@
-/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
-
-Vue.component(VueQrcode.name, VueQrcode)
-
-var locationPath = [
- window.location.protocol,
- '//',
- window.location.host,
- window.location.pathname
-].join('')
-
-var mapPayLink = obj => {
- obj._data = _.clone(obj)
- obj.date = Quasar.utils.date.formatDate(
- new Date(obj.time * 1000),
- 'YYYY-MM-DD HH:mm'
- )
- obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
- obj.print_url = [locationPath, 'print/', obj.id].join('')
- obj.pay_url = [locationPath, 'link/', obj.id].join('')
- return obj
-}
-
-new Vue({
- el: '#vue',
- mixins: [windowMixin],
- data() {
- return {
- currencies: [],
- fiatRates: {},
- checker: null,
- payLinks: [],
- payLinksTable: {
- pagination: {
- rowsPerPage: 10
- }
- },
- nfcTagWriting: false,
- formDialog: {
- show: false,
- fixedAmount: true,
- data: {}
- },
- qrCodeDialog: {
- show: false,
- data: null
- }
- }
- },
- methods: {
- getPayLinks() {
- LNbits.api
- .request(
- 'GET',
- '/lnurlp/api/v1/links?all_wallets=true',
- this.g.user.wallets[0].inkey
- )
- .then(response => {
- this.payLinks = response.data.map(mapPayLink)
- })
- .catch(err => {
- clearInterval(this.checker)
- LNbits.utils.notifyApiError(err)
- })
- },
- closeFormDialog() {
- this.resetFormData()
- },
- openQrCodeDialog(linkId) {
- var link = _.findWhere(this.payLinks, {id: linkId})
- if (link.currency) this.updateFiatRate(link.currency)
-
- this.qrCodeDialog.data = {
- id: link.id,
- amount:
- (link.min === link.max ? link.min : `${link.min} - ${link.max}`) +
- ' ' +
- (link.currency || 'sat'),
- currency: link.currency,
- comments: link.comment_chars
- ? `${link.comment_chars} characters`
- : 'no',
- webhook: link.webhook_url || 'nowhere',
- success:
- link.success_text || link.success_url
- ? 'Display message "' +
- link.success_text +
- '"' +
- (link.success_url ? ' and URL "' + link.success_url + '"' : '')
- : 'do nothing',
- lnurl: link.lnurl,
- pay_url: link.pay_url,
- print_url: link.print_url
- }
- this.qrCodeDialog.show = true
- },
- openUpdateDialog(linkId) {
- const link = _.findWhere(this.payLinks, {id: linkId})
- if (link.currency) this.updateFiatRate(link.currency)
-
- this.formDialog.data = _.clone(link._data)
- this.formDialog.show = true
- this.formDialog.fixedAmount =
- this.formDialog.data.min === this.formDialog.data.max
- },
- sendFormData() {
- const wallet = _.findWhere(this.g.user.wallets, {
- id: this.formDialog.data.wallet
- })
- var data = _.omit(this.formDialog.data, 'wallet')
-
- if (this.formDialog.fixedAmount) data.max = data.min
- if (data.currency === 'satoshis') data.currency = null
- if (isNaN(parseInt(data.comment_chars))) data.comment_chars = 0
-
- if (data.id) {
- this.updatePayLink(wallet, data)
- } else {
- this.createPayLink(wallet, data)
- }
- },
- resetFormData() {
- this.formDialog = {
- show: false,
- fixedAmount: true,
- data: {}
- }
- },
- updatePayLink(wallet, data) {
- let values = _.omit(
- _.pick(
- data,
- 'description',
- 'min',
- 'max',
- 'webhook_url',
- 'success_text',
- 'success_url',
- 'comment_chars',
- 'currency'
- ),
- (value, key) =>
- (key === 'webhook_url' ||
- key === 'success_text' ||
- key === 'success_url') &&
- (value === null || value === '')
- )
-
- LNbits.api
- .request(
- 'PUT',
- '/lnurlp/api/v1/links/' + data.id,
- wallet.adminkey,
- values
- )
- .then(response => {
- this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
- this.payLinks.push(mapPayLink(response.data))
- this.formDialog.show = false
- this.resetFormData()
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- },
- createPayLink(wallet, data) {
- LNbits.api
- .request('POST', '/lnurlp/api/v1/links', wallet.adminkey, data)
- .then(response => {
- this.getPayLinks()
- this.formDialog.show = false
- this.resetFormData()
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- },
- deletePayLink(linkId) {
- var link = _.findWhere(this.payLinks, {id: linkId})
-
- LNbits.utils
- .confirmDialog('Are you sure you want to delete this pay link?')
- .onOk(() => {
- LNbits.api
- .request(
- 'DELETE',
- '/lnurlp/api/v1/links/' + linkId,
- _.findWhere(this.g.user.wallets, {id: link.wallet}).adminkey
- )
- .then(response => {
- this.payLinks = _.reject(this.payLinks, obj => obj.id === linkId)
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- })
- },
- updateFiatRate(currency) {
- LNbits.api
- .request('GET', '/lnurlp/api/v1/rate/' + currency, null)
- .then(response => {
- let rates = _.clone(this.fiatRates)
- rates[currency] = response.data.rate
- this.fiatRates = rates
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- },
- writeNfcTag: async function (lnurl) {
- try {
- if (typeof NDEFReader == 'undefined') {
- throw {
- toString: function () {
- return 'NFC not supported on this device or browser.'
- }
- }
- }
-
- const ndef = new NDEFReader()
-
- this.nfcTagWriting = true
- this.$q.notify({
- message: 'Tap your NFC tag to write the LNURL-pay link to it.'
- })
-
- await ndef.write({
- records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}]
- })
-
- this.nfcTagWriting = false
- this.$q.notify({
- type: 'positive',
- message: 'NFC tag written successfully.'
- })
- } catch (error) {
- this.nfcTagWriting = false
- this.$q.notify({
- type: 'negative',
- message: error
- ? error.toString()
- : 'An unexpected error has occurred.'
- })
- }
- }
- },
- created() {
- if (this.g.user.wallets.length) {
- var getPayLinks = this.getPayLinks
- getPayLinks()
- this.checker = setInterval(() => {
- getPayLinks()
- }, 20000)
- }
- LNbits.api
- .request('GET', '/lnurlp/api/v1/currencies')
- .then(response => {
- this.currencies = ['satoshis', ...response.data]
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- }
-})
diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py
deleted file mode 100644
index ea01e04f..00000000
--- a/lnbits/extensions/lnurlp/tasks.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import asyncio
-import json
-
-import httpx
-from loguru import logger
-
-from lnbits.core.crud import update_payment_extra
-from lnbits.core.models import Payment
-from lnbits.helpers import get_current_extension_name
-from lnbits.tasks import register_invoice_listener
-
-from .crud import get_pay_link
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue, get_current_extension_name())
-
- while True:
- payment = await invoice_queue.get()
- await on_invoice_paid(payment)
-
-
-async def on_invoice_paid(payment: Payment):
- if payment.extra.get("tag") != "lnurlp":
- return
-
- if payment.extra.get("wh_status"):
- # this webhook has already been sent
- return
-
- pay_link = await get_pay_link(payment.extra.get("link", -1))
- if pay_link and pay_link.webhook_url:
- async with httpx.AsyncClient() as client:
- try:
- r: httpx.Response = await client.post(
- pay_link.webhook_url,
- json={
- "payment_hash": payment.payment_hash,
- "payment_request": payment.bolt11,
- "amount": payment.amount,
- "comment": payment.extra.get("comment"),
- "lnurlp": pay_link.id,
- "body": json.loads(pay_link.webhook_body)
- if pay_link.webhook_body
- else "",
- },
- headers=json.loads(pay_link.webhook_headers)
- if pay_link.webhook_headers
- else None,
- timeout=40,
- )
- await mark_webhook_sent(
- payment.payment_hash,
- r.status_code,
- r.is_success,
- r.reason_phrase,
- r.text,
- )
- except Exception as ex:
- logger.error(ex)
- await mark_webhook_sent(
- payment.payment_hash, -1, False, "Unexpected Error", str(ex)
- )
-
-
-async def mark_webhook_sent(
- payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
-) -> None:
-
- await update_payment_extra(
- payment_hash,
- {
- "wh_status": status, # keep for backwards compability
- "wh_success": is_success,
- "wh_message": reason_phrase,
- "wh_response": text,
- },
- )
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html b/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html
deleted file mode 100644
index abb37e90..00000000
--- a/lnbits/extensions/lnurlp/templates/lnurlp/_api_docs.html
+++ /dev/null
@@ -1,138 +0,0 @@
-
-
-
-
-
- GET /lnurlp/api/v1/links
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<pay_link_object>, ...]
- Curl example
- curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key:
- {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- GET
- /lnurlp/api/v1/links/<pay_id>
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 201 CREATED (application/json)
-
- {"lnurl": <string>}
- Curl example
- curl -X GET {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
- -H "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
-
- POST /lnurlp/api/v1/links
- Headers
- {"X-Api-Key": <admin_key>}
- Body (application/json)
- {"description": <string> "amount": <integer> "max":
- <integer> "min": <integer> "comment_chars":
- <integer>}
-
- Returns 201 CREATED (application/json)
-
- {"lnurl": <string>}
- Curl example
- curl -X POST {{ request.base_url }}lnurlp/api/v1/links -d
- '{"description": <string>, "amount": <integer>, "max":
- <integer>, "min": <integer>, "comment_chars":
- <integer>}' -H "Content-type: application/json" -H "X-Api-Key:
- {{ user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- PUT
- /lnurlp/api/v1/links/<pay_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Body (application/json)
- {"description": <string>, "amount": <integer>}
-
- Returns 200 OK (application/json)
-
- {"lnurl": <string>}
- Curl example
- curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
- -d '{"description": <string>, "amount": <integer>}' -H
- "Content-type: application/json" -H "X-Api-Key: {{
- user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- DELETE
- /lnurlp/api/v1/links/<pay_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Returns 204 NO CONTENT
-
- Curl example
- curl -X DELETE {{ request.base_url
- }}lnurlp/api/v1/links/<pay_id> -H "X-Api-Key: {{
- user.wallets[0].adminkey }}"
-
-
-
-
-
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/_lnurl.html b/lnbits/extensions/lnurlp/templates/lnurlp/_lnurl.html
deleted file mode 100644
index f2ba8661..00000000
--- a/lnbits/extensions/lnurlp/templates/lnurlp/_lnurl.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
- WARNING: LNURL must be used over https or TOR
- LNURL is a range of lightning-network standards that allow us to use
- lightning-network differently. An LNURL-pay is a link that wallets use
- to fetch an invoice from a server on-demand. The link or QR code is
- fixed, but each time it is read by a compatible wallet a new QR code is
- issued by the service. It can be used to activate machines without them
- having to maintain an electronic screen to generate and show invoices
- locally, or to sell any predefined good or service automatically.
-
-
- Exploring LNURL and finding use cases, is really helping inform
- lightning protocol development, rather than the protocol dictating how
- lightning-network should be engaged with.
-
- Check
- Awesome LNURL
- for further information.
-
-
-
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/display.html b/lnbits/extensions/lnurlp/templates/lnurlp/display.html
deleted file mode 100644
index 7d440378..00000000
--- a/lnbits/extensions/lnurlp/templates/lnurlp/display.html
+++ /dev/null
@@ -1,54 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
-
- Copy LNURL
-
-
-
-
-
-
-
-
- LNbits LNURL-pay link
- Use an LNURL compatible bitcoin wallet to pay.
-
-
-
- {% include "lnurlp/_lnurl.html" %}
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/index.html b/lnbits/extensions/lnurlp/templates/lnurlp/index.html
deleted file mode 100644
index 3fbd3446..00000000
--- a/lnbits/extensions/lnurlp/templates/lnurlp/index.html
+++ /dev/null
@@ -1,345 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New pay link
-
-
-
-
-
-
-
- {% raw %}
-
-
-
- Description
- Amount
- Currency
-
-
-
-
-
-
-
-
-
-
- {{ props.row.description }}
-
-
- {{ props.row.min }}
-
- {{ props.row.min }} - {{ props.row.max }}
-
- {{ props.row.currency || 'sat' }}
-
-
- Webhook to {{ props.row.webhook_url}}
-
-
-
- On success, show message '{{ props.row.success_text }}'
- and URL '{{ props.row.success_url }}'
-
-
-
-
- {{ props.row.comment_chars }}-char comment allowed
-
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} LNURL-pay extension
-
-
-
-
-
- {% include "lnurlp/_api_docs.html" %}
-
- {% include "lnurlp/_lnurl.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update pay link
- Create pay link
- Cancel
-
-
-
-
-
-
-
- {% raw %}
-
-
-
-
- ID: {{ qrCodeDialog.data.id }}
- Amount: {{ qrCodeDialog.data.amount }}
- {{ qrCodeDialog.data.currency }} price: {{
- fiatRates[qrCodeDialog.data.currency] ?
- fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}
- Accepts comments: {{ qrCodeDialog.data.comments }}
- Dispatches webhook to: {{ qrCodeDialog.data.webhook
- }}
- On success: {{ qrCodeDialog.data.success }}
-
- {% endraw %}
-
- Copy LNURL
- Copy sharable link
-
- Write to NFC
-
- Print
- Close
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html b/lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html
deleted file mode 100644
index 5f3129d6..00000000
--- a/lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends "print.html" %} {% block page %}
-
-{% endblock %} {% block styles %}
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/lnurlp/views.py b/lnbits/extensions/lnurlp/views.py
deleted file mode 100644
index c5fa3582..00000000
--- a/lnbits/extensions/lnurlp/views.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Request
-from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
-from starlette.responses import HTMLResponse
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import lnurlp_ext, lnurlp_renderer
-from .crud import get_pay_link
-
-templates = Jinja2Templates(directory="templates")
-
-
-@lnurlp_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return lnurlp_renderer().TemplateResponse(
- "lnurlp/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@lnurlp_ext.get("/link/{link_id}", response_class=HTMLResponse)
-async def display(request: Request, link_id):
- link = await get_pay_link(link_id)
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
- )
- ctx = {"request": request, "lnurl": link.lnurl(req=request)}
- return lnurlp_renderer().TemplateResponse("lnurlp/display.html", ctx)
-
-
-@lnurlp_ext.get("/print/{link_id}", response_class=HTMLResponse)
-async def print_qr(request: Request, link_id):
- link = await get_pay_link(link_id)
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
- )
- ctx = {"request": request, "lnurl": link.lnurl(req=request)}
- return lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", ctx)
diff --git a/lnbits/extensions/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py
deleted file mode 100644
index badaaebf..00000000
--- a/lnbits/extensions/lnurlp/views_api.py
+++ /dev/null
@@ -1,168 +0,0 @@
-import json
-from http import HTTPStatus
-
-from fastapi import Depends, Query, Request
-from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_user
-from lnbits.decorators import WalletTypeInfo, get_key_type
-from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
-
-from . import lnurlp_ext
-from .crud import (
- create_pay_link,
- delete_pay_link,
- get_pay_link,
- get_pay_links,
- update_pay_link,
-)
-from .models import CreatePayLinkData
-
-
-@lnurlp_ext.get("/api/v1/currencies")
-async def api_list_currencies_available():
- return list(currencies.keys())
-
-
-@lnurlp_ext.get("/api/v1/links", status_code=HTTPStatus.OK)
-async def api_links(
- req: Request,
- wallet: WalletTypeInfo = Depends(get_key_type),
- all_wallets: bool = Query(False),
-):
- wallet_ids = [wallet.wallet.id]
-
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- try:
- return [
- {**link.dict(), "lnurl": link.lnurl(req)}
- for link in await get_pay_links(wallet_ids)
- ]
-
- except LnurlInvalidUrl:
- raise HTTPException(
- status_code=HTTPStatus.UPGRADE_REQUIRED,
- detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.",
- )
-
-
-@lnurlp_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_retrieve(
- r: Request, link_id, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- link = await get_pay_link(link_id)
-
- if not link:
- raise HTTPException(
- detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
- )
-
- return {**link.dict(), **{"lnurl": link.lnurl(r)}}
-
-
-@lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
-@lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_create_or_update(
- data: CreatePayLinkData,
- request: Request,
- link_id=None,
- wallet: WalletTypeInfo = Depends(get_key_type),
-):
-
- if data.min > data.max:
- raise HTTPException(
- detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST
- )
-
- if data.currency is None and (
- round(data.min) != data.min or round(data.max) != data.max or data.min < 1
- ):
- raise HTTPException(
- detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST
- )
-
- if data.webhook_headers:
- try:
- json.loads(data.webhook_headers)
- except ValueError:
- raise HTTPException(
- detail="Invalid JSON in webhook_headers.",
- status_code=HTTPStatus.BAD_REQUEST,
- )
-
- if data.webhook_body:
- try:
- json.loads(data.webhook_body)
- except ValueError:
- raise HTTPException(
- detail="Invalid JSON in webhook_body.",
- status_code=HTTPStatus.BAD_REQUEST,
- )
-
- # database only allows int4 entries for min and max. For fiat currencies,
- # we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
- if data.currency and data.fiat_base_multiplier:
- data.min *= data.fiat_base_multiplier
- data.max *= data.fiat_base_multiplier
-
- if data.success_url is not None and not data.success_url.startswith("https://"):
- raise HTTPException(
- detail="Success URL must be secure https://...",
- status_code=HTTPStatus.BAD_REQUEST,
- )
-
- if link_id:
- link = await get_pay_link(link_id)
-
- if not link:
- raise HTTPException(
- detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
- )
-
- link = await update_pay_link(**data.dict(), link_id=link_id)
- else:
- link = await create_pay_link(data, wallet_id=wallet.wallet.id)
- assert link
- return {**link.dict(), "lnurl": link.lnurl(request)}
-
-
-@lnurlp_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type)):
- link = await get_pay_link(link_id)
-
- if not link:
- raise HTTPException(
- detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
- )
-
- await delete_pay_link(link_id)
- return {"success": True}
-
-
-@lnurlp_ext.get("/api/v1/rate/{currency}", status_code=HTTPStatus.OK)
-async def api_check_fiat_rate(currency):
- try:
- rate = await get_fiat_rate_satoshis(currency)
- except AssertionError:
- rate = None
-
- return {"rate": rate}
diff --git a/lnbits/extensions/market/README.md b/lnbits/extensions/market/README.md
deleted file mode 100644
index bbd8fc63..00000000
--- a/lnbits/extensions/market/README.md
+++ /dev/null
@@ -1,283 +0,0 @@
-## Nostr Diagon Alley protocol (for resilient marketplaces)
-
-`authur: Ben Arc`
-
-#### Original protocol https://github.com/lnbits/Diagon-Alley
-
-> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR!
-
-In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked.
-
-A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events.
-
-#### For further information about NOSTR, see https://github.com/nostr-protocol/nostr
-
-
-## Terms
-
-* `merchant` - seller of products with NOSTR key-pair
-* `customer` - buyer of products with NOSTR key-pair
-* `product` - item for sale by the `merchant`
-* `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls)
-* `marketplace` - clientside software for searching `stalls` and purchasing `products`
-
-## Diagon Alley Clients
-
-### Merchant admin
-
-Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`.
-
-The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events.
-
-### Marketplace
-
-`Marketplace` software should be entirely clientside, either as a stand-alone app, or as a purely frontend webpage. A `customer` subscribes to different merchant NOSTR public keys, and those `merchants` `stalls` and `products` become listed and searchable. The marketplace client is like any other ecommerce site, with basket and checkout. `Marketplaces` may also wish to include a `customer` support area for direct message communication with `merchants`.
-
-## `Merchant` publishing/updating products (event)
-
-NIP-01 https://github.com/nostr-protocol/nips/blob/master/01.md uses the basic NOSTR event type.
-
-The `merchant` event that publishes and updates product lists
-
-The below json goes in `content` of NIP-01.
-
-Data from newer events should replace data from older events.
-
-`action` types (used to indicate changes):
-* `update` element has changed
-* `delete` element should be deleted
-* `suspend` element is suspended
-* `unsuspend` element is unsuspended
-
-
-```
-{
- "name": ,
- "description": ,
- "currency": ,
- "action": ,
- "shipping": [
- {
- "id": ,
- "zones": ,
- "price": ,
- },
- {
- "id": ,
- "zones": ,
- "price": ,
- },
- {
- "id": ,
- "zones": ,
- "price": ,
- }
- ],
- "stalls": [
- {
- "id": ,
- "name": ,
- "description": ,
- "categories": ,
- "shipping": ,
- "action": ,
- "products": [
- {
- "id": ,
- "name": ,
- "description": ,
- "categories": ,
- "amount": ,
- "price": ,
- "images": [
- {
- "id": ,
- "name": ,
- "link":
- }
- ],
- "action": ,
- },
- {
- "id": ,
- "name": ,
- "description": ,
- "categories": ,
- "amount": ,
- "price": ,
- "images": [
- {
- "id": ,
- "name": ,
- "link":
- },
- {
- "id": ,
- "name": ,
- "link":
- }
- ],
- "action": ,
- },
- ]
- },
- {
- "id": ,
- "name": ,
- "description": ,
- "categories": ,
- "shipping": ,
- "action": ,
- "products": [
- {
- "id": ,
- "name": ,
- "categories": ,
- "amount": ,
- "price": ,
- "images": [
- {
- "id": ,
- "name": ,
- "link":
- }
- ],
- "action": ,
- }
- ]
- }
- ]
-}
-
-```
-
-As all elements are optional, an `update` `action` to a `product` `image`, may look as simple as:
-
-```
-{
- "stalls": [
- {
- "id": ,
- "products": [
- {
- "id": ,
- "images": [
- {
- "id": ,
- "name": ,
- "link":
- }
- ],
- "action": ,
- },
- ]
- }
- ]
-}
-
-```
-
-
-## Checkout events
-
-NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted
-
-The below json goes in `content` of NIP-04.
-
-### Step 1: `customer` order (event)
-
-
-```
-{
- "id": ,
- "name": ,
- "description": ,
- "address": ,
- "message": ,
- "contact": [
- "nostr": ,
- "phone": ,
- "email":
- ],
- "items": [
- {
- "id": ,
- "quantity": ,
- "message":
- },
- {
- "id": ,
- "quantity": ,
- "message":
- },
- {
- "id": ,
- "quantity": ,
- "message":
- }
-
-}
-
-```
-
-Merchant should verify the sum of product ids + timestamp.
-
-### Step 2: `merchant` request payment (event)
-
-Sent back from the merchant for payment. Any payment option is valid that the merchant can check.
-
-The below json goes in `content` of NIP-04.
-
-`payment_options`/`type` include:
-* `url` URL to a payment page, stripe, paypal, btcpayserver, etc
-* `btc` onchain bitcoin address
-* `ln` bitcoin lightning invoice
-* `lnurl` bitcoin lnurl-pay
-
-```
-{
- "id": ,
- "message": ,
- "payment_options": [
- {
- "type": ,
- "link":
- },
- {
- "type": ,
- "link":
- },
- {
- "type": ,
- "link":
- }
-}
-
-```
-
-### Step 3: `merchant` verify payment/shipped (event)
-
-Once payment has been received and processed.
-
-The below json goes in `content` of NIP-04.
-
-```
-{
- "id": ,
- "message": ,
- "paid": ,
- "shipped": ,
-}
-
-```
-
-## Customer support events
-
-Customer support is handle over whatever communication method was specified. If communicationg via nostr, NIP-04 is used https://github.com/nostr-protocol/nips/blob/master/04.md.
-
-## Additional
-
-Standard data models can be found here here
-
-
-
diff --git a/lnbits/extensions/market/__init__.py b/lnbits/extensions/market/__init__.py
deleted file mode 100644
index a14fe6af..00000000
--- a/lnbits/extensions/market/__init__.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import asyncio
-
-from fastapi import APIRouter
-from starlette.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_market")
-
-market_ext: APIRouter = APIRouter(prefix="/market", tags=["market"])
-
-market_static_files = [
- {
- "path": "/market/static",
- "app": StaticFiles(directory="lnbits/extensions/market/static"),
- "name": "market_static",
- }
-]
-
-# if 'nostradmin' not in LNBITS_ADMIN_EXTENSIONS:
-# @market_ext.get("/", response_class=HTMLResponse)
-# async def index(request: Request):
-# return template_renderer().TemplateResponse(
-# "error.html", {"request": request, "err": "Ask system admin to enable NostrAdmin!"}
-# )
-# else:
-
-
-def market_renderer():
- return template_renderer(["lnbits/extensions/market/templates"])
- # return template_renderer(["lnbits/extensions/market/templates"])
-
-
-from .tasks import wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def market_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/market/config.json b/lnbits/extensions/market/config.json
deleted file mode 100644
index 8a294867..00000000
--- a/lnbits/extensions/market/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Marketplace",
- "short_description": "Webshop/market on LNbits",
- "tile": "/market/static/images/bitcoin-shop.png",
- "contributors": ["benarc", "talvasconcelos"]
-}
diff --git a/lnbits/extensions/market/crud.py b/lnbits/extensions/market/crud.py
deleted file mode 100644
index c2a6ace1..00000000
--- a/lnbits/extensions/market/crud.py
+++ /dev/null
@@ -1,488 +0,0 @@
-from typing import List, Optional, Union
-
-# from lnbits.db import open_ext_db
-from lnbits.db import SQLITE
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import (
- ChatMessage,
- CreateChatMessage,
- CreateMarket,
- Market,
- MarketSettings,
- OrderDetail,
- Orders,
- Products,
- Stalls,
- Zones,
- createOrder,
- createOrderDetails,
- createProduct,
- createStalls,
- createZones,
-)
-
-###Products
-
-
-async def create_market_product(data: createProduct) -> Products:
- product_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO market.products (id, stall, product, categories, description, image, price, quantity)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- product_id,
- data.stall,
- data.product,
- data.categories,
- data.description,
- data.image,
- data.price,
- data.quantity,
- ),
- )
- product = await get_market_product(product_id)
- assert product, "Newly created product couldn't be retrieved"
- return product
-
-
-async def update_market_product(product_id: str, **kwargs) -> Optional[Products]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
-
- await db.execute(
- f"UPDATE market.products SET {q} WHERE id = ?",
- (*kwargs.values(), product_id),
- )
- row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,))
-
- return Products(**row) if row else None
-
-
-async def get_market_product(product_id: str) -> Optional[Products]:
- row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,))
- return Products(**row) if row else None
-
-
-async def get_market_products(stall_ids: Union[str, List[str]]) -> List[Products]:
- if isinstance(stall_ids, str):
- stall_ids = [stall_ids]
-
- # with open_ext_db("market") as db:
- q = ",".join(["?"] * len(stall_ids))
- rows = await db.fetchall(
- f"""
- SELECT * FROM market.products WHERE stall IN ({q})
- """,
- (*stall_ids,),
- )
- return [Products(**row) for row in rows]
-
-
-async def delete_market_product(product_id: str) -> None:
- await db.execute("DELETE FROM market.products WHERE id = ?", (product_id,))
-
-
-###zones
-
-
-async def create_market_zone(user, data: createZones) -> Zones:
- zone_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO market.zones (
- id,
- "user",
- cost,
- countries
-
- )
- VALUES (?, ?, ?, ?)
- """,
- (zone_id, user, data.cost, data.countries.lower()),
- )
-
- zone = await get_market_zone(zone_id)
- assert zone, "Newly created zone couldn't be retrieved"
- return zone
-
-
-async def update_market_zone(zone_id: str, **kwargs) -> Optional[Zones]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE market.zones SET {q} WHERE id = ?",
- (*kwargs.values(), zone_id),
- )
- row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,))
- return Zones(**row) if row else None
-
-
-async def get_market_zone(zone_id: str) -> Optional[Zones]:
- row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,))
- return Zones(**row) if row else None
-
-
-async def get_market_zones(user: str) -> List[Zones]:
- rows = await db.fetchall('SELECT * FROM market.zones WHERE "user" = ?', (user,))
- return [Zones(**row) for row in rows]
-
-
-async def delete_market_zone(zone_id: str) -> None:
- await db.execute("DELETE FROM market.zones WHERE id = ?", (zone_id,))
-
-
-###Stalls
-
-
-async def create_market_stall(data: createStalls) -> Stalls:
- stall_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO market.stalls (
- id,
- wallet,
- name,
- currency,
- publickey,
- relays,
- shippingzones
- )
- VALUES (?, ?, ?, ?, ?, ?, ?)
- """,
- (
- stall_id,
- data.wallet,
- data.name,
- data.currency,
- data.publickey,
- data.relays,
- data.shippingzones,
- ),
- )
-
- stall = await get_market_stall(stall_id)
- assert stall, "Newly created stall couldn't be retrieved"
- return stall
-
-
-async def update_market_stall(stall_id: str, **kwargs) -> Optional[Stalls]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE market.stalls SET {q} WHERE id = ?",
- (*kwargs.values(), stall_id),
- )
- row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
- return Stalls(**row) if row else None
-
-
-async def get_market_stall(stall_id: str) -> Optional[Stalls]:
- row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
- return Stalls(**row) if row else None
-
-
-async def get_market_stalls(wallet_ids: Union[str, List[str]]) -> List[Stalls]:
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM market.stalls WHERE wallet IN ({q})", (*wallet_ids,)
- )
- return [Stalls(**row) for row in rows]
-
-
-async def get_market_stalls_by_ids(stall_ids: Union[str, List[str]]) -> List[Stalls]:
- q = ",".join(["?"] * len(stall_ids))
- rows = await db.fetchall(
- f"SELECT * FROM market.stalls WHERE id IN ({q})", (*stall_ids,)
- )
- return [Stalls(**row) for row in rows]
-
-
-async def delete_market_stall(stall_id: str) -> None:
- await db.execute("DELETE FROM market.stalls WHERE id = ?", (stall_id,))
-
-
-###Orders
-
-
-async def create_market_order(data: createOrder, invoiceid: str):
- returning = "" if db.type == SQLITE else "RETURNING ID"
- method = db.execute if db.type == SQLITE else db.fetchone
-
- result = await (method)(
- f"""
- INSERT INTO market.orders (wallet, shippingzone, address, email, total, invoiceid, paid, shipped)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- {returning}
- """,
- (
- data.wallet,
- data.shippingzone,
- data.address,
- data.email,
- data.total,
- invoiceid,
- False,
- False,
- ),
- )
- if db.type == SQLITE:
- return result._result_proxy.lastrowid
- else:
- return result[0]
-
-
-async def create_market_order_details(order_id: str, data: List[createOrderDetails]):
- for item in data:
- item_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO market.order_details (id, order_id, product_id, quantity)
- VALUES (?, ?, ?, ?)
- """,
- (
- item_id,
- order_id,
- item.product_id,
- item.quantity,
- ),
- )
- order_details = await get_market_order_details(order_id)
- return order_details
-
-
-async def get_market_order_details(order_id: str) -> List[OrderDetail]:
- rows = await db.fetchall(
- "SELECT * FROM market.order_details WHERE order_id = ?", (order_id,)
- )
-
- return [OrderDetail(**row) for row in rows]
-
-
-async def get_market_order(order_id: str) -> Optional[Orders]:
- row = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,))
- return Orders(**row) if row else None
-
-
-async def get_market_order_invoiceid(invoice_id: str) -> Optional[Orders]:
- row = await db.fetchone(
- "SELECT * FROM market.orders WHERE invoiceid = ?", (invoice_id,)
- )
- return Orders(**row) if row else None
-
-
-async def set_market_order_paid(payment_hash: str):
- await db.execute(
- """
- UPDATE market.orders
- SET paid = true
- WHERE invoiceid = ?
- """,
- (payment_hash,),
- )
-
-
-async def set_market_order_pubkey(payment_hash: str, pubkey: str):
- await db.execute(
- """
- UPDATE market.orders
- SET pubkey = ?
- WHERE invoiceid = ?
- """,
- (
- pubkey,
- payment_hash,
- ),
- )
-
-
-async def update_market_product_stock(products):
-
- q = "\n".join(
- [f"""WHEN id='{p.product_id}' THEN quantity - {p.quantity}""" for p in products]
- )
- v = ",".join(["?"] * len(products))
-
- await db.execute(
- f"""
- UPDATE market.products
- SET quantity=(CASE
- {q}
- END)
- WHERE id IN ({v});
- """,
- (*[p.product_id for p in products],),
- )
-
-
-async def get_market_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM market.orders WHERE wallet IN ({q})", (*wallet_ids,)
- )
- #
- return [Orders(**row) for row in rows]
-
-
-async def delete_market_order(order_id: str) -> None:
- await db.execute("DELETE FROM market.orders WHERE id = ?", (order_id,))
-
-
-### Market/Marketplace
-
-
-async def get_market_markets(user: str) -> List[Market]:
- rows = await db.fetchall("SELECT * FROM market.markets WHERE usr = ?", (user,))
- return [Market(**row) for row in rows]
-
-
-async def get_market_market(market_id: str) -> Optional[Market]:
- row = await db.fetchone("SELECT * FROM market.markets WHERE id = ?", (market_id,))
- return Market(**row) if row else None
-
-
-async def get_market_market_stalls(market_id: str):
- rows = await db.fetchall(
- "SELECT * FROM market.market_stalls WHERE marketid = ?", (market_id,)
- )
-
- ids = [row["stallid"] for row in rows]
-
- return await get_market_stalls_by_ids(ids)
-
-
-async def create_market_market(data: CreateMarket):
- market_id = urlsafe_short_hash()
-
- await db.execute(
- """
- INSERT INTO market.markets (id, usr, name)
- VALUES (?, ?, ?)
- """,
- (
- market_id,
- data.usr,
- data.name,
- ),
- )
- market = await get_market_market(market_id)
- assert market, "Newly created market couldn't be retrieved"
- return market
-
-
-async def create_market_market_stalls(market_id: str, data: List[str]):
- for stallid in data:
- id = urlsafe_short_hash()
-
- await db.execute(
- """
- INSERT INTO market.market_stalls (id, marketid, stallid)
- VALUES (?, ?, ?)
- """,
- (
- id,
- market_id,
- stallid,
- ),
- )
- market_stalls = await get_market_market_stalls(market_id)
- return market_stalls
-
-
-async def update_market_market(market_id: str, name: str):
- await db.execute(
- "UPDATE market.markets SET name = ? WHERE id = ?",
- (name, market_id),
- )
- await db.execute(
- "DELETE FROM market.market_stalls WHERE marketid = ?",
- (market_id,),
- )
-
- market = await get_market_market(market_id)
- return market
-
-
-### CHAT / MESSAGES
-
-
-async def create_chat_message(data: CreateChatMessage):
- await db.execute(
- """
- INSERT INTO market.messages (msg, pubkey, id_conversation)
- VALUES (?, ?, ?)
- """,
- (
- data.msg,
- data.pubkey,
- data.room_name,
- ),
- )
-
-
-async def get_market_latest_chat_messages(room_name: str):
- rows = await db.fetchall(
- "SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC LIMIT 20",
- (room_name,),
- )
-
- return [ChatMessage(**row) for row in rows]
-
-
-async def get_market_chat_messages(room_name: str):
- rows = await db.fetchall(
- "SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC",
- (room_name,),
- )
-
- return [ChatMessage(**row) for row in rows]
-
-
-async def get_market_chat_by_merchant(ids: List[str]) -> List[ChatMessage]:
-
- q = ",".join(["?"] * len(ids))
- rows = await db.fetchall(
- f"SELECT * FROM market.messages WHERE id_conversation IN ({q})",
- (*ids,),
- )
- return [ChatMessage(**row) for row in rows]
-
-
-async def get_market_settings(user) -> Optional[MarketSettings]:
- row = await db.fetchone(
- """SELECT * FROM market.settings WHERE "user" = ?""", (user,)
- )
-
- return MarketSettings(**row) if row else None
-
-
-async def create_market_settings(user: str, data):
- await db.execute(
- """
- INSERT INTO market.settings ("user", currency, fiat_base_multiplier)
- VALUES (?, ?, ?)
- """,
- (
- user,
- data.currency,
- data.fiat_base_multiplier,
- ),
- )
-
-
-async def set_market_settings(user: str, data):
- await db.execute(
- """
- UPDATE market.settings
- SET currency = ?, fiat_base_multiplier = ?
- WHERE "user" = ?;
- """,
- (
- data.currency,
- data.fiat_base_multiplier,
- user,
- ),
- )
diff --git a/lnbits/extensions/market/migrations.py b/lnbits/extensions/market/migrations.py
deleted file mode 100644
index 81c3e14f..00000000
--- a/lnbits/extensions/market/migrations.py
+++ /dev/null
@@ -1,156 +0,0 @@
-async def m001_initial(db):
- """
- Initial Market settings table.
- """
- await db.execute(
- """
- CREATE TABLE market.settings (
- "user" TEXT PRIMARY KEY,
- currency TEXT DEFAULT 'sat',
- fiat_base_multiplier INTEGER DEFAULT 1
- );
- """
- )
-
- """
- Initial stalls table.
- """
- await db.execute(
- """
- CREATE TABLE market.stalls (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- name TEXT NOT NULL,
- currency TEXT,
- publickey TEXT,
- relays TEXT,
- shippingzones TEXT NOT NULL,
- rating INTEGER DEFAULT 0
- );
- """
- )
-
- """
- Initial products table.
- """
- await db.execute(
- f"""
- CREATE TABLE market.products (
- id TEXT PRIMARY KEY,
- stall TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE,
- product TEXT NOT NULL,
- categories TEXT,
- description TEXT,
- image TEXT,
- price INTEGER NOT NULL,
- quantity INTEGER NOT NULL,
- rating INTEGER DEFAULT 0
- );
- """
- )
-
- """
- Initial zones table.
- """
- await db.execute(
- """
- CREATE TABLE market.zones (
- id TEXT PRIMARY KEY,
- "user" TEXT NOT NULL,
- cost TEXT NOT NULL,
- countries TEXT NOT NULL
- );
- """
- )
-
- """
- Initial orders table.
- """
- await db.execute(
- f"""
- CREATE TABLE market.orders (
- id {db.serial_primary_key},
- wallet TEXT NOT NULL,
- username TEXT,
- pubkey TEXT,
- shippingzone TEXT NOT NULL,
- address TEXT NOT NULL,
- email TEXT NOT NULL,
- total INTEGER NOT NULL,
- invoiceid TEXT NOT NULL,
- paid BOOLEAN NOT NULL,
- shipped BOOLEAN NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
-
- """
- Initial order details table.
- """
- await db.execute(
- f"""
- CREATE TABLE market.order_details (
- id TEXT PRIMARY KEY,
- order_id INTEGER NOT NULL REFERENCES {db.references_schema}orders (id) ON DELETE CASCADE,
- product_id TEXT NOT NULL REFERENCES {db.references_schema}products (id) ON DELETE CASCADE,
- quantity INTEGER NOT NULL
- );
- """
- )
-
- """
- Initial market table.
- """
- await db.execute(
- """
- CREATE TABLE market.markets (
- id TEXT PRIMARY KEY,
- usr TEXT NOT NULL,
- name TEXT
- );
- """
- )
-
- """
- Initial market stalls table.
- """
- await db.execute(
- f"""
- CREATE TABLE market.market_stalls (
- id TEXT PRIMARY KEY,
- marketid TEXT NOT NULL REFERENCES {db.references_schema}markets (id) ON DELETE CASCADE,
- stallid TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE
- );
- """
- )
-
- """
- Initial chat messages table.
- """
- await db.execute(
- f"""
- CREATE TABLE market.messages (
- id {db.serial_primary_key},
- msg TEXT NOT NULL,
- pubkey TEXT NOT NULL,
- id_conversation TEXT NOT NULL,
- timestamp TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
-
- if db.type != "SQLITE":
- """
- Create indexes for message fetching
- """
- await db.execute(
- "CREATE INDEX idx_messages_timestamp ON market.messages (timestamp DESC)"
- )
- await db.execute(
- "CREATE INDEX idx_messages_conversations ON market.messages (id_conversation)"
- )
diff --git a/lnbits/extensions/market/models.json b/lnbits/extensions/market/models.json
deleted file mode 100644
index 05bb4b11..00000000
--- a/lnbits/extensions/market/models.json
+++ /dev/null
@@ -1,227 +0,0 @@
-{
- "shipping_zones": [
- "Free (digital)",
- "Worldwide",
- "Europe",
- "Australia",
- "Austria",
- "Belgium",
- "Brazil",
- "Canada",
- "Denmark",
- "Finland",
- "France",
- "Germany",
- "Greece",
- "Hong Kong",
- "Hungary",
- "Ireland",
- "Indonesia",
- "Israel",
- "Italy",
- "Japan",
- "Kazakhstan",
- "Korea",
- "Luxembourg",
- "Malaysia",
- "Mexico",
- "Netherlands",
- "New Zealand",
- "Norway",
- "Poland",
- "Portugal",
- "Russia",
- "Saudi Arabia",
- "Singapore",
- "Spain",
- "Sweden",
- "Switzerland",
- "Thailand",
- "Turkey",
- "Ukraine",
- "United Kingdom**",
- "United States***",
- "Vietnam",
- "China"
- ],
- "categories": [
- "Fashion (clothing and accessories)",
- "Health (and beauty)",
- "Toys (and baby equipment)",
- "Media (Books and CDs)",
- "Groceries (Food and Drink)",
- "Technology (Phones and Computers)",
- "Home (furniture and accessories)",
- "Gifts (flowers, cards, etc)",
- "Adult",
- "Other"
- ],
- "currency": {
- "BTC": "Bitcoin",
- "SAT": "Bitcoin satoshis",
- "AED": "United Arab Emirates Dirham",
- "AFN": "Afghan Afghani",
- "ALL": "Albanian Lek",
- "AMD": "Armenian Dram",
- "ANG": "Netherlands Antillean Gulden",
- "AOA": "Angolan Kwanza",
- "ARS": "Argentine Peso",
- "AUD": "Australian Dollar",
- "AWG": "Aruban Florin",
- "AZN": "Azerbaijani Manat",
- "BAM": "Bosnia and Herzegovina Convertible Mark",
- "BBD": "Barbadian Dollar",
- "BDT": "Bangladeshi Taka",
- "BGN": "Bulgarian Lev",
- "BHD": "Bahraini Dinar",
- "BIF": "Burundian Franc",
- "BMD": "Bermudian Dollar",
- "BND": "Brunei Dollar",
- "BOB": "Bolivian Boliviano",
- "BRL": "Brazilian Real",
- "BSD": "Bahamian Dollar",
- "BTN": "Bhutanese Ngultrum",
- "BWP": "Botswana Pula",
- "BYN": "Belarusian Ruble",
- "BYR": "Belarusian Ruble",
- "BZD": "Belize Dollar",
- "CAD": "Canadian Dollar",
- "CDF": "Congolese Franc",
- "CHF": "Swiss Franc",
- "CLF": "Unidad de Fomento",
- "CLP": "Chilean Peso",
- "CNH": "Chinese Renminbi Yuan Offshore",
- "CNY": "Chinese Renminbi Yuan",
- "COP": "Colombian Peso",
- "CRC": "Costa Rican Colón",
- "CUC": "Cuban Convertible Peso",
- "CVE": "Cape Verdean Escudo",
- "CZK": "Czech Koruna",
- "DJF": "Djiboutian Franc",
- "DKK": "Danish Krone",
- "DOP": "Dominican Peso",
- "DZD": "Algerian Dinar",
- "EGP": "Egyptian Pound",
- "ERN": "Eritrean Nakfa",
- "ETB": "Ethiopian Birr",
- "EUR": "Euro",
- "FJD": "Fijian Dollar",
- "FKP": "Falkland Pound",
- "GBP": "British Pound",
- "GEL": "Georgian Lari",
- "GGP": "Guernsey Pound",
- "GHS": "Ghanaian Cedi",
- "GIP": "Gibraltar Pound",
- "GMD": "Gambian Dalasi",
- "GNF": "Guinean Franc",
- "GTQ": "Guatemalan Quetzal",
- "GYD": "Guyanese Dollar",
- "HKD": "Hong Kong Dollar",
- "HNL": "Honduran Lempira",
- "HRK": "Croatian Kuna",
- "HTG": "Haitian Gourde",
- "HUF": "Hungarian Forint",
- "IDR": "Indonesian Rupiah",
- "ILS": "Israeli New Sheqel",
- "IMP": "Isle of Man Pound",
- "INR": "Indian Rupee",
- "IQD": "Iraqi Dinar",
- "ISK": "Icelandic Króna",
- "JEP": "Jersey Pound",
- "JMD": "Jamaican Dollar",
- "JOD": "Jordanian Dinar",
- "JPY": "Japanese Yen",
- "KES": "Kenyan Shilling",
- "KGS": "Kyrgyzstani Som",
- "KHR": "Cambodian Riel",
- "KMF": "Comorian Franc",
- "KRW": "South Korean Won",
- "KWD": "Kuwaiti Dinar",
- "KYD": "Cayman Islands Dollar",
- "KZT": "Kazakhstani Tenge",
- "LAK": "Lao Kip",
- "LBP": "Lebanese Pound",
- "LKR": "Sri Lankan Rupee",
- "LRD": "Liberian Dollar",
- "LSL": "Lesotho Loti",
- "LYD": "Libyan Dinar",
- "MAD": "Moroccan Dirham",
- "MDL": "Moldovan Leu",
- "MGA": "Malagasy Ariary",
- "MKD": "Macedonian Denar",
- "MMK": "Myanmar Kyat",
- "MNT": "Mongolian Tögrög",
- "MOP": "Macanese Pataca",
- "MRO": "Mauritanian Ouguiya",
- "MUR": "Mauritian Rupee",
- "MVR": "Maldivian Rufiyaa",
- "MWK": "Malawian Kwacha",
- "MXN": "Mexican Peso",
- "MYR": "Malaysian Ringgit",
- "MZN": "Mozambican Metical",
- "NAD": "Namibian Dollar",
- "NGN": "Nigerian Naira",
- "NIO": "Nicaraguan Córdoba",
- "NOK": "Norwegian Krone",
- "NPR": "Nepalese Rupee",
- "NZD": "New Zealand Dollar",
- "OMR": "Omani Rial",
- "PAB": "Panamanian Balboa",
- "PEN": "Peruvian Sol",
- "PGK": "Papua New Guinean Kina",
- "PHP": "Philippine Peso",
- "PKR": "Pakistani Rupee",
- "PLN": "Polish Złoty",
- "PYG": "Paraguayan Guaraní",
- "QAR": "Qatari Riyal",
- "RON": "Romanian Leu",
- "RSD": "Serbian Dinar",
- "RUB": "Russian Ruble",
- "RWF": "Rwandan Franc",
- "SAR": "Saudi Riyal",
- "SBD": "Solomon Islands Dollar",
- "SCR": "Seychellois Rupee",
- "SEK": "Swedish Krona",
- "SGD": "Singapore Dollar",
- "SHP": "Saint Helenian Pound",
- "SLL": "Sierra Leonean Leone",
- "SOS": "Somali Shilling",
- "SRD": "Surinamese Dollar",
- "SSP": "South Sudanese Pound",
- "STD": "São Tomé and Príncipe Dobra",
- "SVC": "Salvadoran Colón",
- "SZL": "Swazi Lilangeni",
- "THB": "Thai Baht",
- "TJS": "Tajikistani Somoni",
- "TMT": "Turkmenistani Manat",
- "TND": "Tunisian Dinar",
- "TOP": "Tongan Paʻanga",
- "TRY": "Turkish Lira",
- "TTD": "Trinidad and Tobago Dollar",
- "TWD": "New Taiwan Dollar",
- "TZS": "Tanzanian Shilling",
- "UAH": "Ukrainian Hryvnia",
- "UGX": "Ugandan Shilling",
- "USD": "US Dollar",
- "UYU": "Uruguayan Peso",
- "UZS": "Uzbekistan Som",
- "VEF": "Venezuelan Bolívar",
- "VES": "Venezuelan Bolívar Soberano",
- "VND": "Vietnamese Đồng",
- "VUV": "Vanuatu Vatu",
- "WST": "Samoan Tala",
- "XAF": "Central African Cfa Franc",
- "XAG": "Silver (Troy Ounce)",
- "XAU": "Gold (Troy Ounce)",
- "XCD": "East Caribbean Dollar",
- "XDR": "Special Drawing Rights",
- "XOF": "West African Cfa Franc",
- "XPD": "Palladium",
- "XPF": "Cfp Franc",
- "XPT": "Platinum",
- "YER": "Yemeni Rial",
- "ZAR": "South African Rand",
- "ZMW": "Zambian Kwacha",
- "ZWL": "Zimbabwean Dollar"
- }
-}
diff --git a/lnbits/extensions/market/models.py b/lnbits/extensions/market/models.py
deleted file mode 100644
index ea7f6f20..00000000
--- a/lnbits/extensions/market/models.py
+++ /dev/null
@@ -1,135 +0,0 @@
-from typing import List, Optional
-
-from fastapi.param_functions import Query
-from pydantic import BaseModel
-
-
-class MarketSettings(BaseModel):
- user: str
- currency: str
- fiat_base_multiplier: int
-
-
-class SetSettings(BaseModel):
- currency: str
- fiat_base_multiplier: int = Query(100, ge=1)
-
-
-class Stalls(BaseModel):
- id: str
- wallet: str
- name: str
- currency: str
- publickey: Optional[str]
- relays: Optional[str]
- shippingzones: str
-
-
-class createStalls(BaseModel):
- wallet: str = Query(...)
- name: str = Query(...)
- currency: str = Query("sat")
- publickey: str = Query(None)
- relays: str = Query(None)
- shippingzones: str = Query(...)
-
-
-class createProduct(BaseModel):
- stall: str = Query(...)
- product: str = Query(...)
- categories: str = Query(None)
- description: str = Query(None)
- image: str = Query(None)
- price: float = Query(0, ge=0)
- quantity: int = Query(0, ge=0)
-
-
-class Products(BaseModel):
- id: str
- stall: str
- product: str
- categories: Optional[str]
- description: Optional[str]
- image: Optional[str]
- price: float
- quantity: int
-
-
-class createZones(BaseModel):
- cost: float = Query(0, ge=0)
- countries: str = Query(...)
-
-
-class Zones(BaseModel):
- id: str
- user: str
- cost: float
- countries: str
-
-
-class OrderDetail(BaseModel):
- id: str
- order_id: str
- product_id: str
- quantity: int
-
-
-class createOrderDetails(BaseModel):
- product_id: str = Query(...)
- quantity: int = Query(..., ge=1)
-
-
-class createOrder(BaseModel):
- wallet: str = Query(...)
- username: str = Query(None)
- pubkey: str = Query(None)
- shippingzone: str = Query(...)
- address: str = Query(...)
- email: str = Query(...)
- total: int = Query(...)
- products: List[createOrderDetails]
-
-
-class Orders(BaseModel):
- id: str
- wallet: str
- username: Optional[str]
- pubkey: Optional[str]
- shippingzone: str
- address: str
- email: str
- total: int
- invoiceid: str
- paid: bool
- shipped: bool
- time: int
-
-
-class CreateMarket(BaseModel):
- usr: str = Query(...)
- name: str = Query(None)
- stalls: List[str] = Query(...)
-
-
-class Market(BaseModel):
- id: str
- usr: str
- name: Optional[str]
-
-
-class CreateMarketStalls(BaseModel):
- stallid: str
-
-
-class ChatMessage(BaseModel):
- id: str
- msg: str
- pubkey: str
- id_conversation: str
- timestamp: int
-
-
-class CreateChatMessage(BaseModel):
- msg: str = Query(..., min_length=1)
- pubkey: str = Query(...)
- room_name: str = Query(...)
diff --git a/lnbits/extensions/market/notifier.py b/lnbits/extensions/market/notifier.py
deleted file mode 100644
index 4fe366f3..00000000
--- a/lnbits/extensions/market/notifier.py
+++ /dev/null
@@ -1,91 +0,0 @@
-## adapted from https://github.com/Sentymental/chat-fastapi-websocket
-"""
-Create a class Notifier that will handle messages
-and delivery to the specific person
-"""
-
-import json
-from collections import defaultdict
-from typing import AsyncGenerator
-
-from fastapi import WebSocket
-from loguru import logger
-
-from .crud import create_chat_message
-from .models import CreateChatMessage
-
-
-class Notifier:
- """
- Manages chatrooms, sessions and members.
-
- Methods:
- - get_notification_generator(self): async generator with notification messages
- - get_members(self, room_name: str): get members in room
- - push(message: str, room_name: str): push message
- - connect(websocket: WebSocket, room_name: str): connect to room
- - remove(websocket: WebSocket, room_name: str): remove
- - _notify(message: str, room_name: str): notifier
- """
-
- def __init__(self):
- # Create sessions as a dict:
- self.sessions: dict = defaultdict(dict)
-
- # Create notification generator:
- self.generator = self.get_notification_generator()
-
- async def get_notification_generator(self) -> AsyncGenerator:
- """Notification Generator"""
-
- while True:
- message = yield
- msg = message["message"]
- room_name = message["room_name"]
- await self._notify(msg, room_name)
-
- def get_members(self, room_name: str):
- """Get all members in a room"""
-
- try:
- logger.info(f"Looking for members in room: {room_name}")
- return self.sessions[room_name]
-
- except Exception:
- logger.exception(f"There is no member in room: {room_name}")
- return None
-
- async def push(self, message: str, room_name: str):
- """Push a message"""
- message_body = {"message": message, "room_name": room_name}
- await self.generator.asend(message_body)
-
- async def connect(self, websocket: WebSocket, room_name: str):
- """Connect to room"""
-
- await websocket.accept()
- if self.sessions[room_name] == {} or len(self.sessions[room_name]) == 0:
- self.sessions[room_name] = []
-
- self.sessions[room_name].append(websocket)
- print(f"Connections ...: {self.sessions[room_name]}")
-
- def remove(self, websocket: WebSocket, room_name: str):
- """Remove websocket from room"""
-
- self.sessions[room_name].remove(websocket)
- print(f"Connection removed...\nOpen connections...: {self.sessions[room_name]}")
-
- async def _notify(self, message: str, room_name: str):
- """Notifier"""
- d = json.loads(message)
- d["room_name"] = room_name
- db_msg = CreateChatMessage.parse_obj(d)
- await create_chat_message(data=db_msg)
-
- remaining_sessions = []
- while len(self.sessions[room_name]) > 0:
- websocket = self.sessions[room_name].pop()
- await websocket.send_text(message)
- remaining_sessions.append(websocket)
- self.sessions[room_name] = remaining_sessions
diff --git a/lnbits/extensions/market/static/images/bitcoin-shop.png b/lnbits/extensions/market/static/images/bitcoin-shop.png
deleted file mode 100644
index debffbb2..00000000
Binary files a/lnbits/extensions/market/static/images/bitcoin-shop.png and /dev/null differ
diff --git a/lnbits/extensions/market/static/images/placeholder.png b/lnbits/extensions/market/static/images/placeholder.png
deleted file mode 100644
index c7d3a947..00000000
Binary files a/lnbits/extensions/market/static/images/placeholder.png and /dev/null differ
diff --git a/lnbits/extensions/market/tasks.py b/lnbits/extensions/market/tasks.py
deleted file mode 100644
index b102e0f1..00000000
--- a/lnbits/extensions/market/tasks.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import asyncio
-
-from loguru import logger
-
-from lnbits.core.models import Payment
-from lnbits.tasks import register_invoice_listener
-
-from .crud import (
- get_market_order_details,
- get_market_order_invoiceid,
- set_market_order_paid,
- update_market_product_stock,
-)
-
-
-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") != "market":
- return
-
- order = await get_market_order_invoiceid(payment.payment_hash)
- if not order:
- logger.error("this should never happen", payment)
- return
-
- # set order as paid
- await set_market_order_paid(payment.payment_hash)
-
- # deduct items sold from stock
- details = await get_market_order_details(order.id)
- await update_market_product_stock(details)
diff --git a/lnbits/extensions/market/templates/market/_api_docs.html b/lnbits/extensions/market/templates/market/_api_docs.html
deleted file mode 100644
index f0d97dbf..00000000
--- a/lnbits/extensions/market/templates/market/_api_docs.html
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
-
-
- LNbits Market (Nostr support coming soon)
-
-
-
- Create Shipping Zones you're willing to ship to
- Create a Stall to list yiur products on
- Create products to put on the Stall
- Take orders
- Includes chat support!
-
- The first LNbits market idea 'Diagon Alley' helped create Nostr, and soon
- this market extension will have the option to work on Nostr 'Diagon Alley'
- mode, by the merchant, market, and buyer all having keys, and data being
- routed through Nostr relays.
-
-
- Created by,
- Tal Vasconcelos ,
- Ben Arc
-
-
-
-
-
-
-
-
-
- GET
- /market/api/v1/stall/products/<relay_id>
- Body (application/json)
-
- Returns 201 CREATED (application/json)
-
- Product JSON list
- Curl example
- curl -X GET {{ request.url_root
- }}api/v1/stall/products/<relay_id>
-
-
-
-
-
-
- POST
- /market/api/v1/stall/order/<relay_id>
- Body (application/json)
- {"id": <string>, "address": <string>, "shippingzone":
- <integer>, "email": <string>, "quantity":
- <integer>}
-
- Returns 201 CREATED (application/json)
-
- {"checking_id": <string>,"payment_request":
- <string>}
- Curl example
- curl -X POST {{ request.url_root
- }}api/v1/stall/order/<relay_id> -d '{"id": <product_id&>,
- "email": <customer_email>, "address": <customer_address>,
- "quantity": 2, "shippingzone": 1}' -H "Content-type: application/json"
-
-
-
-
-
-
-
- GET
- /market/api/v1/stall/checkshipped/<checking_id>
- Headers
-
- Returns 200 OK (application/json)
-
- {"shipped": <boolean>}
- Curl example
- curl -X GET {{ request.url_root
- }}api/v1/stall/checkshipped/<checking_id> -H "Content-type:
- application/json"
-
-
-
-
diff --git a/lnbits/extensions/market/templates/market/_chat_box.html b/lnbits/extensions/market/templates/market/_chat_box.html
deleted file mode 100644
index 05b0c58f..00000000
--- a/lnbits/extensions/market/templates/market/_chat_box.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
- Messages
-
-
-
-
-
-
-
-
-
-
-
diff --git a/lnbits/extensions/market/templates/market/_dialogs.html b/lnbits/extensions/market/templates/market/_dialogs.html
deleted file mode 100644
index a0ab84b3..00000000
--- a/lnbits/extensions/market/templates/market/_dialogs.html
+++ /dev/null
@@ -1,405 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update Product
-
- Create Product
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
- Update Shipping Zone
- Create Shipping Zone
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
- Update Marketplace
- Launch Marketplace
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Generate keys
-
-
- Restore keys
-
-
-
-
-
-
-
-
- Update Stall
- Create Stall
- Cancel
-
-
-
-
-
-
-
- How to use Market
-
-
- Create Shipping Zones you're willing to ship to. You can define
- different values for different zones.
-
-
-
-
-
- Create a Stall and provide private and public keys to use for
- communication. If you don't have one, LNbits will create a key pair for
- you. It will be saved and can be used on other stalls.
-
-
-
-
-
-
- Create your products, add a small description and an image. Choose to
- what stall, if you have more than one, it belongs to
-
-
-
-
-
-
-
-
-
-
diff --git a/lnbits/extensions/market/templates/market/_tables.html b/lnbits/extensions/market/templates/market/_tables.html
deleted file mode 100644
index 280bb9f1..00000000
--- a/lnbits/extensions/market/templates/market/_tables.html
+++ /dev/null
@@ -1,443 +0,0 @@
-
-
-
-
-
-
Orders
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
- Product shipped?
-
-
-
-
-
-
-
-
-
-
-
- Order Details
-
-
-
- Products
- {{ products.length && (_.findWhere(products, {id:
- col.product_id})).product }}
- Quantity: {{ col.quantity }}
-
-
-
-
-
- Shipping to
- {{ props.row.address }}
-
-
-
-
-
- User info
- {{ props.row.username }}
- {{ props.row.email }}
- {{ props.row.pubkey }}
-
-
-
-
- Total
- {{ props.row.total }}
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
- Products
-
-
- Add a product
-
-
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
- Disabled: link to pass to stall relays when using
- nostr
-
-
- {{ col.value }}
-
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
Market Stalls
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
- Stall simple UI marketping cart
-
-
- {{ col.value }}
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
Marketplaces
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
- Link to pass to stall relay
-
-
- {{ col.name == 'stalls' ? stallName(col.value) : col.value }}
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
Shipping Zones
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
diff --git a/lnbits/extensions/market/templates/market/index.html b/lnbits/extensions/market/templates/market/index.html
deleted file mode 100644
index 0f8fa78b..00000000
--- a/lnbits/extensions/market/templates/market/index.html
+++ /dev/null
@@ -1,1422 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
- {% include "market/_dialogs.html" %}
-
-
-
- + Shipping Zone Create a shipping zone
- + Stall
-
- Create a market stall to list products on
-
- + Stall
-
- Create a market stall to list products on
-
- + Product List a product
- + Product List a product
- setCurrency(value)"
- >
- Create Market
-
- Makes a simple frontend market for your stalls (not
- NOSTR)
-
-
-
- Market
- Make a market of multiple stalls.
-
-
-
-
- Coming soon...
- Export all Data
-
- Export all data (markets, products, orders, etc...)
-
-
-
- {% include "market/_tables.html" %}
-
-
-
-
-
-
Keys
-
-
- Export to CSV
-
-
-
-
-
-
-
- {% raw %}
-
-
- {{ keys[type] }}
-
-
- {{ type == 'pubkey' ? 'Public Key' : 'Private Key' }}Click to copy
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
-
- LNbits Market Extension (Nostr support coming soon)
-
-
-
-
- {% include "market/_api_docs.html" %}
-
-
-
- {% include "market/_chat_box.html" %}
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/market.html b/lnbits/extensions/market/templates/market/market.html
deleted file mode 100644
index e59bb245..00000000
--- a/lnbits/extensions/market/templates/market/market.html
+++ /dev/null
@@ -1,175 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
- Market: {{ market.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% raw %}
-
-
-
-
-
- {{ item.product }}
-
-
-
-
-
-
-
-
-
- {{ item.stallName }}
-
-
- {{ item.price }} sats BTC {{ (item.price / 1e8).toFixed(8) }}
-
-
- {{ getAmountFormated(item.price, item.currency) }}
- ({{ getValueInSats(item.price, item.currency) }} sats)
-
-
{{item.quantity}} left
-
-
- {{cat}}
-
-
-
{{ item.description }}
-
-
-
-
-
-
- Stall: {{ item.stallName }}
-
- Visit Stall
-
-
- {% endraw %}
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/order.html b/lnbits/extensions/market/templates/market/order.html
deleted file mode 100644
index 5be606f9..00000000
--- a/lnbits/extensions/market/templates/market/order.html
+++ /dev/null
@@ -1,564 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
- {% raw %}
- {{ stall.name }}
-
- Public Key: {{ sliceKey(stall.publickey) }}
- Click to copy
-
- {% endraw %}
-
-
-
-
- { changeOrder() }"
- emit-value
- >
-
-
-
-
- {% raw %}
-
-
- {{p.quantity}} x
-
-
-
-
-
-
-
- {{ p.name }}
-
-
-
- {{ getAmountFormated(p.price) }}
- {{p.price}} sats
-
-
- {% endraw %}
-
-
-
-
-
-
- Bellow are the keys needed to contact the merchant. They are
- stored in the browser!
-
-
-
-
- {% raw %}
-
-
- {{ user.keys[type] }}
-
-
- {{ type == 'publickey' ? 'Public Key' : 'Private Key' }}
-
- {% endraw %}
-
-
-
-
-
- Backup keys
- Download your keys
-
- Restore keys
- Restore keys
-
- Delete data
- Delete all data from browser
-
-
-
-
-
- Export, or send, this page to another device
-
-
-
- Click to copy
-
-
-
- Copy URL
- Export, or send, this page to another device
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Bookmark this page
-
- Don't forget to bookmark this page to be able to check on your order!
-
-
- You can backup your keys, and export the page to another device also.
-
-
- Close
-
-
-
-
-{% endblock %} {% block scripts %}
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/product.html b/lnbits/extensions/market/templates/market/product.html
deleted file mode 100644
index 66f56691..00000000
--- a/lnbits/extensions/market/templates/market/product.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "public.html" %} {% block page %}
-Product page
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/stall.html b/lnbits/extensions/market/templates/market/stall.html
deleted file mode 100644
index f9189b30..00000000
--- a/lnbits/extensions/market/templates/market/stall.html
+++ /dev/null
@@ -1,531 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
- Stall: {{ stall.name }}
-
-
-
-
-
-
-
-
-
- {% raw %}
-
- {{ cart.size }}
-
- {% endraw %}
-
-
- {% raw %}
-
-
- {{p.quantity}} x
-
-
-
-
-
-
-
-
- {{ p.name }}
-
-
-
-
- {{unit != 'sat' ? getAmountFormated(p.price) : p.price +
- 'sats'}}
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% raw %}
-
-
-
- Add to cart
-
-
-
- {{ item.product }}
-
-
-
-
-
-
-
-
-
- {{ item.price }} sats BTC {{ (item.price / 1e8).toFixed(8) }}
-
-
- {{ getAmountFormated(item.price) }}
- ({{ getValueInSats(item.price) }} sats)
-
- {{item.quantity}} left
-
-
- {{cat}}
-
-
-
{{ item.description }}
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
-
- Click to restore saved public key
-
-
-
-
-
- Select the shipping zone:
-
-
-
-
- {% raw %} Total: {{ unit != 'sat' ? getAmountFormated(finalCost) :
- finalCost + 'sats' }}
- ({{ getValueInSats(finalCost) }} sats)
- {% endraw %}
-
-
- Checkout
- Cancel
-
-
-
-
-
-
-
-
-
-
-
- Copy invoice
- Close
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/market/views.py b/lnbits/extensions/market/views.py
deleted file mode 100644
index f9e7131b..00000000
--- a/lnbits/extensions/market/views.py
+++ /dev/null
@@ -1,155 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Query, Request, WebSocket, WebSocketDisconnect
-from fastapi.templating import Jinja2Templates
-from loguru import logger
-from starlette.exceptions import HTTPException
-from starlette.responses import HTMLResponse
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import market_ext, market_renderer
-from .crud import (
- create_market_settings,
- get_market_market,
- get_market_market_stalls,
- get_market_order_details,
- get_market_order_invoiceid,
- get_market_products,
- get_market_settings,
- get_market_stall,
- get_market_zone,
-)
-from .models import SetSettings
-from .notifier import Notifier
-
-templates = Jinja2Templates(directory="templates")
-
-
-@market_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- settings = await get_market_settings(user=user.id)
-
- if not settings:
- await create_market_settings(
- user=user.id, data=SetSettings(currency="sat", fiat_base_multiplier=1)
- )
- settings = await get_market_settings(user.id)
- assert settings
- return market_renderer().TemplateResponse(
- "market/index.html",
- {"request": request, "user": user.dict(), "currency": settings.currency},
- )
-
-
-@market_ext.get("/stalls/{stall_id}", response_class=HTMLResponse)
-async def stall(request: Request, stall_id):
- stall = await get_market_stall(stall_id)
- products = await get_market_products(stall_id)
-
- if not stall:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Stall does not exist."
- )
-
- zones = []
- for id in stall.shippingzones.split(","):
- zone = await get_market_zone(id)
- assert zone
- z = zone.dict()
- zones.append({"label": z["countries"], "cost": z["cost"], "value": z["id"]})
-
- _stall = stall.dict()
-
- _stall["zones"] = zones
-
- return market_renderer().TemplateResponse(
- "market/stall.html",
- {
- "request": request,
- "stall": _stall,
- "products": [product.dict() for product in products],
- },
- )
-
-
-@market_ext.get("/market/{market_id}", response_class=HTMLResponse)
-async def market(request: Request, market_id):
- market = await get_market_market(market_id)
-
- if not market:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Marketplace doesn't exist."
- )
-
- stalls = await get_market_market_stalls(market_id)
- stalls_ids = [stall.id for stall in stalls]
- products = [product.dict() for product in await get_market_products(stalls_ids)]
-
- return market_renderer().TemplateResponse(
- "market/market.html",
- {
- "request": request,
- "market": market,
- "stalls": [stall.dict() for stall in stalls],
- "products": products,
- },
- )
-
-
-@market_ext.get("/order", response_class=HTMLResponse)
-async def order_chat(
- request: Request,
- merch: str = Query(...),
- invoice_id: str = Query(...),
- keys: str = Query(None),
-):
- stall = await get_market_stall(merch)
- assert stall
- order = await get_market_order_invoiceid(invoice_id)
- assert order
- _order = await get_market_order_details(order.id)
- products = await get_market_products(stall.id)
- assert products
-
- return market_renderer().TemplateResponse(
- "market/order.html",
- {
- "request": request,
- "stall": {
- "id": stall.id,
- "name": stall.name,
- "publickey": stall.publickey,
- "wallet": stall.wallet,
- "currency": stall.currency,
- },
- "publickey": keys.split(",")[0] if keys else None,
- "privatekey": keys.split(",")[1] if keys else None,
- "order_id": order.invoiceid,
- "order": [details.dict() for details in _order],
- "products": [product.dict() for product in products],
- },
- )
-
-
-##################WEBSOCKET ROUTES########################
-
-# Initialize Notifier:
-notifier = Notifier()
-
-
-@market_ext.websocket("/ws/{room_name}")
-async def websocket_endpoint(websocket: WebSocket, room_name: str):
- await notifier.connect(websocket, room_name)
- try:
- while True:
- data = await websocket.receive_text()
- room_members = notifier.get_members(room_name) or []
- if websocket not in room_members:
- logger.warning("Sender not in room member: Reconnecting...")
- await notifier.connect(websocket, room_name)
- await notifier._notify(data, room_name)
-
- except WebSocketDisconnect:
- notifier.remove(websocket, room_name)
diff --git a/lnbits/extensions/market/views_api.py b/lnbits/extensions/market/views_api.py
deleted file mode 100644
index 221d51bb..00000000
--- a/lnbits/extensions/market/views_api.py
+++ /dev/null
@@ -1,527 +0,0 @@
-from http import HTTPStatus
-from typing import Optional
-
-from fastapi import Depends, Query
-from loguru import logger
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_user
-from lnbits.core.services import create_invoice
-from lnbits.core.views.api import api_payment
-from lnbits.decorators import (
- WalletTypeInfo,
- get_key_type,
- require_admin_key,
- require_invoice_key,
-)
-from lnbits.helpers import urlsafe_short_hash
-from lnbits.utils.exchange_rates import currencies
-
-from . import db, market_ext
-from .crud import (
- create_market_market,
- create_market_market_stalls,
- create_market_order,
- create_market_order_details,
- create_market_product,
- create_market_settings,
- create_market_stall,
- create_market_zone,
- delete_market_order,
- delete_market_product,
- delete_market_stall,
- delete_market_zone,
- get_market_chat_by_merchant,
- get_market_chat_messages,
- get_market_latest_chat_messages,
- get_market_market,
- get_market_market_stalls,
- get_market_markets,
- get_market_order,
- get_market_order_details,
- get_market_order_invoiceid,
- get_market_orders,
- get_market_product,
- get_market_products,
- get_market_settings,
- get_market_stall,
- get_market_stalls,
- get_market_zone,
- get_market_zones,
- set_market_order_pubkey,
- set_market_settings,
- update_market_market,
- update_market_product,
- update_market_stall,
- update_market_zone,
-)
-from .models import (
- CreateMarket,
- SetSettings,
- createOrder,
- createProduct,
- createStalls,
- createZones,
-)
-
-# from lnbits.db import open_ext_db
-
-
-### Products
-@market_ext.get("/api/v1/products")
-async def api_market_products(
- wallet: WalletTypeInfo = Depends(require_invoice_key),
- all_stalls: bool = Query(False),
-):
- wallet_ids = [wallet.wallet.id]
-
- if all_stalls:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- stalls = [stall.id for stall in await get_market_stalls(wallet_ids)]
-
- if not stalls:
- return
-
- return [product.dict() for product in await get_market_products(stalls)]
-
-
-@market_ext.post("/api/v1/products")
-@market_ext.put("/api/v1/products/{product_id}")
-async def api_market_product_create(
- data: createProduct,
- product_id=None,
- wallet: WalletTypeInfo = Depends(require_invoice_key),
-):
- # For fiat currencies,
- # we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
- settings = await get_market_settings(user=wallet.wallet.user)
- assert settings
-
- stall = await get_market_stall(stall_id=data.stall)
- assert stall
-
- if stall.currency != "sat":
- data.price *= settings.fiat_base_multiplier
-
- if data.image:
- image_is_url = data.image.startswith("https://") or data.image.startswith(
- "http://"
- )
-
- if not image_is_url:
-
- def size(b64string):
- return int((len(b64string) * 3) / 4 - b64string.count("=", -2))
-
- image_size = size(data.image) / 1024
- if image_size > 100:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=f"Image size is too big, {int(image_size)}Kb. Max: 100kb, Compress the image at https://tinypng.com, or use an URL.",
- )
-
- if product_id:
- product = await get_market_product(product_id)
- if not product:
- return {"message": "Product does not exist."}
-
- # stall = await get_market_stall(stall_id=product.stall)
- if stall.wallet != wallet.wallet.id:
- return {"message": "Not your product."}
-
- product = await update_market_product(product_id, **data.dict())
- else:
- product = await create_market_product(data=data)
- assert product
- return product.dict()
-
-
-@market_ext.delete("/api/v1/products/{product_id}")
-async def api_market_products_delete(
- product_id, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- product = await get_market_product(product_id)
-
- if not product:
- return {"message": "Product does not exist."}
-
- stall = await get_market_stall(product.stall)
- assert stall
-
- if stall.wallet != wallet.wallet.id:
- return {"message": "Not your Market."}
-
- await delete_market_product(product_id)
- raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
-
-
-# # # Shippingzones
-
-
-@market_ext.get("/api/v1/zones")
-async def api_market_zones(wallet: WalletTypeInfo = Depends(get_key_type)):
-
- return await get_market_zones(wallet.wallet.user)
-
-
-@market_ext.post("/api/v1/zones")
-async def api_market_zone_create(
- data: createZones, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- zone = await create_market_zone(user=wallet.wallet.user, data=data)
- return zone.dict()
-
-
-@market_ext.post("/api/v1/zones/{zone_id}")
-async def api_market_zone_update(
- data: createZones,
- zone_id: str,
- wallet: WalletTypeInfo = Depends(require_admin_key),
-):
- zone = await get_market_zone(zone_id)
- if not zone:
- return {"message": "Zone does not exist."}
- if zone.user != wallet.wallet.user:
- return {"message": "Not your record."}
- zone = await update_market_zone(zone_id, **data.dict())
- return zone
-
-
-@market_ext.delete("/api/v1/zones/{zone_id}")
-async def api_market_zone_delete(
- zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- zone = await get_market_zone(zone_id)
-
- if not zone:
- return {"message": "zone does not exist."}
-
- if zone.user != wallet.wallet.user:
- return {"message": "Not your zone."}
-
- await delete_market_zone(zone_id)
- raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
-
-
-# # # Stalls
-
-
-@market_ext.get("/api/v1/stalls")
-async def api_market_stalls(
- wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
-):
- wallet_ids = [wallet.wallet.id]
-
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- return [stall.dict() for stall in await get_market_stalls(wallet_ids)]
-
-
-@market_ext.post("/api/v1/stalls")
-@market_ext.put("/api/v1/stalls/{stall_id}")
-async def api_market_stall_create(
- data: createStalls,
- stall_id: Optional[str] = None,
- wallet: WalletTypeInfo = Depends(require_invoice_key),
-):
-
- if stall_id:
- stall = await get_market_stall(stall_id)
- if not stall:
- return {"message": "Withdraw stall does not exist."}
-
- if stall.wallet != wallet.wallet.id:
- return {"message": "Not your withdraw stall."}
-
- stall = await update_market_stall(stall_id, **data.dict())
- else:
- stall = await create_market_stall(data=data)
- assert stall
- return stall.dict()
-
-
-@market_ext.delete("/api/v1/stalls/{stall_id}")
-async def api_market_stall_delete(
- stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- stall = await get_market_stall(stall_id)
-
- if not stall:
- return {"message": "Stall does not exist."}
-
- if stall.wallet != wallet.wallet.id:
- return {"message": "Not your Stall."}
-
- await delete_market_stall(stall_id)
- raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
-
-
-###Orders
-
-
-@market_ext.get("/api/v1/orders")
-async def api_market_orders(
- wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
-):
- wallet_ids = [wallet.wallet.id]
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- orders = await get_market_orders(wallet_ids)
- if not orders:
- return
- orders_with_details = []
- for order in orders:
- _order = order.dict()
- _order["details"] = await get_market_order_details(_order["id"])
- orders_with_details.append(_order)
- try:
- return orders_with_details # [order for order in orders]
- # return [order.dict() for order in await get_market_orders(wallet_ids)]
- except:
- return {"message": "We could not retrieve the orders."}
-
-
-@market_ext.get("/api/v1/orders/{order_id}")
-async def api_market_order_by_id(order_id: str):
- order = await get_market_order(order_id)
- assert order
- _order = order.dict()
- _order["details"] = await get_market_order_details(order_id)
-
- return _order
-
-
-@market_ext.post("/api/v1/orders")
-async def api_market_order_create(data: createOrder):
- ref = urlsafe_short_hash()
-
- payment_hash, payment_request = await create_invoice(
- wallet_id=data.wallet,
- amount=data.total,
- memo="New order on Market",
- extra={
- "tag": "market",
- "reference": ref,
- },
- )
- order_id = await create_market_order(invoiceid=payment_hash, data=data)
- logger.debug(f"ORDER ID {order_id}")
- logger.debug(f"PRODUCTS {data.products}")
- await create_market_order_details(order_id=order_id, data=data.products)
- return {
- "payment_hash": payment_hash,
- "payment_request": payment_request,
- "order_reference": ref,
- }
-
-
-@market_ext.get("/api/v1/orders/payments/{payment_hash}")
-async def api_market_check_payment(payment_hash: str):
- order = await get_market_order_invoiceid(payment_hash)
- if not order:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Order does not exist."
- )
- try:
- status = await api_payment(payment_hash)
-
- except Exception as exc:
- logger.error(exc)
- return {"paid": False}
- return status
-
-
-@market_ext.delete("/api/v1/orders/{order_id}")
-async def api_market_order_delete(
- order_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- order = await get_market_order(order_id)
-
- if not order:
- return {"message": "Order does not exist."}
-
- if order.wallet != wallet.wallet.id:
- return {"message": "Not your Order."}
-
- await delete_market_order(order_id)
-
- raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
-
-
-# @market_ext.get("/api/v1/orders/paid/{order_id}")
-# async def api_market_order_paid(
-# order_id, wallet: WalletTypeInfo = Depends(require_admin_key)
-# ):
-# await db.execute(
-# "UPDATE market.orders SET paid = ? WHERE id = ?",
-# (
-# True,
-# order_id,
-# ),
-# )
-# return "", HTTPStatus.OK
-
-
-@market_ext.get("/api/v1/order/pubkey/{payment_hash}/{pubkey}")
-async def api_market_order_pubkey(payment_hash: str, pubkey: str):
- await set_market_order_pubkey(payment_hash, pubkey)
- return "", HTTPStatus.OK
-
-
-@market_ext.get("/api/v1/orders/shipped/{order_id}")
-async def api_market_order_shipped(
- order_id, shipped: bool = Query(...), wallet: WalletTypeInfo = Depends(get_key_type)
-):
- await db.execute(
- "UPDATE market.orders SET shipped = ? WHERE id = ?",
- (
- shipped,
- order_id,
- ),
- )
- order = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,))
-
- return order
-
-
-###List products based on stall id
-
-
-# @market_ext.get("/api/v1/stall/products/{stall_id}")
-# async def api_market_stall_products(
-# stall_id, wallet: WalletTypeInfo = Depends(get_key_type)
-# ):
-
-# rows = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
-# if not rows:
-# return {"message": "Stall does not exist."}
-
-# products = db.fetchone("SELECT * FROM market.products WHERE wallet = ?", (rows[1],))
-# if not products:
-# return {"message": "No products"}
-
-# return [products.dict() for products in await get_market_products(rows[1])]
-
-
-###Check a product has been shipped
-
-
-# @market_ext.get("/api/v1/stall/checkshipped/{checking_id}")
-# async def api_market_stall_checkshipped(
-# checking_id, wallet: WalletTypeInfo = Depends(get_key_type)
-# ):
-# rows = await db.fetchone(
-# "SELECT * FROM market.orders WHERE invoiceid = ?", (checking_id,)
-# )
-# return {"shipped": rows["shipped"]}
-
-
-##
-# MARKETS
-##
-
-
-@market_ext.get("/api/v1/markets")
-async def api_market_markets(wallet: WalletTypeInfo = Depends(get_key_type)):
- # await get_market_market_stalls(market_id="FzpWnMyHQMcRppiGVua4eY")
- try:
- return [
- market.dict() for market in await get_market_markets(wallet.wallet.user)
- ]
- except:
- return {"message": "We could not retrieve the markets."}
-
-
-@market_ext.get("/api/v1/markets/{market_id}/stalls")
-async def api_market_market_stalls(market_id: str):
- stall_ids = await get_market_market_stalls(market_id)
- return stall_ids
-
-
-@market_ext.post("/api/v1/markets")
-@market_ext.put("/api/v1/markets/{market_id}")
-async def api_market_market_create(
- data: CreateMarket,
- market_id: Optional[str] = None,
- wallet: WalletTypeInfo = Depends(require_invoice_key),
-):
- if market_id:
- market = await get_market_market(market_id)
- if not market:
- return {"message": "Market does not exist."}
-
- if market.usr != wallet.wallet.user:
- return {"message": "Not your market."}
-
- market = await update_market_market(market_id, data.name)
- else:
- market = await create_market_market(data=data)
-
- assert market
- await create_market_market_stalls(market_id=market.id, data=data.stalls)
-
- return market.dict()
-
-
-## MESSAGES/CHAT
-
-
-@market_ext.get("/api/v1/chat/messages/merchant")
-async def api_get_merchant_messages(
- orders: str = Query(...), wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- return [msg.dict() for msg in await get_market_chat_by_merchant(orders.split(","))]
-
-
-@market_ext.get("/api/v1/chat/messages/{room_name}")
-async def api_get_latest_chat_msg(room_name: str, all_messages: bool = Query(False)):
- if all_messages:
- messages = await get_market_chat_messages(room_name)
- else:
- messages = await get_market_latest_chat_messages(room_name)
-
- return messages
-
-
-@market_ext.get("/api/v1/currencies")
-async def api_list_currencies_available():
- return list(currencies.keys())
-
-
-@market_ext.get("/api/v1/settings")
-async def api_get_settings(wallet: WalletTypeInfo = Depends(require_admin_key)):
- user = wallet.wallet.user
-
- settings = await get_market_settings(user)
-
- return settings
-
-
-@market_ext.post("/api/v1/settings")
-@market_ext.put("/api/v1/settings/{usr}")
-async def api_set_settings(
- data: SetSettings,
- usr: Optional[str] = None,
- wallet: WalletTypeInfo = Depends(require_admin_key),
-):
- if usr:
- if usr != wallet.wallet.user:
- return {"message": "Not your Market."}
-
- settings = await get_market_settings(user=usr)
- assert settings
-
- if settings.user != wallet.wallet.user:
- return {"message": "Not your Market."}
-
- return await set_market_settings(usr, data)
-
- user = wallet.wallet.user
-
- return await create_market_settings(user, data)
diff --git a/lnbits/extensions/ngrok/README.md b/lnbits/extensions/ngrok/README.md
deleted file mode 100644
index 626c788f..00000000
--- a/lnbits/extensions/ngrok/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-Ngrok
-Serve lnbits over https for free using ngrok
-
-
-
-How it works
-
-When enabled, ngrok creates a tunnel to ngrok.io with https support and tells you the https web address where you can access your lnbits instance. If you are not the first user to enable it, it doesn't create a new one, it just tells you the existing one. Useful for creating/managing/using lnurls, which must be served either via https or via tor. Note that if you restart your device, your device will generate a new url. If anyone is using your old one for wallets, lnurls, etc., whatever they are doing will stop working.
-
-Installation
-
-Check the Extensions page on your instance of lnbits. If you have copy of lnbits with ngrok as one of the built in extensions, click Enable -- that's the only thing you need to do to install it.
-
-If your copy of lnbits does not have ngrok as one of the built in extensions, stop lnbits, create go into your lnbits folder, and run this command: ./venv/bin/pip install pyngrok. Then go into the lnbits subdirectory and the extensions subdirectory within that. (So lnbits > lnbits > extensions.) Create a new subdirectory in there called freetunnel, download this repository as a zip file, and unzip it in the freetunnel directory. If your unzipper creates a new "freetunnel" subdirectory, take everything out of there and put it in the freetunnel directory you created. Then go back to the top level lnbits directory and run these commands:
-
-```
-./venv/bin/quart assets
-./venv/bin/quart migrate
-./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
-```
-
-Optional: set up an ngrok.com account
-
-The default setup makes a tunnel on a random subdomain, and the session times out after 24h or a certain bandwith limit. You can set up an account at ngrok.com; a free plan removes the timeout, and a paid plan lets you choose a custom subdomain (or even use your own domain). For this, get an auth token from ngrok.com, and then set it up as `NGROK_AUTHTOKEN` environment variable on your `.env` file e.g., if your auth token is xxxx, add a line NGROK_AUTHTOKEN=xxxx.
diff --git a/lnbits/extensions/ngrok/__init__.py b/lnbits/extensions/ngrok/__init__.py
deleted file mode 100644
index 16ac4650..00000000
--- a/lnbits/extensions/ngrok/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from fastapi import APIRouter
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_ngrok")
-
-ngrok_ext: APIRouter = APIRouter(prefix="/ngrok", tags=["ngrok"])
-
-
-def ngrok_renderer():
- return template_renderer(["lnbits/extensions/ngrok/templates"])
-
-
-from .views import * # noqa: F401,F403
diff --git a/lnbits/extensions/ngrok/config.json.example b/lnbits/extensions/ngrok/config.json.example
deleted file mode 100644
index 58e9ff8e..00000000
--- a/lnbits/extensions/ngrok/config.json.example
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Ngrok",
- "short_description": "Serve lnbits over https for free using ngrok",
- "icon": "trip_origin",
- "contributors": ["supertestnet"]
-}
diff --git a/lnbits/extensions/ngrok/migrations.py b/lnbits/extensions/ngrok/migrations.py
deleted file mode 100644
index f9b8b37d..00000000
--- a/lnbits/extensions/ngrok/migrations.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# async def m001_initial(db):
-
-# await db.execute(
-# """
-# CREATE TABLE example.example (
-# id TEXT PRIMARY KEY,
-# wallet TEXT NOT NULL,
-# time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """
-# );
-# """
-# )
diff --git a/lnbits/extensions/ngrok/templates/ngrok/index.html b/lnbits/extensions/ngrok/templates/ngrok/index.html
deleted file mode 100644
index b8bb53bb..00000000
--- a/lnbits/extensions/ngrok/templates/ngrok/index.html
+++ /dev/null
@@ -1,60 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
-
-
- Access this lnbits instance at the following url
-
-
-
- {{ ngrok }}
-
-
-
-
-
-
-
-
- Ngrok extension
-
-
-
-
-
- Note that if you restart your device, your device will generate a
- new url. If anyone is using your old one for wallets, lnurls,
- etc., whatever they are doing will stop working.
-
- Created by
- Supertestnet .
-
-
-
-
-
-
-
-{% endblock %}{% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/ngrok/views.py b/lnbits/extensions/ngrok/views.py
deleted file mode 100644
index 2fa4df9c..00000000
--- a/lnbits/extensions/ngrok/views.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from os import getenv
-
-from fastapi import Depends, Request
-from fastapi.templating import Jinja2Templates
-from pyngrok import conf, ngrok # type: ignore
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import ngrok_ext, ngrok_renderer
-
-templates = Jinja2Templates(directory="templates")
-
-
-def log_event_callback(log):
- string = str(log)
- string2 = string[string.find('url="https') : string.find('url="https') + 80]
- if string2:
- string3 = string2
- string4 = string3[4:]
- global string5
- string5 = string4.replace('"', "")
-
-
-conf.get_default().log_event_callback = log_event_callback
-
-ngrok_authtoken = getenv("NGROK_AUTHTOKEN")
-if ngrok_authtoken is not None:
- ngrok.set_auth_token(ngrok_authtoken)
-
-port = getenv("PORT")
-ngrok_tunnel = ngrok.connect(port)
-
-
-@ngrok_ext.get("/")
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return ngrok_renderer().TemplateResponse(
- "ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()} # type: ignore
- )
diff --git a/lnbits/extensions/paywall/README.md b/lnbits/extensions/paywall/README.md
deleted file mode 100644
index 738485e2..00000000
--- a/lnbits/extensions/paywall/README.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Paywall
-
-## Hide content behind a paywall, a user has to pay some amount to access your hidden content
-
-A Paywall is a way of restricting to content via a purchase or paid subscription. For example to read a determined blog post, or to continue reading further, to access a downloads area, etc...
-
-## Usage
-
-1. Create a paywall by clicking "NEW PAYWALL"\
- 
-2. Fill the options for your PAYWALL
- - select the wallet
- - set the link that will be unlocked after a successful payment
- - give your paywall a _Title_
- - an optional small description
- - and set an amount a user must pay to access the hidden content. Note this is the minimum amount, a user can over pay if they wish
- - if _Remember payments_ is checked, a returning paying user won't have to pay again for the same content.\
- 
-3. You can then use your paywall link to secure your awesome content\
- 
-4. When a user wants to access your hidden content, he can use the minimum amount or increase and click the "_Check icon_" to generate an invoice, user will then be redirected to the content page\
- 
diff --git a/lnbits/extensions/paywall/__init__.py b/lnbits/extensions/paywall/__init__.py
deleted file mode 100644
index 5565a934..00000000
--- a/lnbits/extensions/paywall/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from fastapi import APIRouter
-from starlette.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_paywall")
-
-paywall_ext: APIRouter = APIRouter(prefix="/paywall", tags=["Paywall"])
-
-paywall_static_files = [
- {
- "path": "/paywall/static",
- "app": StaticFiles(directory="lnbits/extensions/paywall/static"),
- "name": "paywall_static",
- }
-]
-
-
-def paywall_renderer():
- return template_renderer(["lnbits/extensions/paywall/templates"])
-
-
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/paywall/config.json b/lnbits/extensions/paywall/config.json
deleted file mode 100644
index 749d1989..00000000
--- a/lnbits/extensions/paywall/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Paywall",
- "short_description": "Create paywalls for content",
- "tile": "/paywall/static/image/paywall.png",
- "contributors": ["eillarra"]
-}
diff --git a/lnbits/extensions/paywall/crud.py b/lnbits/extensions/paywall/crud.py
deleted file mode 100644
index cb9e210d..00000000
--- a/lnbits/extensions/paywall/crud.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from typing import List, Optional, Union
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import CreatePaywall, Paywall
-
-
-async def create_paywall(wallet_id: str, data: CreatePaywall) -> Paywall:
- paywall_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO paywall.paywalls (id, wallet, url, memo, description, amount, remembers)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- """,
- (
- paywall_id,
- wallet_id,
- data.url,
- data.memo,
- data.description,
- data.amount,
- int(data.remembers),
- ),
- )
-
- paywall = await get_paywall(paywall_id)
- assert paywall, "Newly created paywall couldn't be retrieved"
- return paywall
-
-
-async def get_paywall(paywall_id: str) -> Optional[Paywall]:
- row = await db.fetchone(
- "SELECT * FROM paywall.paywalls WHERE id = ?", (paywall_id,)
- )
- return Paywall.from_row(row) if row else None
-
-
-async def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM paywall.paywalls WHERE wallet IN ({q})", (*wallet_ids,)
- )
-
- return [Paywall.from_row(row) for row in rows]
-
-
-async def delete_paywall(paywall_id: str) -> None:
- await db.execute("DELETE FROM paywall.paywalls WHERE id = ?", (paywall_id,))
diff --git a/lnbits/extensions/paywall/migrations.py b/lnbits/extensions/paywall/migrations.py
deleted file mode 100644
index 9b3341fd..00000000
--- a/lnbits/extensions/paywall/migrations.py
+++ /dev/null
@@ -1,63 +0,0 @@
-async def m001_initial(db):
- """
- Initial paywalls table.
- """
- await db.execute(
- f"""
- CREATE TABLE paywall.paywalls (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- secret TEXT NOT NULL,
- url TEXT NOT NULL,
- memo TEXT NOT NULL,
- amount {db.big_int} NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
-
-
-async def m002_redux(db):
- """
- Creates an improved paywalls table and migrates the existing data.
- """
- await db.execute("ALTER TABLE paywall.paywalls RENAME TO paywalls_old")
- await db.execute(
- f"""
- CREATE TABLE paywall.paywalls (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- url TEXT NOT NULL,
- memo TEXT NOT NULL,
- description TEXT NULL,
- amount {db.big_int} DEFAULT 0,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """,
- remembers INTEGER DEFAULT 0,
- extras TEXT NULL
- );
- """
- )
-
- for row in [
- list(row) for row in await db.fetchall("SELECT * FROM paywall.paywalls_old")
- ]:
- await db.execute(
- """
- INSERT INTO paywall.paywalls (
- id,
- wallet,
- url,
- memo,
- amount,
- time
- )
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (row[0], row[1], row[3], row[4], row[5], row[6]),
- )
-
- await db.execute("DROP TABLE paywall.paywalls_old")
diff --git a/lnbits/extensions/paywall/models.py b/lnbits/extensions/paywall/models.py
deleted file mode 100644
index 7082c541..00000000
--- a/lnbits/extensions/paywall/models.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import json
-from sqlite3 import Row
-from typing import Optional
-
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class CreatePaywall(BaseModel):
- url: str = Query(...)
- memo: str = Query(...)
- description: str = Query(None)
- amount: int = Query(..., ge=0)
- remembers: bool = Query(...)
-
-
-class CreatePaywallInvoice(BaseModel):
- amount: int = Query(..., ge=1)
-
-
-class CheckPaywallInvoice(BaseModel):
- payment_hash: str = Query(...)
-
-
-class Paywall(BaseModel):
- id: str
- wallet: str
- url: str
- memo: str
- description: Optional[str]
- amount: int
- time: int
- remembers: bool
- extras: Optional[dict]
-
- @classmethod
- def from_row(cls, row: Row) -> "Paywall":
- data = dict(row)
- data["remembers"] = bool(data["remembers"])
- data["extras"] = json.loads(data["extras"]) if data["extras"] else None
- return cls(**data)
diff --git a/lnbits/extensions/paywall/static/image/paywall.png b/lnbits/extensions/paywall/static/image/paywall.png
deleted file mode 100644
index 0331a953..00000000
Binary files a/lnbits/extensions/paywall/static/image/paywall.png and /dev/null differ
diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html
deleted file mode 100644
index 0fd8bdd3..00000000
--- a/lnbits/extensions/paywall/templates/paywall/_api_docs.html
+++ /dev/null
@@ -1,148 +0,0 @@
-
-
-
-
-
- GET /paywall/api/v1/paywalls
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<paywall_object>, ...]
- Curl example
- curl -X GET {{ request.base_url }}paywall/api/v1/paywalls -H
- "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- POST /paywall/api/v1/paywalls
- Headers
- {"X-Api-Key": <admin_key>}
- Body (application/json)
- {"amount": <integer>, "description": <string>, "memo":
- <string>, "remembers": <boolean>, "url":
- <string>}
-
- Returns 201 CREATED (application/json)
-
- {"amount": <integer>, "description": <string>, "id":
- <string>, "memo": <string>, "remembers": <boolean>,
- "time": <int>, "url": <string>, "wallet":
- <string>}
- Curl example
- curl -X POST {{ request.base_url }}paywall/api/v1/paywalls -d
- '{"url": <string>, "memo": <string>, "description":
- <string>, "amount": <integer>, "remembers":
- <boolean>}' -H "Content-type: application/json" -H "X-Api-Key:
- {{ user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- POST
- /paywall/api/v1/paywalls/<paywall_id>/invoice
- Body (application/json)
- {"amount": <integer>}
-
- Returns 201 CREATED (application/json)
-
- {"payment_hash": <string>, "payment_request":
- <string>}
- Curl example
- curl -X POST {{ request.base_url
- }}paywall/api/v1/paywalls/<paywall_id>/invoice -d '{"amount":
- <integer>}' -H "Content-type: application/json"
-
-
-
-
-
-
-
- POST
- /paywall/api/v1/paywalls/<paywall_id>/check_invoice
- Body (application/json)
- {"payment_hash": <string>}
-
- Returns 200 OK (application/json)
-
- {"paid": false}
- {"paid": true, "url": <string>, "remembers":
- <boolean>}
- Curl example
- curl -X POST {{ request.base_url
- }}paywall/api/v1/paywalls/<paywall_id>/check_invoice -d
- '{"payment_hash": <string>}' -H "Content-type: application/json"
-
-
-
-
-
-
-
- DELETE
- /paywall/api/v1/paywalls/<paywall_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Returns 204 NO CONTENT
-
- Curl example
- curl -X DELETE {{ request.base_url
- }}paywall/api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{
- user.wallets[0].adminkey }}"
-
-
-
-
-
diff --git a/lnbits/extensions/paywall/templates/paywall/display.html b/lnbits/extensions/paywall/templates/paywall/display.html
deleted file mode 100644
index a5edd102..00000000
--- a/lnbits/extensions/paywall/templates/paywall/display.html
+++ /dev/null
@@ -1,168 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
- {{ paywall.memo }}
- {% if paywall.description %}
- {{ paywall.description }}
- {% endif %}
-
-
-
-
-
- Send
-
-
-
-
-
-
-
-
-
- Copy invoice
- Cancel
-
-
-
-
-
-
- You can access the URL behind this paywall:
- {% raw %}{{ redirectUrl }}{% endraw %}
-
-
- Open URL
-
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/paywall/templates/paywall/index.html b/lnbits/extensions/paywall/templates/paywall/index.html
deleted file mode 100644
index 482d1465..00000000
--- a/lnbits/extensions/paywall/templates/paywall/index.html
+++ /dev/null
@@ -1,312 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New paywall
-
-
-
-
-
-
-
-
Paywalls
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} paywall extension
-
-
-
-
- {% include "paywall/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Remember payments
- A succesful payment will be registered in the browser's
- storage, so the user doesn't need to pay again to access the
- URL.
-
-
-
-
- Create paywall
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/paywall/views.py b/lnbits/extensions/paywall/views.py
deleted file mode 100644
index 340f23c3..00000000
--- a/lnbits/extensions/paywall/views.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import paywall_ext, paywall_renderer
-from .crud import get_paywall
-
-
-@paywall_ext.get("/")
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return paywall_renderer().TemplateResponse(
- "paywall/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@paywall_ext.get("/{paywall_id}")
-async def display(request: Request, paywall_id):
- paywall = await get_paywall(paywall_id)
- if not paywall:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
- )
- return paywall_renderer().TemplateResponse(
- "paywall/display.html", {"request": request, "paywall": paywall}
- )
diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py
deleted file mode 100644
index 26480e01..00000000
--- a/lnbits/extensions/paywall/views_api.py
+++ /dev/null
@@ -1,105 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Query
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_user, get_wallet
-from lnbits.core.services import check_transaction_status, create_invoice
-from lnbits.decorators import WalletTypeInfo, get_key_type
-
-from . import paywall_ext
-from .crud import create_paywall, delete_paywall, get_paywall, get_paywalls
-from .models import CheckPaywallInvoice, CreatePaywall, CreatePaywallInvoice
-
-
-@paywall_ext.get("/api/v1/paywalls")
-async def api_paywalls(
- wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
-):
- wallet_ids = [wallet.wallet.id]
-
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- return [paywall.dict() for paywall in await get_paywalls(wallet_ids)]
-
-
-@paywall_ext.post("/api/v1/paywalls")
-async def api_paywall_create(
- data: CreatePaywall, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- paywall = await create_paywall(wallet_id=wallet.wallet.id, data=data)
- return paywall.dict()
-
-
-@paywall_ext.delete("/api/v1/paywalls/{paywall_id}")
-async def api_paywall_delete(
- paywall_id, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- paywall = await get_paywall(paywall_id)
-
- if not paywall:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
- )
-
- if paywall.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your paywall."
- )
-
- await delete_paywall(paywall_id)
- return "", HTTPStatus.NO_CONTENT
-
-
-@paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
-async def api_paywall_create_invoice(
- data: CreatePaywallInvoice, paywall_id: str = Query(None)
-):
- paywall = await get_paywall(paywall_id)
- assert paywall
- if data.amount < paywall.amount:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=f"Minimum amount is {paywall.amount} sat.",
- )
- try:
- amount = data.amount if data.amount > paywall.amount else paywall.amount
- payment_hash, payment_request = await create_invoice(
- wallet_id=paywall.wallet,
- amount=amount,
- memo=f"{paywall.memo}",
- extra={"tag": "paywall"},
- )
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
-
- return {"payment_hash": payment_hash, "payment_request": payment_request}
-
-
-@paywall_ext.post("/api/v1/paywalls/check_invoice/{paywall_id}")
-async def api_paywal_check_invoice(
- data: CheckPaywallInvoice, paywall_id: str = Query(None)
-):
- paywall = await get_paywall(paywall_id)
- payment_hash = data.payment_hash
- if not paywall:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
- )
- try:
- status = await check_transaction_status(paywall.wallet, payment_hash)
- is_paid = not status.pending
- except Exception:
- return {"paid": False}
-
- if is_paid:
- wallet = await get_wallet(paywall.wallet)
- assert wallet
- payment = await wallet.get_payment(payment_hash)
- assert payment
- await payment.set_pending(False)
-
- return {"paid": True, "url": paywall.url, "remembers": paywall.remembers}
- return {"paid": False}
diff --git a/lnbits/extensions/satspay/README.md b/lnbits/extensions/satspay/README.md
deleted file mode 100644
index 4fb24980..00000000
--- a/lnbits/extensions/satspay/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# SatsPay Server
-
-## Create onchain and LN charges. Includes webhooks!
-
-Easilly create invoices that support Lightning Network and on-chain BTC payment.
-
-1. Create a "NEW CHARGE"\
- 
-2. Fill out the invoice fields
- - set a descprition for the payment
- - the amount in sats
- - the time, in minutes, the invoice is valid for, after this period the invoice can't be payed
- - set a webhook that will get the transaction details after a successful payment
- - set to where the user should redirect after payment
- - set the text for the button that will show after payment (not setting this, will display "NONE" in the button)
- - select if you want onchain payment, LN payment or both
- - depending on what you select you'll have to choose the respective wallets where to receive your payment\
- 
-3. The charge will appear on the _Charges_ section\
- 
-4. Your customer/payee will get the payment page
- - they can choose to pay on LN\
- 
- - or pay on chain\
- 
-5. You can check the state of your charges in LNbits\
- 
diff --git a/lnbits/extensions/satspay/__init__.py b/lnbits/extensions/satspay/__init__.py
deleted file mode 100644
index 8f115a3c..00000000
--- a/lnbits/extensions/satspay/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-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_satspay")
-
-
-satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
-
-satspay_static_files = [
- {
- "path": "/satspay/static",
- "app": StaticFiles(directory="lnbits/extensions/satspay/static"),
- "name": "satspay_static",
- }
-]
-
-
-def satspay_renderer():
- return template_renderer(["lnbits/extensions/satspay/templates"])
-
-
-from .tasks import wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def satspay_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/satspay/config.json b/lnbits/extensions/satspay/config.json
deleted file mode 100644
index 6104d360..00000000
--- a/lnbits/extensions/satspay/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "SatsPay Server",
- "short_description": "Create onchain and LN charges",
- "tile": "/satspay/static/image/satspay.png",
- "contributors": ["arcbtc"]
-}
diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py
deleted file mode 100644
index c13d0a4b..00000000
--- a/lnbits/extensions/satspay/crud.py
+++ /dev/null
@@ -1,181 +0,0 @@
-import json
-from typing import List, Optional
-
-from loguru import logger
-
-from lnbits.core.services import create_invoice
-from lnbits.core.views.api import api_payment
-from lnbits.helpers import urlsafe_short_hash
-
-from ..watchonly.crud import get_config, get_fresh_address # type: ignore
-from . import db
-from .helpers import fetch_onchain_balance
-from .models import Charges, CreateCharge, SatsPayThemes
-
-
-async def create_charge(user: str, data: CreateCharge) -> Charges:
- data = CreateCharge(**data.dict())
- charge_id = urlsafe_short_hash()
- if data.onchainwallet:
- config = await get_config(user)
- assert config
- data.extra = json.dumps(
- {"mempool_endpoint": config.mempool_endpoint, "network": config.network}
- )
- onchain = await get_fresh_address(data.onchainwallet)
- if not onchain:
- raise Exception(f"Wallet '{data.onchainwallet}' can no longer be accessed.")
- onchainaddress = onchain.address
- else:
- onchainaddress = None
- if data.lnbitswallet:
- payment_hash, payment_request = await create_invoice(
- wallet_id=data.lnbitswallet,
- amount=data.amount,
- memo=charge_id,
- extra={"tag": "charge"},
- )
- else:
- payment_hash = None
- payment_request = None
- await db.execute(
- """
- INSERT INTO satspay.charges (
- id,
- "user",
- description,
- onchainwallet,
- onchainaddress,
- lnbitswallet,
- payment_request,
- payment_hash,
- webhook,
- completelink,
- completelinktext,
- time,
- amount,
- balance,
- extra,
- custom_css
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- charge_id,
- user,
- data.description,
- data.onchainwallet,
- onchainaddress,
- data.lnbitswallet,
- payment_request,
- payment_hash,
- data.webhook,
- data.completelink,
- data.completelinktext,
- data.time,
- data.amount,
- 0,
- data.extra,
- data.custom_css,
- ),
- )
- charge = await get_charge(charge_id)
- assert charge, "Newly created charge does not exist"
- return charge
-
-
-async def update_charge(charge_id: str, **kwargs) -> Optional[Charges]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE satspay.charges SET {q} WHERE id = ?", (*kwargs.values(), charge_id)
- )
- row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
- return Charges.from_row(row) if row else None
-
-
-async def get_charge(charge_id: str) -> Optional[Charges]:
- row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
- return Charges.from_row(row) if row else None
-
-
-async def get_charges(user: str) -> List[Charges]:
- rows = await db.fetchall(
- """SELECT * FROM satspay.charges WHERE "user" = ? ORDER BY "timestamp" DESC """,
- (user,),
- )
- return [Charges.from_row(row) for row in rows]
-
-
-async def delete_charge(charge_id: str) -> None:
- await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,))
-
-
-async def check_address_balance(charge_id: str) -> Optional[Charges]:
- charge = await get_charge(charge_id)
- assert charge
-
- if not charge.paid:
- if charge.onchainaddress:
- try:
- respAmount = await fetch_onchain_balance(charge)
- if respAmount > charge.balance:
- await update_charge(charge_id=charge_id, balance=respAmount)
- except Exception as e:
- logger.warning(e)
- if charge.lnbitswallet:
- invoice_status = await api_payment(charge.payment_hash)
-
- if invoice_status["paid"]:
- return await update_charge(charge_id=charge_id, balance=charge.amount)
- return await get_charge(charge_id)
-
-
-################## SETTINGS ###################
-
-
-async def save_theme(data: SatsPayThemes, css_id: Optional[str]):
- # insert or update
- if css_id:
- await db.execute(
- """
- UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
- """,
- (data.custom_css, data.title, css_id),
- )
- else:
- css_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO satspay.themes (
- css_id,
- title,
- "user",
- custom_css
- )
- VALUES (?, ?, ?, ?)
- """,
- (
- css_id,
- data.title,
- data.user,
- data.custom_css,
- ),
- )
- return await get_theme(css_id)
-
-
-async def get_theme(css_id: str) -> Optional[SatsPayThemes]:
- row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
- return SatsPayThemes.from_row(row) if row else None
-
-
-async def get_themes(user_id: str) -> List[SatsPayThemes]:
- rows = await db.fetchall(
- """SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "title" DESC """,
- (user_id,),
- )
- return [SatsPayThemes.from_row(row) for row in rows]
-
-
-async def delete_theme(theme_id: str) -> None:
- await db.execute("DELETE FROM satspay.themes WHERE css_id = ?", (theme_id,))
diff --git a/lnbits/extensions/satspay/helpers.py b/lnbits/extensions/satspay/helpers.py
deleted file mode 100644
index 1967c79d..00000000
--- a/lnbits/extensions/satspay/helpers.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import httpx
-from loguru import logger
-
-from .models import Charges
-
-
-def public_charge(charge: Charges):
- c = {
- "id": charge.id,
- "description": charge.description,
- "onchainaddress": charge.onchainaddress,
- "payment_request": charge.payment_request,
- "payment_hash": charge.payment_hash,
- "time": charge.time,
- "amount": charge.amount,
- "balance": charge.balance,
- "paid": charge.paid,
- "timestamp": charge.timestamp,
- "time_elapsed": charge.time_elapsed,
- "time_left": charge.time_left,
- "custom_css": charge.custom_css,
- }
-
- if charge.paid:
- c["completelink"] = charge.completelink
- c["completelinktext"] = charge.completelinktext
-
- return c
-
-
-async def call_webhook(charge: Charges):
- async with httpx.AsyncClient() as client:
- try:
- assert charge.webhook
- r = await client.post(
- charge.webhook,
- json=public_charge(charge),
- timeout=40,
- )
- return {
- "webhook_success": r.is_success,
- "webhook_message": r.reason_phrase,
- "webhook_response": r.text,
- }
- except Exception as e:
- logger.warning(f"Failed to call webhook for charge {charge.id}")
- logger.warning(e)
- return {"webhook_success": False, "webhook_message": str(e)}
-
-
-async def fetch_onchain_balance(charge: Charges):
- endpoint = (
- f"{charge.config.mempool_endpoint}/testnet"
- if charge.config.network == "Testnet"
- else charge.config.mempool_endpoint
- )
- assert endpoint
- assert charge.onchainaddress
- async with httpx.AsyncClient() as client:
- r = await client.get(endpoint + "/api/address/" + charge.onchainaddress)
- return r.json()["chain_stats"]["funded_txo_sum"]
diff --git a/lnbits/extensions/satspay/migrations.py b/lnbits/extensions/satspay/migrations.py
deleted file mode 100644
index 48875469..00000000
--- a/lnbits/extensions/satspay/migrations.py
+++ /dev/null
@@ -1,64 +0,0 @@
-async def m001_initial(db):
- """
- Initial wallet table.
- """
-
- await db.execute(
- f"""
- CREATE TABLE satspay.charges (
- id TEXT NOT NULL PRIMARY KEY,
- "user" TEXT,
- description TEXT,
- onchainwallet TEXT,
- onchainaddress TEXT,
- lnbitswallet TEXT,
- payment_request TEXT,
- payment_hash TEXT,
- webhook TEXT,
- completelink TEXT,
- completelinktext TEXT,
- time INTEGER,
- amount {db.big_int},
- balance {db.big_int} DEFAULT 0,
- timestamp TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
-
-
-async def m002_add_charge_extra_data(db):
- """
- Add 'extra' column for storing various config about the charge (JSON format)
- """
- await db.execute(
- """ALTER TABLE satspay.charges
- ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}';
- """
- )
-
-
-async def m003_add_themes_table(db):
- """
- Themes table
- """
-
- await db.execute(
- """
- CREATE TABLE satspay.themes (
- css_id TEXT NOT NULL PRIMARY KEY,
- "user" TEXT,
- title TEXT,
- custom_css TEXT
- );
- """
- )
-
-
-async def m004_add_custom_css_to_charges(db):
- """
- Add custom css option column to the 'charges' table
- """
-
- await db.execute("ALTER TABLE satspay.charges ADD COLUMN custom_css TEXT;")
diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py
deleted file mode 100644
index c9da401a..00000000
--- a/lnbits/extensions/satspay/models.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import json
-from datetime import datetime, timedelta
-from sqlite3 import Row
-from typing import Optional
-
-from fastapi.param_functions import Query
-from pydantic import BaseModel
-
-DEFAULT_MEMPOOL_CONFIG = (
- '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}'
-)
-
-
-class CreateCharge(BaseModel):
- onchainwallet: str = Query(None)
- lnbitswallet: str = Query(None)
- description: str = Query(...)
- webhook: str = Query(None)
- completelink: str = Query(None)
- completelinktext: str = Query(None)
- custom_css: Optional[str]
- time: int = Query(..., ge=1)
- amount: int = Query(..., ge=1)
- extra: str = DEFAULT_MEMPOOL_CONFIG
-
-
-class ChargeConfig(BaseModel):
- mempool_endpoint: Optional[str]
- network: Optional[str]
- webhook_success: Optional[bool] = False
- webhook_message: Optional[str]
-
-
-class Charges(BaseModel):
- id: str
- description: Optional[str]
- onchainwallet: Optional[str]
- onchainaddress: Optional[str]
- lnbitswallet: Optional[str]
- payment_request: Optional[str]
- payment_hash: Optional[str]
- webhook: Optional[str]
- completelink: Optional[str]
- completelinktext: Optional[str] = "Back to Merchant"
- custom_css: Optional[str]
- extra: str = DEFAULT_MEMPOOL_CONFIG
- time: int
- amount: int
- balance: int
- timestamp: int
-
- @classmethod
- def from_row(cls, row: Row) -> "Charges":
- return cls(**dict(row))
-
- @property
- def time_left(self):
- now = datetime.utcnow().timestamp()
- start = datetime.fromtimestamp(self.timestamp)
- expiration = (start + timedelta(minutes=self.time)).timestamp()
- return (expiration - now) / 60
-
- @property
- def time_elapsed(self):
- return self.time_left < 0
-
- @property
- def paid(self):
- if self.balance >= self.amount:
- return True
- else:
- return False
-
- @property
- def config(self) -> ChargeConfig:
- charge_config = json.loads(self.extra)
- return ChargeConfig(**charge_config)
-
- def must_call_webhook(self):
- return self.webhook and self.paid and self.config.webhook_success is False
-
-
-class SatsPayThemes(BaseModel):
- css_id: str = Query(None)
- title: str = Query(None)
- custom_css: str = Query(None)
- user: Optional[str]
-
- @classmethod
- def from_row(cls, row: Row) -> "SatsPayThemes":
- return cls(**dict(row))
diff --git a/lnbits/extensions/satspay/static/image/satspay.png b/lnbits/extensions/satspay/static/image/satspay.png
deleted file mode 100644
index 82791407..00000000
Binary files a/lnbits/extensions/satspay/static/image/satspay.png and /dev/null differ
diff --git a/lnbits/extensions/satspay/static/js/utils.js b/lnbits/extensions/satspay/static/js/utils.js
deleted file mode 100644
index 5317673f..00000000
--- a/lnbits/extensions/satspay/static/js/utils.js
+++ /dev/null
@@ -1,36 +0,0 @@
-const sleep = ms => new Promise(r => setTimeout(r, ms))
-const retryWithDelay = async function (fn, retryCount = 0) {
- try {
- await sleep(25)
- // Do not return the call directly, use result.
- // Otherwise the error will not be cought in this try-catch block.
- const result = await fn()
- return result
- } catch (err) {
- if (retryCount > 100) throw err
- await sleep((retryCount + 1) * 1000)
- return retryWithDelay(fn, retryCount + 1)
- }
-}
-
-const mapCharge = (obj, oldObj = {}) => {
- const charge = {...oldObj, ...obj}
-
- charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time
- charge.time = minutesToTime(obj.time)
- charge.timeLeft = minutesToTime(obj.time_left)
-
- charge.displayUrl = ['/satspay/', obj.id].join('')
- charge.expanded = oldObj.expanded || false
- charge.pendingBalance = oldObj.pendingBalance || 0
- charge.extra = charge.extra ? JSON.parse(charge.extra) : charge.extra
- return charge
-}
-
-const mapCSS = (obj, oldObj = {}) => {
- const theme = _.clone(obj)
- return theme
-}
-
-const minutesToTime = min =>
- min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : ''
diff --git a/lnbits/extensions/satspay/tasks.py b/lnbits/extensions/satspay/tasks.py
deleted file mode 100644
index 2c636351..00000000
--- a/lnbits/extensions/satspay/tasks.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import asyncio
-import json
-
-from loguru import logger
-
-from lnbits.core.models import Payment
-from lnbits.helpers import get_current_extension_name
-from lnbits.tasks import register_invoice_listener
-
-from .crud import check_address_balance, get_charge, update_charge
-from .helpers import call_webhook
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue, get_current_extension_name())
-
- 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") != "charge":
- # not a charge invoice
- return
-
- assert payment.memo
- charge = await get_charge(payment.memo)
- if not charge:
- logger.error("this should never happen", payment)
- return
-
- await payment.set_pending(False)
- charge = await check_address_balance(charge_id=charge.id)
- assert charge
-
- if charge.must_call_webhook():
- resp = await call_webhook(charge)
- extra = {**charge.config.dict(), **resp}
- await update_charge(charge_id=charge.id, extra=json.dumps(extra))
diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html
deleted file mode 100644
index 2adab8c1..00000000
--- a/lnbits/extensions/satspay/templates/satspay/_api_docs.html
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
- SatsPayServer, create Onchain/LN charges. WARNING: If using with the
- WatchOnly extension, we highly reccomend using a fresh extended public Key
- specifically for SatsPayServer!
-
- Created by,
- Ben Arc ,
- motorina0
-
-
-
- Swagger REST API Documentation
-
-
diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html
deleted file mode 100644
index 3cde24c3..00000000
--- a/lnbits/extensions/satspay/templates/satspay/display.html
+++ /dev/null
@@ -1,479 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
-
-
-
- Time elapsed
-
-
- Charge paid
-
-
-
-
-
- Awaiting payment...
-
- {% raw %} {{ charge.timeLeft }} {% endraw %}
-
-
-
-
-
-
-
-
-
-
-
Total to pay:
-
-
- sat
-
-
-
-
-
Amount paid:
-
-
-
- sat
-
-
-
-
Amount pending:
-
-
- sat
-
-
-
-
-
Amount due:
-
-
-
- sat
-
-
- none
-
-
-
-
-
-
-
-
-
-
-
- bitcoin lightning payment method not available
-
-
-
- pay with lightning
-
-
-
-
-
- bitcoin onchain payment method not available
-
-
-
- pay onchain
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Pay this lightning-network invoice:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Send
-
-
- sats to this onchain address
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %} {% block styles %}
-
-
-{% endblock %} {% block scripts %}
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html
deleted file mode 100644
index 74b3d2cc..00000000
--- a/lnbits/extensions/satspay/templates/satspay/index.html
+++ /dev/null
@@ -1,1011 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- {% raw %}
- New charge
-
-
- New CSS Theme
-
- New CSS Theme
- For security reason, custom css is only available to server
- admins.
-
-
-
-
-
-
-
-
Charges
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Export to CSV
-
-
-
-
-
-
-
-
-
-
- Status
- Title
- Time Left (hh:mm)
- Time To Pay (hh:mm)
- Amount To Pay
- Balance
- Pending Balance
- Onchain Address
-
-
-
-
-
-
-
-
-
-
-
-
- expired
-
-
-
- paid
-
-
- waiting
-
-
-
- {{props.row.description}}
-
-
- {{props.row.timeLeft}}
-
-
-
-
- {{props.row.time}}
-
-
- {{props.row.amount}}
-
-
- {{props.row.balance}}
-
-
-
- {{props.row.pendingBalance ? props.row.pendingBalance : ''}}
-
-
-
- {{props.row.onchainaddress}}
-
-
-
-
-
-
Onchain Wallet:
-
- {{getOnchainWalletName(props.row.onchainwallet)}}
-
-
-
-
LNbits Wallet:
-
- {{getLNbitsWalletName(props.row.lnbitswallet)}}
-
-
-
-
-
-
Webhook:
-
-
-
- {{props.row.webhook_message }}
-
-
-
-
-
ID:
-
{{props.row.id}}
-
-
-
-
- Details
- Refresh Balance
-
-
- Delete
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
- {% raw %}
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} satspay Extension
-
-
-
-
- {% include "satspay/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Onchain Wallet (watch-only) extension MUST be activated and
- have a wallet
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create Charge
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
- Update CSS theme
- Save CSS theme
- Cancel
-
-
-
-
-
-
-
-
-
-
- Close
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py
deleted file mode 100644
index 175b00bd..00000000
--- a/lnbits/extensions/satspay/views.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, HTTPException, Request, Response
-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 satspay_ext, satspay_renderer
-from .crud import get_charge, get_theme
-from .helpers import public_charge
-
-templates = Jinja2Templates(directory="templates")
-
-
-@satspay_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return satspay_renderer().TemplateResponse(
- "satspay/index.html",
- {"request": request, "user": user.dict(), "admin": user.admin},
- )
-
-
-@satspay_ext.get("/{charge_id}", response_class=HTMLResponse)
-async def display_charge(request: Request, charge_id: str):
- charge = await get_charge(charge_id)
- if not charge:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
- )
-
- return satspay_renderer().TemplateResponse(
- "satspay/display.html",
- {
- "request": request,
- "charge_data": public_charge(charge),
- "mempool_endpoint": charge.config.mempool_endpoint,
- "network": charge.config.network,
- },
- )
-
-
-@satspay_ext.get("/css/{css_id}")
-async def display_css(css_id: str):
- theme = await get_theme(css_id)
- if theme:
- return Response(content=theme.custom_css, media_type="text/css")
- return None
diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py
deleted file mode 100644
index 200773fb..00000000
--- a/lnbits/extensions/satspay/views_api.py
+++ /dev/null
@@ -1,180 +0,0 @@
-import json
-from http import HTTPStatus
-
-from fastapi import Depends, HTTPException, Query
-from loguru import logger
-
-from lnbits.decorators import (
- WalletTypeInfo,
- check_admin,
- get_key_type,
- require_admin_key,
- require_invoice_key,
-)
-
-from . import satspay_ext
-from .crud import (
- check_address_balance,
- create_charge,
- delete_charge,
- delete_theme,
- get_charge,
- get_charges,
- get_theme,
- get_themes,
- save_theme,
- update_charge,
-)
-from .helpers import call_webhook, public_charge
-from .models import CreateCharge, SatsPayThemes
-
-
-@satspay_ext.post("/api/v1/charge")
-async def api_charge_create(
- data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
-):
- try:
- charge = await create_charge(user=wallet.wallet.user, data=data)
- assert charge
- return {
- **charge.dict(),
- **{"time_elapsed": charge.time_elapsed},
- **{"time_left": charge.time_left},
- **{"paid": charge.paid},
- }
- except Exception as ex:
- logger.debug(f"Satspay error: {str}")
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
- )
-
-
-@satspay_ext.put(
- "/api/v1/charge/{charge_id}", dependencies=[Depends(require_admin_key)]
-)
-async def api_charge_update(
- data: CreateCharge,
- charge_id: str,
-):
- charge = await update_charge(charge_id=charge_id, data=data)
- assert charge
- return charge.dict()
-
-
-@satspay_ext.get("/api/v1/charges")
-async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
- try:
- return [
- {
- **charge.dict(),
- **{"time_elapsed": charge.time_elapsed},
- **{"time_left": charge.time_left},
- **{"paid": charge.paid},
- **{"webhook_message": charge.config.webhook_message},
- }
- for charge in await get_charges(wallet.wallet.user)
- ]
- except:
- return ""
-
-
-@satspay_ext.get("/api/v1/charge/{charge_id}", dependencies=[Depends(get_key_type)])
-async def api_charge_retrieve(charge_id: str):
- charge = await get_charge(charge_id)
-
- if not charge:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
- )
-
- return {
- **charge.dict(),
- **{"time_elapsed": charge.time_elapsed},
- **{"time_left": charge.time_left},
- **{"paid": charge.paid},
- }
-
-
-@satspay_ext.delete("/api/v1/charge/{charge_id}", dependencies=[Depends(get_key_type)])
-async def api_charge_delete(charge_id: str):
- charge = await get_charge(charge_id)
-
- if not charge:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
- )
-
- await delete_charge(charge_id)
- return "", HTTPStatus.NO_CONTENT
-
-
-#############################BALANCE##########################
-
-
-@satspay_ext.get("/api/v1/charges/balance/{charge_ids}")
-async def api_charges_balance(charge_ids):
- charge_id_list = charge_ids.split(",")
- charges = []
- for charge_id in charge_id_list:
- charge = await api_charge_balance(charge_id)
- charges.append(charge)
- return charges
-
-
-@satspay_ext.get("/api/v1/charge/balance/{charge_id}")
-async def api_charge_balance(charge_id):
- charge = await check_address_balance(charge_id)
-
- if not charge:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
- )
-
- if charge.must_call_webhook():
- resp = await call_webhook(charge)
- extra = {**charge.config.dict(), **resp}
- await update_charge(charge_id=charge.id, extra=json.dumps(extra))
-
- return {**public_charge(charge)}
-
-
-#############################THEMES##########################
-
-
-@satspay_ext.post("/api/v1/themes", dependencies=[Depends(check_admin)])
-@satspay_ext.post("/api/v1/themes/{css_id}", dependencies=[Depends(check_admin)])
-async def api_themes_save(
- data: SatsPayThemes,
- wallet: WalletTypeInfo = Depends(require_admin_key),
- css_id: str = Query(...),
-):
-
- if css_id:
- theme = await save_theme(css_id=css_id, data=data)
- else:
- data.user = wallet.wallet.user
- theme = await save_theme(data=data, css_id="no_id")
- return theme
-
-
-@satspay_ext.get("/api/v1/themes")
-async def api_themes_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
- try:
- return await get_themes(wallet.wallet.user)
- except HTTPException:
- logger.error("Error loading satspay themes")
- logger.error(HTTPException)
- return ""
-
-
-@satspay_ext.delete("/api/v1/themes/{theme_id}", dependencies=[Depends(get_key_type)])
-async def api_theme_delete(theme_id):
- theme = await get_theme(theme_id)
-
- if not theme:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Theme does not exist."
- )
-
- await delete_theme(theme_id)
- return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/scrub/README.md b/lnbits/extensions/scrub/README.md
deleted file mode 100644
index 3b8d0b2d..00000000
--- a/lnbits/extensions/scrub/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Scrub
-
-## Automatically forward funds (Scrub) that get paid to the wallet to an LNURLpay or Lightning Address
-
-SCRUB is a small but handy extension that allows a user to take advantage of all the functionalities inside **LNbits** and upon a payment received to your LNbits wallet, automatically forward it to your desired wallet via LNURL or LNAddress!
-
-Only whole values, integers, are Scrubbed, amounts will be rounded down (example: 6.3 will be 6)! The decimals, if existing, will be kept in your wallet!
-
-[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
-
-## Usage
-
-1. Create an scrub (New Scrub link)\
- 
-
- - select the wallet to be _scrubbed_
- - make a small description
- - enter either an LNURL pay or a lightning address
-
- Make sure your LNURL or LNaddress is correct!
-
-2. A new scrub will show on the _Scrub links_ section\
- 
-
- - only one scrub can be created for each wallet!
- - You can _edit_ or _delete_ the Scrub at any time\
- 
-
-3. On your wallet, you'll see a transaction of a payment received and another right after it as apayment sent, marked with **#scrubed**\
- 
diff --git a/lnbits/extensions/scrub/__init__.py b/lnbits/extensions/scrub/__init__.py
deleted file mode 100644
index 29428af9..00000000
--- a/lnbits/extensions/scrub/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-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_scrub")
-
-scrub_static_files = [
- {
- "path": "/scrub/static",
- "app": StaticFiles(directory="lnbits/extensions/scrub/static"),
- "name": "scrub_static",
- }
-]
-
-scrub_ext: APIRouter = APIRouter(prefix="/scrub", tags=["scrub"])
-
-
-def scrub_renderer():
- return template_renderer(["lnbits/extensions/scrub/templates"])
-
-
-from .tasks import wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def scrub_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/scrub/config.json b/lnbits/extensions/scrub/config.json
deleted file mode 100644
index 93eb871a..00000000
--- a/lnbits/extensions/scrub/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Scrub",
- "short_description": "Pass payments to LNURLp/LNaddress",
- "tile": "/scrub/static/image/scrub.png",
- "contributors": ["arcbtc", "talvasconcelos"]
-}
diff --git a/lnbits/extensions/scrub/crud.py b/lnbits/extensions/scrub/crud.py
deleted file mode 100644
index 1772a8c5..00000000
--- a/lnbits/extensions/scrub/crud.py
+++ /dev/null
@@ -1,80 +0,0 @@
-from typing import List, Optional, Union
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import CreateScrubLink, ScrubLink
-
-
-async def create_scrub_link(data: CreateScrubLink) -> ScrubLink:
- scrub_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO scrub.scrub_links (
- id,
- wallet,
- description,
- payoraddress
- )
- VALUES (?, ?, ?, ?)
- """,
- (
- scrub_id,
- data.wallet,
- data.description,
- data.payoraddress,
- ),
- )
- link = await get_scrub_link(scrub_id)
- assert link, "Newly created link couldn't be retrieved"
- return link
-
-
-async def get_scrub_link(link_id: str) -> Optional[ScrubLink]:
- row = await db.fetchone("SELECT * FROM scrub.scrub_links WHERE id = ?", (link_id,))
- return ScrubLink(**row) if row else None
-
-
-async def get_scrub_links(wallet_ids: Union[str, List[str]]) -> List[ScrubLink]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"""
- SELECT * FROM scrub.scrub_links WHERE wallet IN ({q})
- ORDER BY id
- """,
- (*wallet_ids,),
- )
- return [ScrubLink(**row) for row in rows]
-
-
-async def update_scrub_link(link_id: int, **kwargs) -> Optional[ScrubLink]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE scrub.scrub_links SET {q} WHERE id = ?",
- (*kwargs.values(), link_id),
- )
- row = await db.fetchone("SELECT * FROM scrub.scrub_links WHERE id = ?", (link_id,))
- return ScrubLink(**row) if row else None
-
-
-async def delete_scrub_link(link_id: int) -> None:
- await db.execute("DELETE FROM scrub.scrub_links WHERE id = ?", (link_id,))
-
-
-async def get_scrub_by_wallet(wallet_id) -> Optional[ScrubLink]:
- row = await db.fetchone(
- "SELECT * from scrub.scrub_links WHERE wallet = ?",
- (wallet_id,),
- )
- return ScrubLink(**row) if row else None
-
-
-async def unique_scrubed_wallet(wallet_id):
- (row,) = await db.fetchone(
- "SELECT COUNT(wallet) FROM scrub.scrub_links WHERE wallet = ?",
- (wallet_id,),
- )
- return row
diff --git a/lnbits/extensions/scrub/migrations.py b/lnbits/extensions/scrub/migrations.py
deleted file mode 100644
index f1f4bade..00000000
--- a/lnbits/extensions/scrub/migrations.py
+++ /dev/null
@@ -1,14 +0,0 @@
-async def m001_initial(db):
- """
- Initial scrub table.
- """
- await db.execute(
- """
- CREATE TABLE scrub.scrub_links (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- description TEXT NOT NULL,
- payoraddress TEXT NOT NULL
- );
- """
- )
diff --git a/lnbits/extensions/scrub/models.py b/lnbits/extensions/scrub/models.py
deleted file mode 100644
index 8079f358..00000000
--- a/lnbits/extensions/scrub/models.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from sqlite3 import Row
-
-from pydantic import BaseModel
-from starlette.requests import Request
-
-from lnbits.lnurl import encode as lnurl_encode
-
-
-class CreateScrubLink(BaseModel):
- wallet: str
- description: str
- payoraddress: str
-
-
-class ScrubLink(BaseModel):
- id: str
- wallet: str
- description: str
- payoraddress: str
-
- @classmethod
- def from_row(cls, row: Row) -> "ScrubLink":
- data = dict(row)
- return cls(**data)
-
- def lnurl(self, req: Request) -> str:
- url = req.url_for("scrub.api_lnurl_response", link_id=self.id)
- return lnurl_encode(url)
diff --git a/lnbits/extensions/scrub/static/image/scrub.png b/lnbits/extensions/scrub/static/image/scrub.png
deleted file mode 100644
index b3d4d24f..00000000
Binary files a/lnbits/extensions/scrub/static/image/scrub.png and /dev/null differ
diff --git a/lnbits/extensions/scrub/static/js/index.js b/lnbits/extensions/scrub/static/js/index.js
deleted file mode 100644
index 43990792..00000000
--- a/lnbits/extensions/scrub/static/js/index.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
-
-Vue.component(VueQrcode.name, VueQrcode)
-
-var locationPath = [
- window.location.protocol,
- '//',
- window.location.host,
- window.location.pathname
-].join('')
-
-var mapScrubLink = obj => {
- obj._data = _.clone(obj)
- obj.date = Quasar.utils.date.formatDate(
- new Date(obj.time * 1000),
- 'YYYY-MM-DD HH:mm'
- )
- obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
- obj.print_url = [locationPath, 'print/', obj.id].join('')
- obj.pay_url = [locationPath, obj.id].join('')
- return obj
-}
-
-new Vue({
- el: '#vue',
- mixins: [windowMixin],
- data() {
- return {
- checker: null,
- payLinks: [],
- payLinksTable: {
- pagination: {
- rowsPerPage: 10
- }
- },
- formDialog: {
- show: false,
- data: {}
- },
- qrCodeDialog: {
- show: false,
- data: null
- }
- }
- },
- methods: {
- getScrubLinks() {
- LNbits.api
- .request(
- 'GET',
- '/scrub/api/v1/links?all_wallets=true',
- this.g.user.wallets[0].inkey
- )
- .then(response => {
- this.payLinks = response.data.map(mapScrubLink)
- })
- .catch(err => {
- clearInterval(this.checker)
- LNbits.utils.notifyApiError(err)
- })
- },
- closeFormDialog() {
- this.resetFormData()
- },
- openUpdateDialog(linkId) {
- const link = _.findWhere(this.payLinks, {id: linkId})
-
- this.formDialog.data = _.clone(link._data)
- this.formDialog.show = true
- },
- sendFormData() {
- const wallet = _.findWhere(this.g.user.wallets, {
- id: this.formDialog.data.wallet
- })
- let data = Object.freeze(this.formDialog.data)
- console.log(wallet, data)
-
- if (data.id) {
- this.updateScrubLink(wallet, data)
- } else {
- this.createScrubLink(wallet, data)
- }
- },
- resetFormData() {
- this.formDialog = {
- show: false,
- data: {}
- }
- },
- updateScrubLink(wallet, data) {
- LNbits.api
- .request('PUT', '/scrub/api/v1/links/' + data.id, wallet.adminkey, data)
- .then(response => {
- this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
- this.payLinks.push(mapScrubLink(response.data))
- this.formDialog.show = false
- this.resetFormData()
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- },
- createScrubLink(wallet, data) {
- LNbits.api
- .request('POST', '/scrub/api/v1/links', wallet.adminkey, data)
- .then(response => {
- console.log('RES', response)
- this.getScrubLinks()
- this.formDialog.show = false
- this.resetFormData()
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- },
- deleteScrubLink(linkId) {
- var link = _.findWhere(this.payLinks, {id: linkId})
-
- LNbits.utils
- .confirmDialog('Are you sure you want to delete this pay link?')
- .onOk(() => {
- LNbits.api
- .request(
- 'DELETE',
- '/scrub/api/v1/links/' + linkId,
- _.findWhere(this.g.user.wallets, {id: link.wallet}).adminkey
- )
- .then(response => {
- this.payLinks = _.reject(this.payLinks, obj => obj.id === linkId)
- })
- .catch(err => {
- LNbits.utils.notifyApiError(err)
- })
- })
- }
- },
- created() {
- if (this.g.user.wallets.length) {
- var getScrubLinks = this.getScrubLinks
- getScrubLinks()
- }
- }
-})
diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py
deleted file mode 100644
index 26249bb1..00000000
--- a/lnbits/extensions/scrub/tasks.py
+++ /dev/null
@@ -1,89 +0,0 @@
-import asyncio
-import json
-from http import HTTPStatus
-from math import floor
-from urllib.parse import urlparse
-
-import httpx
-from fastapi import HTTPException
-
-from lnbits import bolt11
-from lnbits.core.models import Payment
-from lnbits.core.services import pay_invoice
-from lnbits.helpers import get_current_extension_name
-from lnbits.tasks import register_invoice_listener
-
-from .crud import get_scrub_by_wallet
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue, get_current_extension_name())
-
- while True:
- payment = await invoice_queue.get()
- await on_invoice_paid(payment)
-
-
-async def on_invoice_paid(payment: Payment):
- # (avoid loops)
- if payment.extra.get("tag") == "scrubed":
- # already scrubbed
- return
-
- scrub_link = await get_scrub_by_wallet(payment.wallet_id)
-
- if not scrub_link:
- return
-
- from lnbits.core.views.api import api_lnurlscan
-
- # DECODE LNURLP OR LNADDRESS
- data = await api_lnurlscan(scrub_link.payoraddress)
-
- # I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267
- domain = urlparse(data["callback"]).netloc
- rounded_amount = floor(payment.amount / 1000) * 1000
-
- async with httpx.AsyncClient() as client:
- try:
- r = await client.get(
- data["callback"],
- params={"amount": rounded_amount},
- timeout=40,
- )
- if r.is_error:
- raise httpx.ConnectError("issue with scrub callback")
- except (httpx.ConnectError, httpx.RequestError):
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=f"Failed to connect to {domain}.",
- )
-
- params = json.loads(r.text)
- if params.get("status") == "ERROR":
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=f"{domain} said: '{params.get('reason', '')}'",
- )
-
- invoice = bolt11.decode(params["pr"])
-
- if invoice.amount_msat != rounded_amount:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=f"{domain} returned an invalid invoice. Expected {payment.amount} msat, got {invoice.amount_msat}.",
- )
-
- payment_hash = await pay_invoice(
- wallet_id=payment.wallet_id,
- payment_request=params["pr"],
- description=data["description"],
- extra={"tag": "scrubed"},
- )
-
- return {
- "payment_hash": payment_hash,
- # maintain backwards compatibility with API clients:
- "checking_id": payment_hash,
- }
diff --git a/lnbits/extensions/scrub/templates/scrub/_api_docs.html b/lnbits/extensions/scrub/templates/scrub/_api_docs.html
deleted file mode 100644
index ae3f44d8..00000000
--- a/lnbits/extensions/scrub/templates/scrub/_api_docs.html
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
-
-
- GET /scrub/api/v1/links
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<pay_link_object>, ...]
- Curl example
- curl -X GET {{ request.base_url }}scrub/api/v1/links?all_wallets=true
- -H "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- GET
- /scrub/api/v1/links/<scrub_id>
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {"id": <string>, "wallet": <string>, "description":
- <string>, "payoraddress": <string>}
- Curl example
- curl -X GET {{ request.base_url }}scrub/api/v1/links/<pay_id>
- -H "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- POST /scrub/api/v1/links
- Headers
- {"X-Api-Key": <admin_key>}
- Body (application/json)
- {"wallet": <string>, "description": <string>,
- "payoraddress": <string>}
-
- Returns 201 CREATED (application/json)
-
- {"id": <string>, "wallet": <string>, "description":
- <string>, "payoraddress": <string>}
- Curl example
- curl -X POST {{ request.base_url }}scrub/api/v1/links -d '{"wallet":
- <string>, "description": <string>, "payoraddress":
- <string>}' -H "Content-type: application/json" -H "X-Api-Key: {{
- user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- PUT
- /scrub/api/v1/links/<pay_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Body (application/json)
- {"wallet": <string>, "description": <string>,
- "payoraddress": <string>}
-
- Returns 200 OK (application/json)
-
- {"id": <string>, "wallet": <string>, "description":
- <string>, "payoraddress": <string>}
- Curl example
- curl -X PUT {{ request.base_url }}scrub/api/v1/links/<pay_id>
- -d '{"wallet": <string>, "description": <string>,
- "payoraddress": <string>}' -H "Content-type: application/json"
- -H "X-Api-Key: {{ user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- DELETE
- /scrub/api/v1/links/<pay_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Returns 204 NO CONTENT
-
- Curl example
- curl -X DELETE {{ request.base_url
- }}scrub/api/v1/links/<pay_id> -H "X-Api-Key: {{
- user.wallets[0].adminkey }}"
-
-
-
-
-
diff --git a/lnbits/extensions/scrub/templates/scrub/_lnurl.html b/lnbits/extensions/scrub/templates/scrub/_lnurl.html
deleted file mode 100644
index f2ba8661..00000000
--- a/lnbits/extensions/scrub/templates/scrub/_lnurl.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
- WARNING: LNURL must be used over https or TOR
- LNURL is a range of lightning-network standards that allow us to use
- lightning-network differently. An LNURL-pay is a link that wallets use
- to fetch an invoice from a server on-demand. The link or QR code is
- fixed, but each time it is read by a compatible wallet a new QR code is
- issued by the service. It can be used to activate machines without them
- having to maintain an electronic screen to generate and show invoices
- locally, or to sell any predefined good or service automatically.
-
-
- Exploring LNURL and finding use cases, is really helping inform
- lightning protocol development, rather than the protocol dictating how
- lightning-network should be engaged with.
-
- Check
- Awesome LNURL
- for further information.
-
-
-
diff --git a/lnbits/extensions/scrub/templates/scrub/index.html b/lnbits/extensions/scrub/templates/scrub/index.html
deleted file mode 100644
index a3756df3..00000000
--- a/lnbits/extensions/scrub/templates/scrub/index.html
+++ /dev/null
@@ -1,156 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New scrub link
-
-
-
-
-
-
-
- {% raw %}
-
-
- Wallet
- Description
- LNURLPay/Address
-
-
-
-
-
- {{ props.row.wallet }}
- {{ props.row.description }}
- {{ props.row.payoraddress }}
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} Scrub extension
-
- Automatically forward funds (Scrub) that get paid to the LNbits
- wallet, to an LNURLpay or Lightning Address.
-
- More info in Scrub's
- readme .
-
-
- Important: wallet will need a float to account for
- any fees, before being able to push a payment
-
-
-
-
-
- {% include "scrub/_api_docs.html" %}
-
- {% include "scrub/_lnurl.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update pay link
- Create pay link
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/scrub/views.py b/lnbits/extensions/scrub/views.py
deleted file mode 100644
index 48958013..00000000
--- a/lnbits/extensions/scrub/views.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from fastapi import Depends, 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 scrub_ext, scrub_renderer
-
-templates = Jinja2Templates(directory="templates")
-
-
-@scrub_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return scrub_renderer().TemplateResponse(
- "scrub/index.html", {"request": request, "user": user.dict()}
- )
diff --git a/lnbits/extensions/scrub/views_api.py b/lnbits/extensions/scrub/views_api.py
deleted file mode 100644
index eae0098d..00000000
--- a/lnbits/extensions/scrub/views_api.py
+++ /dev/null
@@ -1,107 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Query
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_user
-from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
-
-from . import scrub_ext
-from .crud import (
- create_scrub_link,
- delete_scrub_link,
- get_scrub_link,
- get_scrub_links,
- unique_scrubed_wallet,
- update_scrub_link,
-)
-from .models import CreateScrubLink
-
-
-@scrub_ext.get("/api/v1/links", status_code=HTTPStatus.OK)
-async def api_links(
- wallet: WalletTypeInfo = Depends(get_key_type),
- all_wallets: bool = Query(False),
-):
- wallet_ids = [wallet.wallet.id]
-
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- try:
- return [link.dict() for link in await get_scrub_links(wallet_ids)]
-
- except:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail="No SCRUB links made yet",
- )
-
-
-@scrub_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_retrieve(link_id, wallet: WalletTypeInfo = Depends(get_key_type)):
- link = await get_scrub_link(link_id)
-
- if not link:
- raise HTTPException(
- detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
- )
-
- return link
-
-
-@scrub_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
-@scrub_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_scrub_create_or_update(
- data: CreateScrubLink,
- link_id=None,
- wallet: WalletTypeInfo = Depends(require_admin_key),
-):
- if link_id:
- link = await get_scrub_link(link_id)
-
- if not link:
- raise HTTPException(
- detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
- )
-
- link = await update_scrub_link(**data.dict(), link_id=link_id)
- else:
- wallet_has_scrub = await unique_scrubed_wallet(wallet_id=data.wallet)
- if wallet_has_scrub > 0:
- raise HTTPException(
- detail="Wallet is already being Scrubbed",
- status_code=HTTPStatus.FORBIDDEN,
- )
- link = await create_scrub_link(data=data)
-
- return link
-
-
-@scrub_ext.delete("/api/v1/links/{link_id}")
-async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
- link = await get_scrub_link(link_id)
-
- if not link:
- raise HTTPException(
- detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
- )
-
- await delete_scrub_link(link_id)
- return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/streamalerts/README.md b/lnbits/extensions/streamalerts/README.md
deleted file mode 100644
index e19ff277..00000000
--- a/lnbits/extensions/streamalerts/README.md
+++ /dev/null
@@ -1,39 +0,0 @@
-Stream Alerts
-Integrate Bitcoin Donations into your livestream alerts
-The StreamAlerts extension allows you to integrate Bitcoin Lightning (and on-chain) paymnents in to your existing Streamlabs alerts!
-
-
-
-How to set it up
-
-At the moment, the only service that has an open API to work with is Streamlabs, so this setup requires linking your Twitch/YouTube/Facebook account to Streamlabs.
-
-1. Log into [Streamlabs](https://streamlabs.com/login?r=https://streamlabs.com/dashboard).
-1. Navigate to the API settings page to register an App:
-
-
-
-1. Fill out the form with anything it will accept as valid. Most fields can be gibberish, as the application is not supposed to ever move past the "testing" stage and is for your personal use only.
-In the "Whitelist Users" field, input the username of a Twitch account you control. While this feature is *technically* limited to Twitch, you can use the alerts overlay for donations on YouTube and Facebook as well.
-For now, simply set the "Redirect URI" to `http://localhost`, you will change this soon.
-Then, hit create:
-
-1. In LNbits, enable the Stream Alerts extension and optionally the SatsPayServer (to observe donations directly) and Onchain Wallet (watch-only) (to accept on-chain donations) extenions:
-
-1. Create a "NEW SERVICE" using the button. Fill in all the information (you get your Client ID and Secret from the Streamlabs App page):
-
-
-1. Right-click and copy the "Redirect URI for Streamlabs" link (you might have to refresh the page for the text to turn into a link) and input it into the "Redirect URI" field for your Streamelements App, and hit "Save Settings":
-
-
-1. You can now authenticate your app on LNbits by clicking on this button and following the instructions. Make sure to log in with the Twitch account you entered in the "Whitelist Users" field:
-
-
-If everything worked correctly, you should now be redirected back to LNbits. When scrolling all the way right, you should now see that the service has been authenticated:
-
-You can now share the link to your donations page, which you can get here:
-
-
-Of course, this has to be available publicly on the internet (or, depending on your viewers' technical ability, over Tor).
-When your viewers donate to you, these donations will now show up in your Streamlabs donation feed, as well as your alerts overlay (if it is configured to include donations).
-CONGRATS! Let the sats flow!
diff --git a/lnbits/extensions/streamalerts/__init__.py b/lnbits/extensions/streamalerts/__init__.py
deleted file mode 100644
index 603547d1..00000000
--- a/lnbits/extensions/streamalerts/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from fastapi import APIRouter
-from fastapi.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_streamalerts")
-
-streamalerts_ext: APIRouter = APIRouter(prefix="/streamalerts", tags=["streamalerts"])
-
-streamalerts_static_files = [
- {
- "path": "/streamalerts/static",
- "app": StaticFiles(directory="lnbits/extensions/streamalerts/static"),
- "name": "streamalerts_static",
- }
-]
-
-
-def streamalerts_renderer():
- return template_renderer(["lnbits/extensions/streamalerts/templates"])
-
-
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/streamalerts/config.json b/lnbits/extensions/streamalerts/config.json
deleted file mode 100644
index 24451b24..00000000
--- a/lnbits/extensions/streamalerts/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Stream Alerts",
- "short_description": "Bitcoin donations in stream alerts",
- "tile": "/streamalerts/static/image/streamalerts.png",
- "contributors": ["Fittiboy"]
-}
diff --git a/lnbits/extensions/streamalerts/crud.py b/lnbits/extensions/streamalerts/crud.py
deleted file mode 100644
index 55618a1a..00000000
--- a/lnbits/extensions/streamalerts/crud.py
+++ /dev/null
@@ -1,284 +0,0 @@
-from typing import Optional
-
-import httpx
-
-from lnbits.core.crud import get_wallet
-from lnbits.db import SQLITE
-from lnbits.helpers import urlsafe_short_hash
-
-# todo: use the API, not direct import
-from ..satspay.crud import delete_charge # type: ignore
-from . import db
-from .models import CreateService, Donation, Service
-
-
-async def get_service_redirect_uri(request, service_id):
- """Return the service's redirect URI, to be given to the third party API"""
- uri_base = request.url.scheme + "://"
- uri_base += request.headers["Host"] + "/streamalerts/api/v1"
- redirect_uri = uri_base + f"/authenticate/{service_id}"
- return redirect_uri
-
-
-async def get_charge_details(service_id):
- """Return the default details for a satspay charge
-
- These might be different depending for services implemented in the future.
- """
- service = await get_service(service_id)
- assert service, f"Could not fetch service: {service_id}"
-
- wallet_id = service.wallet
- wallet = await get_wallet(wallet_id)
- assert wallet, f"Could not fetch wallet: {wallet_id}"
-
- user = wallet.user
- return {
- "time": 1440,
- "user": user,
- "lnbitswallet": wallet_id,
- "onchainwallet": service.onchain,
- }
-
-
-async def create_donation(
- id: str,
- wallet: str,
- cur_code: str,
- sats: int,
- amount: float,
- service: int,
- name: str = "Anonymous",
- message: str = "",
- posted: bool = False,
-) -> Donation:
- """Create a new Donation"""
- await db.execute(
- """
- INSERT INTO streamalerts.Donations (
- id,
- wallet,
- name,
- message,
- cur_code,
- sats,
- amount,
- service,
- posted
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (id, wallet, name, message, cur_code, sats, amount, service, posted),
- )
-
- donation = await get_donation(id)
- assert donation, "Newly created donation couldn't be retrieved"
- return donation
-
-
-async def post_donation(donation_id: str) -> dict:
- """Post donations to their respective third party APIs
-
- If the donation has already been posted, it will not be posted again.
- """
- donation = await get_donation(donation_id)
- if not donation:
- return {"message": "Donation not found!"}
- if donation.posted:
- return {"message": "Donation has already been posted!"}
-
- service = await get_service(donation.service)
- assert service, "Couldn't fetch service to donate to"
-
- if service.servicename == "Streamlabs":
- url = "https://streamlabs.com/api/v1.0/donations"
- data = {
- "name": donation.name[:25],
- "message": donation.message[:255],
- "identifier": "LNbits",
- "amount": donation.amount,
- "currency": donation.cur_code.upper(),
- "access_token": service.token,
- }
- async with httpx.AsyncClient() as client:
- response = await client.post(url, data=data)
- elif service.servicename == "StreamElements":
- return {"message": "StreamElements not yet supported!"}
- else:
- return {"message": "Unsopported servicename"}
- await db.execute(
- "UPDATE streamalerts.Donations SET posted = 1 WHERE id = ?", (donation_id,)
- )
- return response.json()
-
-
-async def create_service(data: CreateService) -> Service:
- """Create a new Service"""
-
- returning = "" if db.type == SQLITE else "RETURNING ID"
- method = db.execute if db.type == SQLITE else db.fetchone
-
- result = await (method)(
- f"""
- INSERT INTO streamalerts.Services (
- twitchuser,
- client_id,
- client_secret,
- wallet,
- servicename,
- authenticated,
- state,
- onchain
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- {returning}
- """,
- (
- data.twitchuser,
- data.client_id,
- data.client_secret,
- data.wallet,
- data.servicename,
- False,
- urlsafe_short_hash(),
- data.onchain,
- ),
- )
- if db.type == SQLITE:
- service_id = result._result_proxy.lastrowid
- else:
- service_id = result[0] # type: ignore
-
- service = await get_service(service_id)
- assert service, f"Could not fetch service: {service_id}"
- return service
-
-
-async def get_service(
- service_id: int, by_state: Optional[str] = None
-) -> Optional[Service]:
- """Return a service either by ID or, available, by state
-
- Each Service's donation page is reached through its "state" hash
- instead of the ID, preventing accidental payments to the wrong
- streamer via typos like 2 -> 3.
- """
- if by_state:
- row = await db.fetchone(
- "SELECT * FROM streamalerts.Services WHERE state = ?", (by_state,)
- )
- else:
- row = await db.fetchone(
- "SELECT * FROM streamalerts.Services WHERE id = ?", (service_id,)
- )
- return Service.from_row(row) if row else None
-
-
-async def get_services(wallet_id: str) -> Optional[list]:
- """Return all services belonging assigned to the wallet_id"""
- rows = await db.fetchall(
- "SELECT * FROM streamalerts.Services WHERE wallet = ?", (wallet_id,)
- )
- return [Service.from_row(row) for row in rows] if rows else None
-
-
-async def authenticate_service(service_id, code, redirect_uri):
- """Use authentication code from third party API to retreive access token"""
- # The API token is passed in the querystring as 'code'
- service = await get_service(service_id)
- assert service, f"Could not fetch service: {service_id}"
- wallet = await get_wallet(service.wallet)
- assert wallet, f"Could not fetch wallet: {service.wallet}"
- user = wallet.user
- url = "https://streamlabs.com/api/v1.0/token"
- data = {
- "grant_type": "authorization_code",
- "code": code,
- "client_id": service.client_id,
- "client_secret": service.client_secret,
- "redirect_uri": redirect_uri,
- }
- async with httpx.AsyncClient() as client:
- response = (await client.post(url, data=data)).json()
- token = response["access_token"]
- success = await service_add_token(service_id, token)
- return f"/streamalerts/?usr={user}", success
-
-
-async def service_add_token(service_id, token):
- """Add access token to its corresponding Service
-
- This also sets authenticated = 1 to make sure the token
- is not overwritten.
- Tokens for Streamlabs never need to be refreshed.
- """
- service = await get_service(service_id)
- assert service, f"Could not fetch service: {service_id}"
- if service.authenticated:
- return False
-
- await db.execute(
- "UPDATE streamalerts.Services SET authenticated = 1, token = ? where id = ?",
- (token, service_id),
- )
- return True
-
-
-async def delete_service(service_id: int) -> None:
- """Delete a Service and all corresponding Donations"""
- await db.execute("DELETE FROM streamalerts.Services WHERE id = ?", (service_id,))
- rows = await db.fetchall(
- "SELECT * FROM streamalerts.Donations WHERE service = ?", (service_id,)
- )
- for row in rows:
- await delete_donation(row["id"])
-
-
-async def get_donation(donation_id: str) -> Optional[Donation]:
- """Return a Donation"""
- row = await db.fetchone(
- "SELECT * FROM streamalerts.Donations WHERE id = ?", (donation_id,)
- )
- return Donation.from_row(row) if row else None
-
-
-async def get_donations(wallet_id: str) -> Optional[list]:
- """Return all streamalerts.Donations assigned to wallet_id"""
- rows = await db.fetchall(
- "SELECT * FROM streamalerts.Donations WHERE wallet = ?", (wallet_id,)
- )
- return [Donation.from_row(row) for row in rows] if rows else None
-
-
-async def delete_donation(donation_id: str) -> None:
- """Delete a Donation and its corresponding statspay charge"""
- await db.execute("DELETE FROM streamalerts.Donations WHERE id = ?", (donation_id,))
- await delete_charge(donation_id)
-
-
-async def update_donation(donation_id: str, **kwargs) -> Donation:
- """Update a Donation"""
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE streamalerts.Donations SET {q} WHERE id = ?",
- (*kwargs.values(), donation_id),
- )
- row = await db.fetchone(
- "SELECT * FROM streamalerts.Donations WHERE id = ?", (donation_id,)
- )
- assert row, "Newly updated donation couldn't be retrieved"
- return Donation(**row)
-
-
-async def update_service(service_id: str, **kwargs) -> Service:
- """Update a service"""
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE streamalerts.Services SET {q} WHERE id = ?",
- (*kwargs.values(), service_id),
- )
- row = await db.fetchone(
- "SELECT * FROM streamalerts.Services WHERE id = ?", (service_id,)
- )
- assert row, "Newly updated service couldn't be retrieved"
- return Service(**row)
diff --git a/lnbits/extensions/streamalerts/migrations.py b/lnbits/extensions/streamalerts/migrations.py
deleted file mode 100644
index 7d50e8f1..00000000
--- a/lnbits/extensions/streamalerts/migrations.py
+++ /dev/null
@@ -1,35 +0,0 @@
-async def m001_initial(db):
-
- await db.execute(
- f"""
- CREATE TABLE IF NOT EXISTS streamalerts.Services (
- id {db.serial_primary_key},
- state TEXT NOT NULL,
- twitchuser TEXT NOT NULL,
- client_id TEXT NOT NULL,
- client_secret TEXT NOT NULL,
- wallet TEXT NOT NULL,
- onchain TEXT,
- servicename TEXT NOT NULL,
- authenticated BOOLEAN NOT NULL,
- token TEXT
- );
- """
- )
-
- await db.execute(
- f"""
- CREATE TABLE IF NOT EXISTS streamalerts.Donations (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- name TEXT NOT NULL,
- message TEXT NOT NULL,
- cur_code TEXT NOT NULL,
- sats {db.big_int} NOT NULL,
- amount FLOAT NOT NULL,
- service INTEGER NOT NULL,
- posted BOOLEAN NOT NULL,
- FOREIGN KEY(service) REFERENCES {db.references_schema}Services(id)
- );
- """
- )
diff --git a/lnbits/extensions/streamalerts/models.py b/lnbits/extensions/streamalerts/models.py
deleted file mode 100644
index ae0ffab5..00000000
--- a/lnbits/extensions/streamalerts/models.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from sqlite3 import Row
-from typing import Optional
-
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class CreateService(BaseModel):
- twitchuser: str = Query(...)
- client_id: str = Query(...)
- client_secret: str = Query(...)
- wallet: str = Query(...)
- servicename: str = Query(...)
- onchain: str = Query(None)
-
-
-class CreateDonation(BaseModel):
- name: str = Query("Anonymous")
- sats: int = Query(..., ge=1)
- service: int = Query(...)
- message: str = Query("")
-
-
-class ValidateDonation(BaseModel):
- id: str = Query(...)
-
-
-class Donation(BaseModel):
- """A Donation simply contains all the necessary information about a
- user's donation to a streamer
- """
-
- id: str # This ID always corresponds to a satspay charge ID
- wallet: str
- name: str # Name of the donor
- message: str # Donation message
- cur_code: str # Three letter currency code accepted by Streamlabs
- sats: int
- amount: float # The donation amount after fiat conversion
- service: int # The ID of the corresponding Service
- posted: bool # Whether the donation has already been posted to a Service
-
- @classmethod
- def from_row(cls, row: Row) -> "Donation":
- return cls(**dict(row))
-
-
-class Service(BaseModel):
- """A Service represents an integration with a third-party API
-
- Currently, Streamlabs is the only supported Service.
- """
-
- id: int
- state: str # A random hash used during authentication
- twitchuser: str # The Twitch streamer's username
- client_id: str # Third party service Client ID
- client_secret: str # Secret corresponding to the Client ID
- wallet: str
- onchain: Optional[str]
- servicename: str # Currently, this will just always be "Streamlabs"
- authenticated: bool # Whether a token (see below) has been acquired yet
- token: Optional[str] # The token with which to authenticate requests
-
- @classmethod
- def from_row(cls, row: Row) -> "Service":
- return cls(**dict(row))
diff --git a/lnbits/extensions/streamalerts/static/image/streamalerts.png b/lnbits/extensions/streamalerts/static/image/streamalerts.png
deleted file mode 100644
index 63724ec3..00000000
Binary files a/lnbits/extensions/streamalerts/static/image/streamalerts.png and /dev/null differ
diff --git a/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html b/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html
deleted file mode 100644
index 132a0e1d..00000000
--- a/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- Stream Alerts: Integrate Bitcoin into your stream alerts!
-
-
- Accept Bitcoin donations on Twitch, and integrate them into your alerts.
- Present your viewers with a simple donation page, and add those donations
- to Streamlabs to play alerts on your stream!
- For detailed setup instructions, check out
-
- this guide!
-
- Created by,
- Fitti
-
-
-
-
diff --git a/lnbits/extensions/streamalerts/templates/streamalerts/display.html b/lnbits/extensions/streamalerts/templates/streamalerts/display.html
deleted file mode 100644
index 8a0b2d59..00000000
--- a/lnbits/extensions/streamalerts/templates/streamalerts/display.html
+++ /dev/null
@@ -1,97 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
- Donate Bitcoin to {{ twitchuser }}
-
-
-
-
-
-
- Submit
-
-
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/streamalerts/templates/streamalerts/index.html b/lnbits/extensions/streamalerts/templates/streamalerts/index.html
deleted file mode 100644
index 1639afe5..00000000
--- a/lnbits/extensions/streamalerts/templates/streamalerts/index.html
+++ /dev/null
@@ -1,505 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New Service
-
-
-
-
-
-
-
-
Services
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
- Redirect URI for Streamlabs
-
-
- {{ col.value }}
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
Donations
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
- {{ col.label }}
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} Stream Alerts extension
-
-
-
-
- {% include "streamalerts/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Onchain Wallet (watch-only) extension MUST be activated and
- have a wallet
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update Service
-
- Create Service
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/streamalerts/views.py b/lnbits/extensions/streamalerts/views.py
deleted file mode 100644
index ac63e9c5..00000000
--- a/lnbits/extensions/streamalerts/views.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends
-from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
-from starlette.responses import HTMLResponse
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import streamalerts_ext, streamalerts_renderer
-from .crud import get_service
-
-templates = Jinja2Templates(directory="templates")
-
-
-@streamalerts_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- """Return the extension's settings page"""
- return streamalerts_renderer().TemplateResponse(
- "streamalerts/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@streamalerts_ext.get("/{state}")
-async def donation(state, request: Request):
- """Return the donation form for the Service corresponding to state"""
- service = await get_service(0, by_state=state)
- if not service:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Service does not exist."
- )
- return streamalerts_renderer().TemplateResponse(
- "streamalerts/display.html",
- {"request": request, "twitchuser": service.twitchuser, "service": service.id},
- )
diff --git a/lnbits/extensions/streamalerts/views_api.py b/lnbits/extensions/streamalerts/views_api.py
deleted file mode 100644
index 7bf952c7..00000000
--- a/lnbits/extensions/streamalerts/views_api.py
+++ /dev/null
@@ -1,269 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Query
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
-from starlette.responses import RedirectResponse
-
-from lnbits.core.crud import get_user
-from lnbits.decorators import WalletTypeInfo, get_key_type
-
-# todo: use the API, not direct import
-from lnbits.extensions.satspay.models import CreateCharge # type: ignore
-from lnbits.utils.exchange_rates import btc_price
-
-# todo: use the API, not direct import
-from ..satspay.crud import create_charge, get_charge # type: ignore
-from . import streamalerts_ext
-from .crud import (
- authenticate_service,
- create_donation,
- create_service,
- delete_donation,
- delete_service,
- get_charge_details,
- get_donation,
- get_donations,
- get_service,
- get_service_redirect_uri,
- get_services,
- post_donation,
- update_donation,
- update_service,
-)
-from .models import CreateDonation, CreateService, ValidateDonation
-
-
-@streamalerts_ext.post("/api/v1/services")
-async def api_create_service(
- data: CreateService, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- """Create a service, which holds data about how/where to post donations"""
- try:
- service = await create_service(data=data)
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
-
- return service.dict()
-
-
-@streamalerts_ext.get("/api/v1/getaccess/{service_id}")
-async def api_get_access(service_id, request: Request):
- """Redirect to Streamlabs' Approve/Decline page for API access for Service
- with service_id
- """
- service = await get_service(service_id)
- if service:
- redirect_uri = await get_service_redirect_uri(request, service_id)
- params = {
- "response_type": "code",
- "client_id": service.client_id,
- "redirect_uri": redirect_uri,
- "scope": "donations.create",
- "state": service.state,
- }
- endpoint_url = "https://streamlabs.com/api/v1.0/authorize/?"
- querystring = "&".join([f"{key}={value}" for key, value in params.items()])
- redirect_url = endpoint_url + querystring
- return RedirectResponse(redirect_url)
- else:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="Service does not exist!"
- )
-
-
-@streamalerts_ext.get("/api/v1/authenticate/{service_id}")
-async def api_authenticate_service(
- service_id, request: Request, code: str = Query(...), state: str = Query(...)
-):
- """Endpoint visited via redirect during third party API authentication
-
- If successful, an API access token will be added to the service, and
- the user will be redirected to index.html.
- """
-
- service = await get_service(service_id)
- assert service
-
- if service.state != state:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="State doesn't match!"
- )
-
- redirect_uri = request.url.scheme + "://" + request.headers["Host"]
- redirect_uri += f"/streamalerts/api/v1/authenticate/{service_id}"
- url, success = await authenticate_service(service_id, code, redirect_uri)
- if success:
- return RedirectResponse(url)
- else:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="Service already authenticated!"
- )
-
-
-@streamalerts_ext.post("/api/v1/donations")
-async def api_create_donation(data: CreateDonation, request: Request):
- """Take data from donation form and return satspay charge"""
- # Currency is hardcoded while frotnend is limited
- cur_code = "USD"
- sats = data.sats
- message = data.message
- # Fiat amount is calculated here while frontend is limited
- price = await btc_price(cur_code)
- amount = sats * (10 ** (-8)) * price
- webhook_base = request.url.scheme + "://" + request.headers["Host"]
- service_id = data.service
- service = await get_service(service_id)
- assert service
- charge_details = await get_charge_details(service.id)
- name = data.name if data.name else "Anonymous"
-
- description = f"{sats} sats donation from {name} to {service.twitchuser}"
- create_charge_data = CreateCharge(
- amount=sats,
- completelink=f"https://twitch.tv/{service.twitchuser}",
- completelinktext="Back to Stream!",
- webhook=webhook_base + "/streamalerts/api/v1/postdonation",
- description=description,
- **charge_details,
- )
- charge = await create_charge(user=charge_details["user"], data=create_charge_data)
- await create_donation(
- id=charge.id,
- wallet=service.wallet,
- message=message,
- name=name,
- cur_code=cur_code,
- sats=data.sats,
- amount=amount,
- service=data.service,
- )
- return {"redirect_url": f"/satspay/{charge.id}"}
-
-
-@streamalerts_ext.post("/api/v1/postdonation")
-async def api_post_donation(request: Request, data: ValidateDonation):
- """Post a paid donation to Stremalabs/StreamElements.
- This endpoint acts as a webhook for the SatsPayServer extension."""
-
- donation_id = data.id
- charge = await get_charge(donation_id)
- if charge and charge.paid:
- return await post_donation(donation_id)
- else:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="Not a paid charge!"
- )
-
-
-@streamalerts_ext.get("/api/v1/services")
-async def api_get_services(g: WalletTypeInfo = Depends(get_key_type)):
- """Return list of all services assigned to wallet with given invoice key"""
- user = await get_user(g.wallet.user)
- wallet_ids = user.wallet_ids if user else []
- services = []
- for wallet_id in wallet_ids:
- new_services = await get_services(wallet_id)
- services += new_services if new_services else []
- return [service.dict() for service in services] if services else []
-
-
-@streamalerts_ext.get("/api/v1/donations")
-async def api_get_donations(g: WalletTypeInfo = Depends(get_key_type)):
- """Return list of all donations assigned to wallet with given invoice
- key
- """
- user = await get_user(g.wallet.user)
- wallet_ids = user.wallet_ids if user else []
- donations = []
- for wallet_id in wallet_ids:
- new_donations = await get_donations(wallet_id)
- donations += new_donations if new_donations else []
- return [donation.dict() for donation in donations] if donations else []
-
-
-@streamalerts_ext.put("/api/v1/donations/{donation_id}")
-async def api_update_donation(
- data: CreateDonation, donation_id=None, g: WalletTypeInfo = Depends(get_key_type)
-):
- """Update a donation with the data given in the request"""
- if donation_id:
- donation = await get_donation(donation_id)
-
- if not donation:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Donation does not exist."
- )
-
- if donation.wallet != g.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your donation."
- )
-
- donation = await update_donation(donation_id, **data.dict())
- else:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="No donation ID specified"
- )
-
- return donation.dict()
-
-
-@streamalerts_ext.put("/api/v1/services/{service_id}")
-async def api_update_service(
- data: CreateService, service_id=None, g: WalletTypeInfo = Depends(get_key_type)
-):
- """Update a service with the data given in the request"""
- if service_id:
- service = await get_service(service_id)
-
- if not service:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Service does not exist."
- )
-
- if service.wallet != g.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your service."
- )
-
- service = await update_service(service_id, **data.dict())
- else:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="No service ID specified"
- )
- return service.dict()
-
-
-@streamalerts_ext.delete("/api/v1/donations/{donation_id}")
-async def api_delete_donation(donation_id, g: WalletTypeInfo = Depends(get_key_type)):
- """Delete the donation with the given donation_id"""
- donation = await get_donation(donation_id)
- if not donation:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="No donation with this ID!"
- )
- if donation.wallet != g.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Not authorized to delete this donation!",
- )
- await delete_donation(donation_id)
- return "", HTTPStatus.NO_CONTENT
-
-
-@streamalerts_ext.delete("/api/v1/services/{service_id}")
-async def api_delete_service(service_id, g: WalletTypeInfo = Depends(get_key_type)):
- """Delete the service with the given service_id"""
- service = await get_service(service_id)
- if not service:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="No service with this ID!"
- )
- if service.wallet != g.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Not authorized to delete this service!",
- )
- await delete_service(service_id)
- return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md
deleted file mode 100644
index 3797c761..00000000
--- a/lnbits/extensions/subdomains/README.md
+++ /dev/null
@@ -1,54 +0,0 @@
-Subdomains Extension
-
-So the goal of the extension is to allow the owner of a domain to sell subdomains to anyone who is willing to pay some money for it.
-
-[](https://youtu.be/O1X0fy3uNpw 'video tutorial subdomains')
-
-## Requirements
-
-- Free Cloudflare account
-- Cloudflare as a DNS server provider
-- Cloudflare TOKEN and Cloudflare zone-ID where the domain is parked
-
-## Usage
-
-1. Register at Cloudflare and setup your domain with them. (Just follow instructions they provide...)
-2. Change DNS server at your domain registrar to point to Cloudflare's
-3. Get Cloudflare zone-ID for your domain
-
-4. Get Cloudflare API TOKEN
-
-
-5. Open the LNbits subdomains extension and register your domain
-6. Click on the button in the table to open the public form that was generated for your domain
-
- - Extension also supports webhooks so you can get notified when someone buys a new subdomain\
-
-
-## API Endpoints
-
-- **Domains**
- - GET /api/v1/domains
- - POST /api/v1/domains
- - PUT /api/v1/domains/
- - DELETE /api/v1/domains/
-- **Subdomains**
- - GET /api/v1/subdomains
- - POST /api/v1/subdomains/
- - GET /api/v1/subdomains/
- - DELETE /api/v1/subdomains/
-
-### Cloudflare
-
-- Cloudflare offers programmatic subdomain registration... (create new A record)
-- you can keep your existing domain's registrar, you just have to transfer dns records to the cloudflare (free service)
-- more information:
- - https://api.cloudflare.com/#getting-started-requests
- - API endpoints needed for our project:
- - https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
- - https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
- - https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record
- - https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
-- api can be used by providing authorization token OR authorization key
- - check API Tokens and API Keys : https://api.cloudflare.com/#getting-started-requests
-- Cloudflare API postman collection: https://support.cloudflare.com/hc/en-us/articles/115002323852-Using-Cloudflare-API-with-Postman-Collections
diff --git a/lnbits/extensions/subdomains/__init__.py b/lnbits/extensions/subdomains/__init__.py
deleted file mode 100644
index 7434555d..00000000
--- a/lnbits/extensions/subdomains/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-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_subdomains")
-
-subdomains_ext: APIRouter = APIRouter(prefix="/subdomains", tags=["subdomains"])
-
-subdomains_static_files = [
- {
- "path": "/subdomains/static",
- "app": StaticFiles(directory="lnbits/extensions/subdomains/static"),
- "name": "subdomains_static",
- }
-]
-
-
-def subdomains_renderer():
- return template_renderer(["lnbits/extensions/subdomains/templates"])
-
-
-from .tasks import wait_for_paid_invoices
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
-
-
-def subdomains_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/subdomains/cloudflare.py b/lnbits/extensions/subdomains/cloudflare.py
deleted file mode 100644
index 3d3b9bde..00000000
--- a/lnbits/extensions/subdomains/cloudflare.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import httpx
-
-from .models import Domains
-
-
-async def cloudflare_create_subdomain(
- domain: Domains, subdomain: str, record_type: str, ip: str
-):
- # Call to cloudflare sort of a dry-run - if success delete the domain and wait for payment
- ### SEND REQUEST TO CLOUDFLARE
- url = (
- "https://api.cloudflare.com/client/v4/zones/"
- + domain.cf_zone_id
- + "/dns_records"
- )
- header = {
- "Authorization": "Bearer " + domain.cf_token,
- "Content-Type": "application/json",
- }
- aRecord = subdomain + "." + domain.domain
- async with httpx.AsyncClient() as client:
- r = await client.post(
- url,
- headers=header,
- json={
- "type": record_type,
- "name": aRecord,
- "content": ip,
- "ttl": 0,
- "proxied": False,
- },
- timeout=40,
- )
- r.raise_for_status()
- return r.json()
-
-
-async def cloudflare_deletesubdomain(domain: Domains, domain_id: str):
- url = (
- "https://api.cloudflare.com/client/v4/zones/"
- + domain.cf_zone_id
- + "/dns_records"
- )
- header = {
- "Authorization": "Bearer " + domain.cf_token,
- "Content-Type": "application/json",
- }
- async with httpx.AsyncClient() as client:
- await client.delete(url + "/" + domain_id, headers=header, timeout=40)
diff --git a/lnbits/extensions/subdomains/config.json b/lnbits/extensions/subdomains/config.json
deleted file mode 100644
index cec2ec64..00000000
--- a/lnbits/extensions/subdomains/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Subdomains",
- "short_description": "Sell subdomains of your domain",
- "tile": "/subdomains/static/image/subdomains.png",
- "contributors": ["grmkris"]
-}
diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py
deleted file mode 100644
index b3476ed9..00000000
--- a/lnbits/extensions/subdomains/crud.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from typing import List, Optional, Union
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import CreateDomain, CreateSubdomain, Domains, Subdomains
-
-
-async def create_subdomain(payment_hash, wallet, data: CreateSubdomain) -> Subdomains:
- await db.execute(
- """
- INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- payment_hash,
- data.domain,
- data.email,
- data.subdomain,
- data.ip,
- wallet,
- data.sats,
- data.duration,
- False,
- data.record_type,
- ),
- )
-
- new_subdomain = await get_subdomain(payment_hash)
- assert new_subdomain, "Newly created subdomain couldn't be retrieved"
- return new_subdomain
-
-
-async def set_subdomain_paid(payment_hash: str) -> Subdomains:
- row = await db.fetchone(
- "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
- (payment_hash,),
- )
- if row[8] is False:
- await db.execute(
- """
- UPDATE subdomains.subdomain
- SET paid = true
- WHERE id = ?
- """,
- (payment_hash,),
- )
-
- domaindata = await get_domain(row[1])
- assert domaindata, "Couldn't get domain from paid subdomain"
-
- amount = domaindata.amountmade + row[8]
- await db.execute(
- """
- UPDATE subdomains.domain
- SET amountmade = ?
- WHERE id = ?
- """,
- (amount, row[1]),
- )
-
- new_subdomain = await get_subdomain(payment_hash)
- assert new_subdomain, "Newly paid subdomain couldn't be retrieved"
- return new_subdomain
-
-
-async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
- row = await db.fetchone(
- "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
- (subdomain_id,),
- )
- return Subdomains(**row) if row else None
-
-
-async def get_subdomainBySubdomain(subdomain: str) -> Optional[Subdomains]:
- row = await db.fetchone(
- "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
- (subdomain,),
- )
- return Subdomains(**row) if row else None
-
-
-async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})",
- (*wallet_ids,),
- )
-
- return [Subdomains(**row) for row in rows]
-
-
-async def delete_subdomain(subdomain_id: str) -> None:
- await db.execute("DELETE FROM subdomains.subdomain WHERE id = ?", (subdomain_id,))
-
-
-# Domains
-
-
-async def create_domain(data: CreateDomain) -> Domains:
- domain_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO subdomains.domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- domain_id,
- data.wallet,
- data.domain,
- data.webhook,
- data.cf_token,
- data.cf_zone_id,
- data.description,
- data.cost,
- 0,
- data.allowed_record_types,
- ),
- )
-
- new_domain = await get_domain(domain_id)
- assert new_domain, "Newly created domain couldn't be retrieved"
- return new_domain
-
-
-async def update_domain(domain_id: str, **kwargs) -> Domains:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE subdomains.domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id)
- )
- row = await db.fetchone(
- "SELECT * FROM subdomains.domain WHERE id = ?", (domain_id,)
- )
- assert row, "Newly updated domain couldn't be retrieved"
- return Domains(**row)
-
-
-async def get_domain(domain_id: str) -> Optional[Domains]:
- row = await db.fetchone(
- "SELECT * FROM subdomains.domain WHERE id = ?", (domain_id,)
- )
- return Domains(**row) if row else None
-
-
-async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM subdomains.domain WHERE wallet IN ({q})", (*wallet_ids,)
- )
-
- return [Domains(**row) for row in rows]
-
-
-async def delete_domain(domain_id: str) -> None:
- await db.execute("DELETE FROM subdomains.domain WHERE id = ?", (domain_id,))
diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py
deleted file mode 100644
index 292d1f18..00000000
--- a/lnbits/extensions/subdomains/migrations.py
+++ /dev/null
@@ -1,41 +0,0 @@
-async def m001_initial(db):
-
- await db.execute(
- """
- CREATE TABLE subdomains.domain (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- domain TEXT NOT NULL,
- webhook TEXT,
- cf_token TEXT NOT NULL,
- cf_zone_id TEXT NOT NULL,
- description TEXT NOT NULL,
- cost INTEGER NOT NULL,
- amountmade INTEGER NOT NULL,
- allowed_record_types TEXT NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
-
- await db.execute(
- """
- CREATE TABLE subdomains.subdomain (
- id TEXT PRIMARY KEY,
- domain TEXT NOT NULL,
- email TEXT NOT NULL,
- subdomain TEXT NOT NULL,
- ip TEXT NOT NULL,
- wallet TEXT NOT NULL,
- sats INTEGER NOT NULL,
- duration INTEGER NOT NULL,
- paid BOOLEAN NOT NULL,
- record_type TEXT NOT NULL,
- time TIMESTAMP NOT NULL DEFAULT """
- + db.timestamp_now
- + """
- );
- """
- )
diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py
deleted file mode 100644
index 552c37c7..00000000
--- a/lnbits/extensions/subdomains/models.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class CreateDomain(BaseModel):
- wallet: str = Query(...)
- domain: str = Query(...)
- cf_token: str = Query(...)
- cf_zone_id: str = Query(...)
- webhook: str = Query("")
- description: str = Query(..., min_length=0)
- cost: int = Query(..., ge=0)
- allowed_record_types: str = Query(...)
-
-
-class CreateSubdomain(BaseModel):
- domain: str = Query(...)
- subdomain: str = Query(...)
- email: str = Query(...)
- ip: str = Query(...)
- sats: int = Query(..., ge=0)
- duration: int = Query(...)
- record_type: str = Query(...)
-
-
-class Domains(BaseModel):
- id: str
- wallet: str
- domain: str
- cf_token: str
- cf_zone_id: str
- webhook: str
- description: str
- cost: int
- amountmade: int
- time: int
- allowed_record_types: str
-
-
-class Subdomains(BaseModel):
- id: str
- wallet: str
- domain: str
- domain_name: str
- subdomain: str
- email: str
- ip: str
- sats: int
- duration: int
- paid: bool
- time: int
- record_type: str
diff --git a/lnbits/extensions/subdomains/static/image/subdomains.png b/lnbits/extensions/subdomains/static/image/subdomains.png
deleted file mode 100644
index c552cb7b..00000000
Binary files a/lnbits/extensions/subdomains/static/image/subdomains.png and /dev/null differ
diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py
deleted file mode 100644
index 42b7885a..00000000
--- a/lnbits/extensions/subdomains/tasks.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import asyncio
-
-import httpx
-from loguru import logger
-
-from lnbits.core.models import Payment
-from lnbits.helpers import get_current_extension_name
-from lnbits.tasks import register_invoice_listener
-
-from .cloudflare import cloudflare_create_subdomain
-from .crud import get_domain, set_subdomain_paid
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue, get_current_extension_name())
-
- 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") != "lnsubdomain":
- # not an lnsubdomain invoice
- return
-
- await payment.set_pending(False)
- subdomain = await set_subdomain_paid(payment_hash=payment.payment_hash)
- domain = await get_domain(subdomain.domain)
-
- ### Create subdomain
- try:
- cf_response = await cloudflare_create_subdomain(
- domain=domain, # type: ignore
- subdomain=subdomain.subdomain,
- record_type=subdomain.record_type,
- ip=subdomain.ip,
- )
- except Exception as exc:
- logger.error(exc)
- logger.error("could not create subdomain on cloudflare")
- return
-
- ### Use webhook to notify about cloudflare registration
- if domain and domain.webhook:
- async with httpx.AsyncClient() as client:
- try:
- r = await client.post(
- domain.webhook,
- json={
- "domain": subdomain.domain_name,
- "subdomain": subdomain.subdomain,
- "record_type": subdomain.record_type,
- "email": subdomain.email,
- "ip": subdomain.ip,
- "cost:": str(subdomain.sats) + " sats",
- "duration": str(subdomain.duration) + " days",
- "cf_response": cf_response,
- },
- timeout=40,
- )
- assert r
- except AssertionError:
- pass
diff --git a/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html b/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html
deleted file mode 100644
index 035d67a6..00000000
--- a/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
- lnSubdomains: Get paid sats to sell your subdomains
-
-
- Charge people for using your subdomain name...
-
- More details
-
-
- Created by,
- Kris
-
-
-
-
-
diff --git a/lnbits/extensions/subdomains/templates/subdomains/display.html b/lnbits/extensions/subdomains/templates/subdomains/display.html
deleted file mode 100644
index f11c9ddc..00000000
--- a/lnbits/extensions/subdomains/templates/subdomains/display.html
+++ /dev/null
@@ -1,221 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
- {{ domain_domain }}
-
- {{ domain_desc }}
-
-
-
-
-
-
-
-
-
-
-
- Cost per day: {{ domain_cost }} sats
- {% raw %} Total cost: {{amountSats}} sats {% endraw %}
-
-
- Submit
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
- Copy invoice
- Close
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html
deleted file mode 100644
index a39773e7..00000000
--- a/lnbits/extensions/subdomains/templates/subdomains/index.html
+++ /dev/null
@@ -1,549 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
-
- New Domain
-
-
-
-
-
-
-
-
Domains
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
Subdomains
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} Subdomain extension
-
-
-
-
- {% include "subdomains/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update Form
- Create Domain
- Cancel
-
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/subdomains/util.py b/lnbits/extensions/subdomains/util.py
deleted file mode 100644
index 9265e870..00000000
--- a/lnbits/extensions/subdomains/util.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import re
-import socket
-
-
-# Function to validate domain name.
-def isValidDomain(str):
- # Regex to check valid
- # domain name.
- regex = "^((?!-)[A-Za-z0-9-]{1,63}(?Tip Jars
-Accept tips in Bitcoin, with small messages attached!
-The TipJar extension allows you to integrate Bitcoin Lightning (and on-chain) tips into your website or social media!
-
-
-
-How to set it up
-
-1. Simply create a new Tip Jar with the desired details (onchain optional):
-
-1. Share the URL you get from this little button:
-
-
-
-And that's it already! Let the sats flow!
diff --git a/lnbits/extensions/tipjar/__init__.py b/lnbits/extensions/tipjar/__init__.py
deleted file mode 100644
index b7e2b967..00000000
--- a/lnbits/extensions/tipjar/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from fastapi import APIRouter
-from fastapi.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_tipjar")
-
-tipjar_ext: APIRouter = APIRouter(prefix="/tipjar", tags=["tipjar"])
-
-tipjar_static_files = [
- {
- "path": "/tipjar/static",
- "app": StaticFiles(directory="lnbits/extensions/tipjar/static"),
- "name": "tipjar_static",
- }
-]
-
-
-def tipjar_renderer():
- return template_renderer(["lnbits/extensions/tipjar/templates"])
-
-
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/tipjar/config.json b/lnbits/extensions/tipjar/config.json
deleted file mode 100644
index 90f634ed..00000000
--- a/lnbits/extensions/tipjar/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Tip Jar",
- "short_description": "Accept Bitcoin donations, with messages attached!",
- "tile": "/tipjar/static/image/tipjar.png",
- "contributors": ["Fittiboy"]
-}
diff --git a/lnbits/extensions/tipjar/crud.py b/lnbits/extensions/tipjar/crud.py
deleted file mode 100644
index 3ea45d0d..00000000
--- a/lnbits/extensions/tipjar/crud.py
+++ /dev/null
@@ -1,123 +0,0 @@
-from typing import Optional
-
-from lnbits.db import SQLITE
-
-# todo: use the API, not direct import
-from ..satspay.crud import delete_charge # type: ignore
-from . import db
-from .models import Tip, TipJar, createTipJar
-
-
-async def create_tip(
- id: str, wallet: str, message: str, name: str, sats: int, tipjar: str
-) -> Tip:
- """Create a new Tip"""
- await db.execute(
- """
- INSERT INTO tipjar.Tips (
- id,
- wallet,
- name,
- message,
- sats,
- tipjar
- )
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (id, wallet, name, message, sats, tipjar),
- )
-
- tip = await get_tip(id)
- assert tip, "Newly created tip couldn't be retrieved"
- return tip
-
-
-async def create_tipjar(data: createTipJar) -> TipJar:
- """Create a new TipJar"""
-
- returning = "" if db.type == SQLITE else "RETURNING ID"
- method = db.execute if db.type == SQLITE else db.fetchone
-
- result = await (method)(
- f"""
- INSERT INTO tipjar.TipJars (
- name,
- wallet,
- webhook,
- onchain
- )
- VALUES (?, ?, ?, ?)
- {returning}
- """,
- (data.name, data.wallet, data.webhook, data.onchain),
- )
- if db.type == SQLITE:
- tipjar_id = result._result_proxy.lastrowid
- else:
- tipjar_id = result[0]
-
- tipjar = await get_tipjar(tipjar_id)
- assert tipjar
- return tipjar
-
-
-async def get_tipjar(tipjar_id: int) -> Optional[TipJar]:
- """Return a tipjar by ID"""
- row = await db.fetchone("SELECT * FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
- return TipJar(**row) if row else None
-
-
-async def get_tipjars(wallet_id: str) -> Optional[list]:
- """Return all TipJars belonging assigned to the wallet_id"""
- rows = await db.fetchall(
- "SELECT * FROM tipjar.TipJars WHERE wallet = ?", (wallet_id,)
- )
- return [TipJar(**row) for row in rows] if rows else None
-
-
-async def delete_tipjar(tipjar_id: int) -> None:
- """Delete a TipJar and all corresponding Tips"""
- rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE tipjar = ?", (tipjar_id,))
- for row in rows:
- await delete_tip(row["id"])
- await db.execute("DELETE FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
-
-
-async def get_tip(tip_id: str) -> Optional[Tip]:
- """Return a Tip"""
- row = await db.fetchone("SELECT * FROM tipjar.Tips WHERE id = ?", (tip_id,))
- return Tip(**row) if row else None
-
-
-async def get_tips(wallet_id: str) -> Optional[list]:
- """Return all Tips assigned to wallet_id"""
- rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE wallet = ?", (wallet_id,))
- return [Tip(**row) for row in rows] if rows else None
-
-
-async def delete_tip(tip_id: str) -> None:
- """Delete a Tip and its corresponding statspay charge"""
- await db.execute("DELETE FROM tipjar.Tips WHERE id = ?", (tip_id,))
- await delete_charge(tip_id)
-
-
-async def update_tip(tip_id: str, **kwargs) -> Tip:
- """Update a Tip"""
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE tipjar.Tips SET {q} WHERE id = ?", (*kwargs.values(), tip_id)
- )
- row = await db.fetchone("SELECT * FROM tipjar.Tips WHERE id = ?", (tip_id,))
- assert row, "Newly updated tip couldn't be retrieved"
- return Tip(**row)
-
-
-async def update_tipjar(tipjar_id: str, **kwargs) -> TipJar:
- """Update a tipjar"""
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE tipjar.TipJars SET {q} WHERE id = ?", (*kwargs.values(), tipjar_id)
- )
- row = await db.fetchone("SELECT * FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
- assert row, "Newly updated tipjar couldn't be retrieved"
- return TipJar(**row)
diff --git a/lnbits/extensions/tipjar/migrations.py b/lnbits/extensions/tipjar/migrations.py
deleted file mode 100644
index d8f6da3f..00000000
--- a/lnbits/extensions/tipjar/migrations.py
+++ /dev/null
@@ -1,27 +0,0 @@
-async def m001_initial(db):
-
- await db.execute(
- f"""
- CREATE TABLE IF NOT EXISTS tipjar.TipJars (
- id {db.serial_primary_key},
- name TEXT NOT NULL,
- wallet TEXT NOT NULL,
- onchain TEXT,
- webhook TEXT
- );
- """
- )
-
- await db.execute(
- f"""
- CREATE TABLE IF NOT EXISTS tipjar.Tips (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
- name TEXT NOT NULL,
- message TEXT NOT NULL,
- sats {db.big_int} NOT NULL,
- tipjar {db.big_int} NOT NULL,
- FOREIGN KEY(tipjar) REFERENCES {db.references_schema}TipJars(id)
- );
- """
- )
diff --git a/lnbits/extensions/tipjar/models.py b/lnbits/extensions/tipjar/models.py
deleted file mode 100644
index 655888da..00000000
--- a/lnbits/extensions/tipjar/models.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from sqlite3 import Row
-from typing import Optional
-
-from pydantic import BaseModel
-
-
-class createTip(BaseModel):
- id: str
- wallet: str
- sats: int
- tipjar: int
- name: str = "Anonymous"
- message: str = ""
-
-
-class Tip(BaseModel):
- """A Tip represents a single donation"""
-
- id: str # This ID always corresponds to a satspay charge ID
- wallet: str
- name: str # Name of the donor
- message: str # Donation message
- sats: int
- tipjar: int # The ID of the corresponding tip jar
-
- @classmethod
- def from_row(cls, row: Row) -> "Tip":
- return cls(**dict(row))
-
-
-class createTipJar(BaseModel):
- name: str
- wallet: str
- webhook: Optional[str]
- onchain: Optional[str]
-
-
-class createTips(BaseModel):
- name: str
- sats: str
- tipjar: str
- message: str
-
-
-class TipJar(BaseModel):
- """A TipJar represents a user's tip jar"""
-
- id: int
- name: str # The name of the donatee
- wallet: str # Lightning wallet
- onchain: Optional[str] # Watchonly wallet
- webhook: Optional[str] # URL to POST tips to
-
- @classmethod
- def from_row(cls, row: Row) -> "TipJar":
- return cls(**dict(row))
diff --git a/lnbits/extensions/tipjar/static/image/tipjar.png b/lnbits/extensions/tipjar/static/image/tipjar.png
deleted file mode 100644
index 6f0d69b7..00000000
Binary files a/lnbits/extensions/tipjar/static/image/tipjar.png and /dev/null differ
diff --git a/lnbits/extensions/tipjar/templates/tipjar/_api_docs.html b/lnbits/extensions/tipjar/templates/tipjar/_api_docs.html
deleted file mode 100644
index 2fd8cf08..00000000
--- a/lnbits/extensions/tipjar/templates/tipjar/_api_docs.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- Tip Jar: Receive tips with messages!
-
-
- Your personal Bitcoin tip page, which supports lightning and on-chain
- payments. Notifications, including a donation message, can be sent via
- webhook.
-
- Created by,
- Fitti
-
-
-
-
diff --git a/lnbits/extensions/tipjar/templates/tipjar/display.html b/lnbits/extensions/tipjar/templates/tipjar/display.html
deleted file mode 100644
index 80e5c6fe..00000000
--- a/lnbits/extensions/tipjar/templates/tipjar/display.html
+++ /dev/null
@@ -1,94 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
- Tip {{ donatee }} some sats!
-
-
-
-
-
-
- Submit
-
-
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/tipjar/templates/tipjar/index.html b/lnbits/extensions/tipjar/templates/tipjar/index.html
deleted file mode 100644
index 19fca6e4..00000000
--- a/lnbits/extensions/tipjar/templates/tipjar/index.html
+++ /dev/null
@@ -1,443 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New TipJar
-
-
-
-
-
-
-
-
TipJars
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
Tips
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
- {{ col.label }}
-
-
-
-
-
-
- {{ col.value }}
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} TipJar extension
-
-
-
-
- {% include "tipjar/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Watch-Only extension MUST be activated and have a wallet
-
-
-
-
-
-
-
-
-
-
-
- Update TipJar
-
- Create TipJar
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/tipjar/views.py b/lnbits/extensions/tipjar/views.py
deleted file mode 100644
index ddb1b63c..00000000
--- a/lnbits/extensions/tipjar/views.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Query, Request
-from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import tipjar_ext, tipjar_renderer
-from .crud import get_tipjar
-
-templates = Jinja2Templates(directory="templates")
-
-
-@tipjar_ext.get("/")
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return tipjar_renderer().TemplateResponse(
- "tipjar/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@tipjar_ext.get("/{tipjar_id}")
-async def tip(request: Request, tipjar_id: int = Query(None)):
- """Return the donation form for the Tipjar corresponding to id"""
- tipjar = await get_tipjar(tipjar_id)
- if not tipjar:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="TipJar does not exist."
- )
-
- return tipjar_renderer().TemplateResponse(
- "tipjar/display.html",
- {"request": request, "donatee": tipjar.name, "tipjar": tipjar.id},
- )
diff --git a/lnbits/extensions/tipjar/views_api.py b/lnbits/extensions/tipjar/views_api.py
deleted file mode 100644
index 7d420fae..00000000
--- a/lnbits/extensions/tipjar/views_api.py
+++ /dev/null
@@ -1,220 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, Query
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_user, get_wallet
-from lnbits.decorators import WalletTypeInfo, get_key_type
-
-# todo: use the API, not direct import
-from ..satspay.crud import create_charge # type: ignore
-from ..satspay.models import CreateCharge # type: ignore
-from . import tipjar_ext
-from .crud import (
- create_tip,
- create_tipjar,
- delete_tip,
- delete_tipjar,
- get_tip,
- get_tipjar,
- get_tipjars,
- get_tips,
- update_tip,
- update_tipjar,
-)
-from .models import createTip, createTipJar, createTips
-
-
-@tipjar_ext.post("/api/v1/tipjars")
-async def api_create_tipjar(data: createTipJar):
- """Create a tipjar, which holds data about how/where to post tips"""
- try:
- tipjar = await create_tipjar(data)
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
-
- return tipjar.dict()
-
-
-async def user_from_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
- return wallet.wallet.user
-
-
-@tipjar_ext.post("/api/v1/tips")
-async def api_create_tip(data: createTips):
- """Take data from tip form and return satspay charge"""
- sats = int(data.sats)
- message = data.message
- if not message:
- message = "No message"
- tipjar_id = int(data.tipjar)
- tipjar = await get_tipjar(tipjar_id)
- if not tipjar:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Tipjar does not exist."
- )
-
- wallet_id = tipjar.wallet
- wallet = await get_wallet(wallet_id)
- if not wallet:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Tipjar wallet does not exist."
- )
-
- name = data.name
-
- # Ensure that description string can be split reliably
- name = name.replace('"', "''")
- if not name:
- name = "Anonymous"
-
- description = f"{name}: {message}"
- charge = await create_charge(
- user=wallet.user,
- data=CreateCharge(
- amount=sats,
- webhook=tipjar.webhook or "",
- description=description,
- onchainwallet=tipjar.onchain or "",
- lnbitswallet=tipjar.wallet,
- completelink="/tipjar/" + str(tipjar_id),
- completelinktext="Thanks for the tip!",
- time=1440,
- custom_css="",
- ),
- )
-
- await create_tip(
- id=charge.id,
- wallet=tipjar.wallet,
- message=message,
- name=name,
- sats=int(data.sats),
- tipjar=data.tipjar,
- )
-
- return {"redirect_url": f"/satspay/{charge.id}"}
-
-
-@tipjar_ext.get("/api/v1/tipjars")
-async def api_get_tipjars(wallet: WalletTypeInfo = Depends(get_key_type)):
- """Return list of all tipjars assigned to wallet with given invoice key"""
- user = await get_user(wallet.wallet.user)
- if not user:
- return []
- tipjars = []
- for wallet_id in user.wallet_ids:
- new_tipjars = await get_tipjars(wallet_id)
- tipjars += new_tipjars if new_tipjars else []
- return [tipjar.dict() for tipjar in tipjars]
-
-
-@tipjar_ext.get("/api/v1/tips")
-async def api_get_tips(wallet: WalletTypeInfo = Depends(get_key_type)):
- """Return list of all tips assigned to wallet with given invoice key"""
- user = await get_user(wallet.wallet.user)
- if not user:
- return []
- tips = []
- for wallet_id in user.wallet_ids:
- new_tips = await get_tips(wallet_id)
- tips += new_tips if new_tips else []
- return [tip.dict() for tip in tips]
-
-
-@tipjar_ext.put("/api/v1/tips/{tip_id}")
-async def api_update_tip(
- data: createTip,
- wallet: WalletTypeInfo = Depends(get_key_type),
- tip_id: str = Query(None),
-):
- """Update a tip with the data given in the request"""
- if tip_id:
- tip = await get_tip(tip_id)
-
- if not tip:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Tip does not exist."
- )
-
- if tip.wallet != wallet.wallet.id:
-
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your tip."
- )
-
- tip = await update_tip(tip_id, **data.dict())
- else:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="No tip ID specified"
- )
- return tip.dict()
-
-
-@tipjar_ext.put("/api/v1/tipjars/{tipjar_id}")
-async def api_update_tipjar(
- data: createTipJar,
- wallet: WalletTypeInfo = Depends(get_key_type),
- tipjar_id: int = Query(None),
-):
- """Update a tipjar with the data given in the request"""
- if tipjar_id:
- tipjar = await get_tipjar(tipjar_id)
-
- if not tipjar:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="TipJar does not exist."
- )
-
- if tipjar.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your tipjar."
- )
-
- tipjar = await update_tipjar(str(tipjar_id), **data.dict())
- else:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail="No tipjar ID specified"
- )
- return tipjar.dict()
-
-
-@tipjar_ext.delete("/api/v1/tips/{tip_id}")
-async def api_delete_tip(
- wallet: WalletTypeInfo = Depends(get_key_type), tip_id: str = Query(None)
-):
- """Delete the tip with the given tip_id"""
- tip = await get_tip(tip_id)
- if not tip:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="No tip with this ID!"
- )
- if tip.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Not authorized to delete this tip!",
- )
- await delete_tip(tip_id)
-
- return "", HTTPStatus.NO_CONTENT
-
-
-@tipjar_ext.delete("/api/v1/tipjars/{tipjar_id}")
-async def api_delete_tipjar(
- wallet: WalletTypeInfo = Depends(get_key_type), tipjar_id: int = Query(None)
-):
- """Delete the tipjar with the given tipjar_id"""
- tipjar = await get_tipjar(tipjar_id)
- if not tipjar:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="No tipjar with this ID!"
- )
- if tipjar.wallet != wallet.wallet.id:
-
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Not authorized to delete this tipjar!",
- )
- await delete_tipjar(tipjar_id)
-
- return "", HTTPStatus.NO_CONTENT
diff --git a/lnbits/extensions/watchonly/README.md b/lnbits/extensions/watchonly/README.md
deleted file mode 100644
index 4a8f2841..00000000
--- a/lnbits/extensions/watchonly/README.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Onchain Wallet (watch-only)
-
-## Monitor an onchain wallet and generate addresses for onchain payments
-
-Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API.
-
-You can now use this wallet on the LNbits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension
-
-Video demo
-
-### Wallet Account
- - a user can add one or more `xPubs` or `descriptors`
- - the `xPub` must be unique per user
- - such and entry is called an `Wallet Account`
- - the addresses in a `Wallet Account` are split into `Receive Addresses` and `Change Address`
- - the user interacts directly only with the `Receive Addresses` (by sharing them)
- - see [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) for more details
- - same `xPub` will always generate the same addresses (deterministic)
- - when a `Wallet Account` is created, there are generated `20 Receive Addresses` and `5 Change Address`
- - the limits can be change from the `Config` page (see `screenshot 1`)
- - regular wallets only scan up to `20` empty receive addresses. If the user generates addresses beyond this limit a warning is shown (see `screenshot 4`)
- - an account can be added `From Hardware Device`
-
-### Scan Blockchain
- - when the user clicks `Scan Blockchain`, the wallet will loop over the all addresses (for each account)
- - if funds are found, then the list is extended
- - will scan addresses for all wallet accounts
- - the search is done on the client-side (using the `mempool.space` API). `mempool.space` has a limit on the number of req/sec, therefore it is expected for the scanning to start fast, but slow down as more HTTP requests have to be retried
- - addresses can also be rescanned individually form the `Address Details` section (`Addresses` tab) of each address
-
-### New Receive Address
- - the `New Receive Address` button show the user the NEXT un-used address
- - un-used means funds have not already been sent to that address AND the address has not already been shared
- - internally there is a counter that keeps track of the last shared address
- - it is possible to add a `Note` to each address in order to remember when/with whom it was shared
- - mind the gap (`screenshot 4`)
-
-### Addresses Tab
-- the `Addresses` tab contains a list with the addresses for all the `Wallet Accounts`
- - only one entry per address will be shown (even if there are multiple UTXOs at that address)
- - several filter criteria can be applied
- - unconfirmed funds are also taken into account
- - `Address Details` can be viewed by clicking the `Expand` button
-
-### History Tap
- - shows the chronological order of transactions
- - it shows unconfirmed transactions at the top
- - it can be exported as CSV file
-
-### Coins Tab
- - shows the UTXOs for all wallets
- - there can be multiple UTXOs for the same address
-
-### New Payment
- - create a new `Partially Signed Bitcoin Transaction`
- - multiple `Send Addresses` can be added
- - the `Max` button next to an address is for sending the remaining funds to this address (no change)
- - the user can select the inputs (UTXOs) manually, or it can use of the basic selection algorithms
- - amounts have to be provided for the `Send Addresses` beforehand (so the algorithm knows the amount to be selected)
- - `Show Change` allows to select from which account the change address will be selected (defaults to the first one)
- - `Show Custom Fee` allows to manually select the fee
- - it defaults to the `Medium` value at the moment the `New Payment` button was clicked
- - it can be refreshed
- - warnings are shown if the fee is too Low or to High
-
-### Check & Send
- - creates the PSBT and sends it to the Hardware Wallet
- - a confirmation will be shown for each Output and for the Fee
- - after the user confirms the addresses and amounts, the transaction will be signed on the Hardware Device
-
-### Share PSBT
- - Show the PSBT without sending it to the Hardware Wallet
-
-## Screensots
-- screenshot 1:
-
-
-- screenshot 2:
-
-
-- screenshot 3:
-
-
-- screenshot 4:
-
-
-
diff --git a/lnbits/extensions/watchonly/__init__.py b/lnbits/extensions/watchonly/__init__.py
deleted file mode 100644
index 49adb462..00000000
--- a/lnbits/extensions/watchonly/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from fastapi import APIRouter
-from fastapi.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_watchonly")
-
-watchonly_static_files = [
- {
- "path": "/watchonly/static",
- "app": StaticFiles(directory="lnbits/extensions/watchonly/static"),
- "name": "watchonly_static",
- }
-]
-
-watchonly_ext: APIRouter = APIRouter(prefix="/watchonly", tags=["watchonly"])
-
-
-def watchonly_renderer():
- return template_renderer(["lnbits/extensions/watchonly/templates"])
-
-
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/watchonly/config.json b/lnbits/extensions/watchonly/config.json
deleted file mode 100644
index c9fec893..00000000
--- a/lnbits/extensions/watchonly/config.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "Onchain Wallet",
- "short_description": "Onchain watch only wallets",
- "tile": "/watchonly/static/bitcoin-wallet.png",
- "contributors": [
- "arcbtc",
- "motorina0"
- ]
-}
diff --git a/lnbits/extensions/watchonly/crud.py b/lnbits/extensions/watchonly/crud.py
deleted file mode 100644
index 8472fa01..00000000
--- a/lnbits/extensions/watchonly/crud.py
+++ /dev/null
@@ -1,246 +0,0 @@
-import json
-from typing import List, Optional
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .helpers import derive_address
-from .models import Address, Config, WalletAccount
-
-##########################WALLETS####################
-
-
-async def create_watch_wallet(user: str, w: WalletAccount) -> WalletAccount:
- wallet_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO watchonly.wallets (
- id,
- "user",
- masterpub,
- fingerprint,
- title,
- type,
- address_no,
- balance,
- network,
- meta
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- wallet_id,
- user,
- w.masterpub,
- w.fingerprint,
- w.title,
- w.type,
- w.address_no,
- w.balance,
- w.network,
- w.meta,
- ),
- )
- wallet = await get_watch_wallet(wallet_id)
- assert wallet
- return wallet
-
-
-async def get_watch_wallet(wallet_id: str) -> Optional[WalletAccount]:
- row = await db.fetchone(
- "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,)
- )
- return WalletAccount.from_row(row) if row else None
-
-
-async def get_watch_wallets(user: str, network: str) -> List[WalletAccount]:
- rows = await db.fetchall(
- """SELECT * FROM watchonly.wallets WHERE "user" = ? AND network = ?""",
- (user, network),
- )
- return [WalletAccount(**row) for row in rows]
-
-
-async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[WalletAccount]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
-
- await db.execute(
- f"UPDATE watchonly.wallets SET {q} WHERE id = ?", (*kwargs.values(), wallet_id)
- )
- row = await db.fetchone(
- "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,)
- )
- return WalletAccount.from_row(row) if row else None
-
-
-async def delete_watch_wallet(wallet_id: str) -> None:
- await db.execute("DELETE FROM watchonly.wallets WHERE id = ?", (wallet_id,))
-
-
-########################ADDRESSES#######################
-
-
-async def get_fresh_address(wallet_id: str) -> Optional[Address]:
- # todo: move logic to views_api after satspay refactoring
- wallet = await get_watch_wallet(wallet_id)
-
- if not wallet:
- return None
-
- wallet_addresses = await get_addresses(wallet_id)
- receive_addresses = list(
- filter(
- lambda addr: addr.branch_index == 0 and addr.has_activity, wallet_addresses
- )
- )
- last_receive_index = (
- receive_addresses.pop().address_index if receive_addresses else -1
- )
- address_index = (
- last_receive_index
- if last_receive_index > wallet.address_no
- else wallet.address_no
- )
-
- address = await get_address_at_index(wallet_id, 0, address_index + 1)
-
- if not address:
- addresses = await create_fresh_addresses(
- wallet_id, address_index + 1, address_index + 2
- )
- address = addresses.pop()
-
- await update_watch_wallet(wallet_id, **{"address_no": address_index + 1})
-
- return address
-
-
-async def create_fresh_addresses(
- wallet_id: str,
- start_address_index: int,
- end_address_index: int,
- change_address=False,
-) -> List[Address]:
- if start_address_index > end_address_index:
- return []
-
- wallet = await get_watch_wallet(wallet_id)
- if not wallet:
- return []
-
- branch_index = 1 if change_address else 0
-
- for address_index in range(start_address_index, end_address_index):
- address = await derive_address(wallet.masterpub, address_index, branch_index)
-
- await db.execute(
- """
- INSERT INTO watchonly.addresses (
- id,
- address,
- wallet,
- amount,
- branch_index,
- address_index
- )
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (urlsafe_short_hash(), address, wallet_id, 0, branch_index, address_index),
- )
-
- # return fresh addresses
- rows = await db.fetchall(
- """
- SELECT * FROM watchonly.addresses
- WHERE wallet = ? AND branch_index = ? AND address_index >= ? AND address_index < ?
- ORDER BY branch_index, address_index
- """,
- (wallet_id, branch_index, start_address_index, end_address_index),
- )
-
- return [Address(**row) for row in rows]
-
-
-async def get_address(address: str) -> Optional[Address]:
- row = await db.fetchone(
- "SELECT * FROM watchonly.addresses WHERE address = ?", (address,)
- )
- return Address.from_row(row) if row else None
-
-
-async def get_address_at_index(
- wallet_id: str, branch_index: int, address_index: int
-) -> Optional[Address]:
- row = await db.fetchone(
- """
- SELECT * FROM watchonly.addresses
- WHERE wallet = ? AND branch_index = ? AND address_index = ?
- """,
- (
- wallet_id,
- branch_index,
- address_index,
- ),
- )
- return Address.from_row(row) if row else None
-
-
-async def get_addresses(wallet_id: str) -> List[Address]:
- rows = await db.fetchall(
- """
- SELECT * FROM watchonly.addresses WHERE wallet = ?
- ORDER BY branch_index, address_index
- """,
- (wallet_id,),
- )
-
- return [Address(**row) for row in rows]
-
-
-async def update_address(id: str, **kwargs) -> Optional[Address]:
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
-
- await db.execute(
- f"""UPDATE watchonly.addresses SET {q} WHERE id = ? """,
- (*kwargs.values(), id),
- )
- row = await db.fetchone("SELECT * FROM watchonly.addresses WHERE id = ?", (id,))
- return Address.from_row(row) if row else None
-
-
-async def delete_addresses_for_wallet(wallet_id: str) -> None:
- await db.execute("DELETE FROM watchonly.addresses WHERE wallet = ?", (wallet_id,))
-
-
-######################CONFIG#######################
-async def create_config(user: str) -> Config:
- config = Config()
- await db.execute(
- """
- INSERT INTO watchonly.config ("user", json_data)
- VALUES (?, ?)
- """,
- (user, json.dumps(config.dict())),
- )
- row = await db.fetchone(
- """SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
- )
- return json.loads(row[0], object_hook=lambda d: Config(**d))
-
-
-async def update_config(config: Config, user: str) -> Optional[Config]:
- await db.execute(
- """UPDATE watchonly.config SET json_data = ? WHERE "user" = ?""",
- (json.dumps(config.dict()), user),
- )
- row = await db.fetchone(
- """SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
- )
- return json.loads(row[0], object_hook=lambda d: Config(**d))
-
-
-async def get_config(user: str) -> Optional[Config]:
- row = await db.fetchone(
- """SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
- )
- return json.loads(row[0], object_hook=lambda d: Config(**d)) if row else None
diff --git a/lnbits/extensions/watchonly/helpers.py b/lnbits/extensions/watchonly/helpers.py
deleted file mode 100644
index 0ac36454..00000000
--- a/lnbits/extensions/watchonly/helpers.py
+++ /dev/null
@@ -1,76 +0,0 @@
-from typing import Optional, Tuple
-
-from embit.descriptor import Descriptor, Key
-from embit.descriptor.arguments import AllowedDerivation
-from embit.networks import NETWORKS
-
-
-def detect_network(k):
- version = k.key.version
- for network_name in NETWORKS:
- net = NETWORKS[network_name]
- # not found in this network
- if version in [net["xpub"], net["ypub"], net["zpub"], net["Zpub"], net["Ypub"]]:
- return net
-
-
-def parse_key(masterpub: str) -> Tuple[Descriptor, Optional[dict]]:
- """Parses masterpub or descriptor and returns a tuple: (Descriptor, network)
- To create addresses use descriptor.derive(num).address(network=network)
- """
- network = None
- desc = None
- # probably a single key
- if "(" not in masterpub:
- k = Key.from_string(masterpub)
- if not k.is_extended:
- raise ValueError("The key is not a master public key")
- if k.is_private:
- raise ValueError("Private keys are not allowed")
- # check depth
- if k.key.depth != 3:
- raise ValueError(
- "Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors."
- )
- # if allowed derivation is not provided use default /{0,1}/*
- if k.allowed_derivation is None:
- k.allowed_derivation = AllowedDerivation.default()
- # get version bytes
- version = k.key.version
- for network_name in NETWORKS:
- net = NETWORKS[network_name]
- # not found in this network
- if version in [net["xpub"], net["ypub"], net["zpub"]]:
- network = net
- if version == net["xpub"]:
- desc = Descriptor.from_string("pkh(%s)" % str(k))
- elif version == net["ypub"]:
- desc = Descriptor.from_string("sh(wpkh(%s))" % str(k))
- elif version == net["zpub"]:
- desc = Descriptor.from_string("wpkh(%s)" % str(k))
- break
- # we didn't find correct version
- if not network:
- raise ValueError("Unknown master public key version")
- if not desc:
- raise ValueError("descriptor not found, because version did not match")
-
- else:
- desc = Descriptor.from_string(masterpub)
- if not desc.is_wildcard:
- raise ValueError("Descriptor should have wildcards")
- for k in desc.keys:
- if k.is_extended:
- net = detect_network(k)
- if net is None:
- raise ValueError(f"Unknown version: {k}")
- if network is not None and network != net:
- raise ValueError("Keys from different networks")
- network = net
-
- return desc, network
-
-
-async def derive_address(masterpub: str, num: int, branch_index=0):
- desc, network = parse_key(masterpub)
- return desc.derive(num, branch_index).address(network=network)
diff --git a/lnbits/extensions/watchonly/migrations.py b/lnbits/extensions/watchonly/migrations.py
deleted file mode 100644
index bbef40a8..00000000
--- a/lnbits/extensions/watchonly/migrations.py
+++ /dev/null
@@ -1,102 +0,0 @@
-async def m001_initial(db):
- """
- Initial wallet table.
- """
- await db.execute(
- f"""
- CREATE TABLE watchonly.wallets (
- id TEXT NOT NULL PRIMARY KEY,
- "user" TEXT,
- masterpub TEXT NOT NULL,
- title TEXT NOT NULL,
- address_no INTEGER NOT NULL DEFAULT 0,
- balance {db.big_int} NOT NULL
- );
- """
- )
-
- await db.execute(
- f"""
- CREATE TABLE watchonly.addresses (
- id TEXT NOT NULL PRIMARY KEY,
- address TEXT NOT NULL,
- wallet TEXT NOT NULL,
- amount {db.big_int} NOT NULL
- );
- """
- )
-
- await db.execute(
- """
- CREATE TABLE watchonly.mempool (
- "user" TEXT NOT NULL,
- endpoint TEXT NOT NULL
- );
- """
- )
-
-
-async def m002_add_columns_to_adresses(db):
- """
- Add 'branch_index', 'address_index', 'has_activity' and 'note' columns to the 'addresses' table
- """
-
- await db.execute(
- "ALTER TABLE watchonly.addresses ADD COLUMN branch_index INTEGER NOT NULL DEFAULT 0;"
- )
- await db.execute(
- "ALTER TABLE watchonly.addresses ADD COLUMN address_index INTEGER NOT NULL DEFAULT 0;"
- )
- await db.execute(
- "ALTER TABLE watchonly.addresses ADD COLUMN has_activity BOOLEAN DEFAULT false;"
- )
- await db.execute("ALTER TABLE watchonly.addresses ADD COLUMN note TEXT;")
-
-
-async def m003_add_columns_to_wallets(db):
- """
- Add 'type' and 'fingerprint' columns to the 'wallets' table
- """
-
- await db.execute("ALTER TABLE watchonly.wallets ADD COLUMN type TEXT;")
- await db.execute(
- "ALTER TABLE watchonly.wallets ADD COLUMN fingerprint TEXT NOT NULL DEFAULT '';"
- )
-
-
-async def m004_create_config_table(db):
- """
- Allow the extension to persist and retrieve any number of config values.
- Each user has its configurations saved as a JSON string
- """
-
- await db.execute(
- """CREATE TABLE watchonly.config (
- "user" TEXT NOT NULL,
- json_data TEXT NOT NULL
- );"""
- )
-
-
-async def m005_add_network_column_to_wallets(db):
- """
- Add network' column to the 'wallets' table
- """
-
- await db.execute(
- "ALTER TABLE watchonly.wallets ADD COLUMN network TEXT DEFAULT 'Mainnet';"
- )
-
-
-async def m006_drop_mempool_table(db):
- """
- Mempool data is now part of `config`
- """
- await db.execute("DROP TABLE watchonly.mempool;")
-
-
-async def m007_add_wallet_meta_data(db):
- """
- Add 'meta' for storing various metadata about the wallet
- """
- await db.execute("ALTER TABLE watchonly.wallets ADD COLUMN meta TEXT DEFAULT '{}';")
diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py
deleted file mode 100644
index 24d63bfd..00000000
--- a/lnbits/extensions/watchonly/models.py
+++ /dev/null
@@ -1,99 +0,0 @@
-from sqlite3 import Row
-from typing import List, Optional
-
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class CreateWallet(BaseModel):
- masterpub: str = Query("")
- title: str = Query("")
- network: str = "Mainnet"
- meta: str = "{}"
-
-
-class WalletAccount(BaseModel):
- id: str
- masterpub: str
- fingerprint: str
- title: str
- address_no: int
- balance: int
- type: Optional[str] = ""
- network: str = "Mainnet"
- meta: str = "{}"
-
- @classmethod
- def from_row(cls, row: Row) -> "WalletAccount":
- return cls(**dict(row))
-
-
-class Address(BaseModel):
- id: str
- address: str
- wallet: str
- amount: int = 0
- branch_index: int = 0
- address_index: int
- note: Optional[str] = None
- has_activity: bool = False
-
- @classmethod
- def from_row(cls, row: Row) -> "Address":
- return cls(**dict(row))
-
-
-class TransactionInput(BaseModel):
- tx_id: str
- vout: int
- amount: int
- address: str
- branch_index: int
- address_index: int
- wallet: str
- tx_hex: str
-
-
-class TransactionOutput(BaseModel):
- amount: int
- address: str
- branch_index: Optional[int] = None
- address_index: Optional[int] = None
- wallet: Optional[str] = None
-
-
-class MasterPublicKey(BaseModel):
- id: str
- public_key: str
- fingerprint: str
-
-
-class CreatePsbt(BaseModel):
- masterpubs: List[MasterPublicKey]
- inputs: List[TransactionInput]
- outputs: List[TransactionOutput]
- fee_rate: int
- tx_size: int
-
-
-class SerializedTransaction(BaseModel):
- tx_hex: str
-
-
-class ExtractPsbt(BaseModel):
- psbtBase64 = "" # // todo snake case
- inputs: List[SerializedTransaction]
- network = "Mainnet"
-
-
-class SignedTransaction(BaseModel):
- tx_hex: Optional[str]
- tx_json: Optional[str]
-
-
-class Config(BaseModel):
- mempool_endpoint = "https://mempool.space"
- receive_gap_limit = 20
- change_gap_limit = 5
- sats_denominated = True
- network = "Mainnet"
diff --git a/lnbits/extensions/watchonly/static/bitcoin-wallet.png b/lnbits/extensions/watchonly/static/bitcoin-wallet.png
deleted file mode 100644
index 3cd5ac0f..00000000
Binary files a/lnbits/extensions/watchonly/static/bitcoin-wallet.png and /dev/null differ
diff --git a/lnbits/extensions/watchonly/static/components/address-list/address-list.html b/lnbits/extensions/watchonly/static/components/address-list/address-list.html
deleted file mode 100644
index f397ee97..00000000
--- a/lnbits/extensions/watchonly/static/components/address-list/address-list.html
+++ /dev/null
@@ -1,215 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{satBtc(props.row.amount)}}
-
-
-
- {{props.row.note}}
-
-
- {{getWalletName(props.row.wallet)}}
-
-
-
-
-
-
-
-
- QR Code
-
-
- Copy
-
-
-
- Rescan
-
-
- History
-
-
- View Coins
-
-
-
-
-
Note:
-
-
-
-
- Update
-
-
-
-
-
-
-
- {{props.row.error}}
-
-
-
-
-
- Gap limit of 20 addresses exceeded. Other wallets might not
- detect funds at this address.
-
-
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/address-list/address-list.js b/lnbits/extensions/watchonly/static/components/address-list/address-list.js
deleted file mode 100644
index 61545df0..00000000
--- a/lnbits/extensions/watchonly/static/components/address-list/address-list.js
+++ /dev/null
@@ -1,131 +0,0 @@
-async function addressList(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('address-list', {
- name: 'address-list',
- template,
-
- props: [
- 'addresses',
- 'accounts',
- 'mempool-endpoint',
- 'inkey',
- 'sats-denominated'
- ],
- data: function () {
- return {
- show: false,
- history: [],
- selectedWallet: null,
- note: '',
- filterOptions: [
- 'Show Change Addresses',
- 'Show Gap Addresses',
- 'Only With Amount'
- ],
- filterValues: [],
-
- addressesTable: {
- columns: [
- {
- name: 'expand',
- align: 'left',
- label: ''
- },
- {
- name: 'address',
- align: 'left',
- label: 'Address',
- field: 'address',
- sortable: true
- },
- {
- name: 'amount',
- align: 'left',
- label: 'Amount',
- field: 'amount',
- sortable: true
- },
- {
- name: 'note',
- align: 'left',
- label: 'Note',
- field: 'note',
- sortable: true
- },
- {
- name: 'wallet',
- align: 'left',
- label: 'Account',
- field: 'wallet',
- sortable: true
- }
- ],
- pagination: {
- rowsPerPage: 0,
- sortBy: 'amount',
- descending: true
- },
- filter: ''
- }
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
- // todo: bad. base.js not present in custom components
- copyText: function (text, message, position) {
- var notify = this.$q.notify
- Quasar.utils.copyToClipboard(text).then(function () {
- notify({
- message: message || 'Copied to clipboard!',
- position: position || 'bottom'
- })
- })
- },
- getWalletName: function (walletId) {
- const wallet = (this.accounts || []).find(wl => wl.id === walletId)
- return wallet ? wallet.title : 'unknown'
- },
- getFilteredAddresses: function () {
- const selectedWalletId = this.selectedWallet?.id
- const filter = this.filterValues || []
- const includeChangeAddrs = filter.includes('Show Change Addresses')
- const includeGapAddrs = filter.includes('Show Gap Addresses')
- const excludeNoAmount = filter.includes('Only With Amount')
-
- const walletsLimit = (this.accounts || []).reduce((r, w) => {
- r[`_${w.id}`] = w.address_no
- return r
- }, {})
-
- const fAddresses = this.addresses.filter(
- a =>
- (includeChangeAddrs || !a.isChange) &&
- (includeGapAddrs ||
- a.isChange ||
- a.addressIndex <= walletsLimit[`_${a.wallet}`]) &&
- !(excludeNoAmount && a.amount === 0) &&
- (!selectedWalletId || a.wallet === selectedWalletId)
- )
- return fAddresses
- },
-
- scanAddress: async function (addressData) {
- this.$emit('scan:address', addressData)
- },
- showAddressDetails: function (addressData) {
- this.$emit('show-address-details', addressData)
- },
- searchInTab: function (tab, value) {
- this.$emit('search:tab', {tab, value})
- },
- updateNoteForAddress: async function (addressData, note) {
- this.$emit('update:note', {addressId: addressData.id, note})
- }
- },
-
- created: async function () {}
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html b/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html
deleted file mode 100644
index 0df5bebf..00000000
--- a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
Fee Rate:
-
-
-
-
-
-
-
-
-
-
-
- Warning! The fee is too low. The transaction might take a long time to
- confirm.
-
-
- Warning! The fee is too high. You might be overpaying for this
- transaction.
-
-
-
-
-
-
Fee:
-
{{feeValue}} sats
-
- Refresh Fee Rates
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.js b/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.js
deleted file mode 100644
index 7a920a9a..00000000
--- a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.js
+++ /dev/null
@@ -1,64 +0,0 @@
-async function feeRate(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('fee-rate', {
- name: 'fee-rate',
- template,
-
- props: ['rate', 'fee-value', 'sats-denominated', 'mempool-endpoint'],
-
- computed: {
- feeRate: {
- get: function () {
- return this['rate']
- },
- set: function (value) {
- this.$emit('update:rate', +value)
- }
- }
- },
-
- data: function () {
- return {
- recommededFees: {
- fastestFee: 1,
- halfHourFee: 1,
- hourFee: 1,
- economyFee: 1,
- minimumFee: 1
- }
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
-
- refreshRecommendedFees: async function () {
- const fn = async () => {
- const {
- bitcoin: {fees: feesAPI}
- } = mempoolJS({
- hostname: this.mempoolEndpoint
- })
- return feesAPI.getFeesRecommended()
- }
- this.recommededFees = await retryWithDelay(fn)
- },
- getFeeRateLabel: function (feeRate) {
- const fees = this.recommededFees
- if (feeRate >= fees.fastestFee)
- return `High Priority (${feeRate} sat/vB)`
- if (feeRate >= fees.halfHourFee)
- return `Medium Priority (${feeRate} sat/vB)`
- if (feeRate >= fees.hourFee) return `Low Priority (${feeRate} sat/vB)`
- return `No Priority (${feeRate} sat/vB)`
- }
- },
-
- created: async function () {
- await this.refreshRecommendedFees()
- this.feeRate = this.recommededFees.halfHourFee
- }
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/history/history.html b/lnbits/extensions/watchonly/static/components/history/history.html
deleted file mode 100644
index ced03b26..00000000
--- a/lnbits/extensions/watchonly/static/components/history/history.html
+++ /dev/null
@@ -1,146 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Export to CSV
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{props.row.confirmed ? 'Sent' : 'Sending...'}}
-
-
- {{props.row.confirmed ? 'Received' : 'Receiving...'}}
-
-
-
- {{satBtc(props.row.totalAmount || props.row.amount)}}
-
-
-
- {{props.row.address}}
-
- ...
-
-
- {{ props.row.date }}
-
-
-
-
-
-
UTXOs
-
{{satBtc(props.row.amount)}}
-
{{props.row.address}}
-
-
-
-
{{satBtc(s.amount)}}
-
{{s.address}}
-
-
-
Fee
-
{{satBtc(props.row.fee)}}
-
-
-
Block Height
-
{{props.row.height}}
-
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/history/history.js b/lnbits/extensions/watchonly/static/components/history/history.js
deleted file mode 100644
index 81cf44cc..00000000
--- a/lnbits/extensions/watchonly/static/components/history/history.js
+++ /dev/null
@@ -1,98 +0,0 @@
-async function history(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('history', {
- name: 'history',
- template,
-
- props: ['history', 'mempool-endpoint', 'sats-denominated', 'filter'],
- data: function () {
- return {
- historyTable: {
- columns: [
- {
- name: 'expand',
- align: 'left',
- label: ''
- },
- {
- name: 'status',
- align: 'left',
- label: 'Status'
- },
- {
- name: 'amount',
- align: 'left',
- label: 'Amount',
- field: 'amount',
- sortable: true
- },
- {
- name: 'address',
- align: 'left',
- label: 'Address',
- field: 'address',
- sortable: true
- },
- {
- name: 'date',
- align: 'left',
- label: 'Date',
- field: 'date',
- sortable: true
- },
- {
- name: 'txId',
- field: 'txId'
- }
- ],
- exportColums: [
- {
- label: 'Action',
- field: 'action'
- },
- {
- label: 'Date&Time',
- field: 'date'
- },
- {
- label: 'Amount',
- field: 'amount'
- },
- {
- label: 'Fee',
- field: 'fee'
- },
- {
- label: 'Transaction Id',
- field: 'txId'
- }
- ],
- pagination: {
- rowsPerPage: 0
- }
- }
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
- getFilteredAddressesHistory: function () {
- return this.history.filter(a => (!a.isChange || a.sent) && !a.isSubItem)
- },
- exportHistoryToCSV: function () {
- const history = this.getFilteredAddressesHistory().map(a => ({
- ...a,
- action: a.sent ? 'Sent' : 'Received'
- }))
- LNbits.utils.exportCSV(
- this.historyTable.exportColums,
- history,
- 'address-history'
- )
- }
- },
- created: async function () {}
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/my-checkbox/my-checkbox.html b/lnbits/extensions/watchonly/static/components/my-checkbox/my-checkbox.html
deleted file mode 100644
index 83af1248..00000000
--- a/lnbits/extensions/watchonly/static/components/my-checkbox/my-checkbox.html
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/lnbits/extensions/watchonly/static/components/my-checkbox/my-checkbox.js b/lnbits/extensions/watchonly/static/components/my-checkbox/my-checkbox.js
deleted file mode 100644
index 3d22c3a0..00000000
--- a/lnbits/extensions/watchonly/static/components/my-checkbox/my-checkbox.js
+++ /dev/null
@@ -1,16 +0,0 @@
-async function initMyCheckbox(path) {
- const t = await loadTemplateAsync(path)
- Vue.component('my-checkbox', {
- name: 'my-checkbox',
- template: t,
- data() {
- return {checked: false, title: 'Check me'}
- },
- methods: {
- check() {
- this.checked = !this.checked
- console.log('### checked', this.checked)
- }
- }
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/payment/payment.html b/lnbits/extensions/watchonly/static/components/payment/payment.html
deleted file mode 100644
index 6e1d94c7..00000000
--- a/lnbits/extensions/watchonly/static/components/payment/payment.html
+++ /dev/null
@@ -1,312 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Fee Rate:
-
- {{feeRate}} sats/vbyte
- Fee:
- {{satBtc(feeValue)}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Balance:
- {{satBtc(balance)}}
- Selected:
-
- {{satBtc(selectedAmount)}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Below dust limit. Will be used as fee.
-
-
-
-
- Change:
-
- {{satBtc(0)}}
-
-
- {{satBtc(changeAmount)}}
-
-
-
-
-
-
-
-
-
-
Change Account:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Serial Port
-
- Sign using a Serial Port device
-
-
-
-
- Share PSBT
- Share the PSBT as text or Animated QR Code
-
-
-
-
-
-
-
-
-
- The payed amount is higher than the selected amount!
-
-
-
-
-
-
-
-
-
- Close
-
-
-
-
-
-
-
-
- Transaction Details
-
-
-
-
-
-
-
Version
-
{{signedTx.version}}
-
-
-
Locktime
-
{{signedTx.locktime}}
-
-
-
Fee
-
- {{satBtc(signedTx.fee)}}
-
-
-
-
Outputs
-
-
-
- {{satBtc(out.amount)}}
-
-
-
- {{out.address}}
-
-
-
-
-
-
-
-
-
- Send
- Close
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/payment/payment.js b/lnbits/extensions/watchonly/static/components/payment/payment.js
deleted file mode 100644
index e9689003..00000000
--- a/lnbits/extensions/watchonly/static/components/payment/payment.js
+++ /dev/null
@@ -1,379 +0,0 @@
-async function payment(path) {
- const t = await loadTemplateAsync(path)
- Vue.component('payment', {
- name: 'payment',
- template: t,
-
- props: [
- 'accounts',
- 'addresses',
- 'utxos',
- 'mempool-endpoint',
- 'sats-denominated',
- 'serial-signer-ref',
- 'adminkey',
- 'network'
- ],
- watch: {
- immediate: true,
- accounts() {
- this.updateChangeAddress()
- },
- addresses() {
- this.updateChangeAddress()
- }
- },
-
- data: function () {
- return {
- DUST_LIMIT: 546,
- tx: null,
- psbtBase64: null,
- psbtBase64Signed: null,
- signedTx: null,
- signedTxHex: null,
- sentTxId: null,
- signedTxId: null,
- sendToList: [{address: '', amount: undefined}],
- changeWallet: null,
- changeAddress: {},
- showCustomFee: false,
- showCoinSelect: false,
- showChecking: false,
- showChange: false,
- showPsbt: false,
- showFinalTx: false,
- feeRate: 1
- }
- },
-
- computed: {
- txSize: function () {
- const tx = this.createTx()
- return Math.round(txSize(tx))
- },
- txSizeNoChange: function () {
- const tx = this.createTx(true)
- return Math.round(txSize(tx))
- },
- feeValue: function () {
- return this.feeRate * this.txSize
- },
- selectedAmount: function () {
- return this.utxos
- .filter(utxo => utxo.selected)
- .reduce((t, a) => t + (a.amount || 0), 0)
- },
- changeAmount: function () {
- return (
- this.selectedAmount -
- this.totalPayedAmount -
- this.feeRate * this.txSize
- )
- },
- balance: function () {
- return this.utxos.reduce((t, a) => t + (a.amount || 0), 0)
- },
- totalPayedAmount: function () {
- return this.sendToList.reduce((t, a) => t + (a.amount || 0), 0)
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
- clearState: function () {
- this.psbtBase64 = null
- this.psbtBase64Signed = null
- this.signedTx = null
- this.signedTxHex = null
- this.signedTxId = null
- this.sendToList = [{address: '', amount: undefined}]
- this.showChecking = false
- this.showPsbt = false
- this.showFinalTx = false
- },
- checkAndSend: async function () {
- this.showChecking = true
- try {
- if (!this.serialSignerRef.isConnected()) {
- this.$q.notify({
- type: 'warning',
- message: 'Please connect to a Signing device first!',
- timeout: 10000
- })
- return
- }
- const p2trUtxo = this.utxos.find(
- u => u.selected && u.accountType === 'p2tr'
- )
- if (p2trUtxo) {
- this.$q.notify({
- type: 'warning',
- message: 'Taproot Signing not supported yet!',
- caption: 'Please manually deselect the Taproot UTXOs',
- timeout: 10000
- })
- return
- }
- if (!this.serialSignerRef.isAuthenticated()) {
- await this.serialSignerRef.hwwShowPasswordDialog()
- const authenticated = await this.serialSignerRef.isAuthenticating()
- if (!authenticated) return
- }
-
- await this.createPsbt()
-
- if (this.psbtBase64) {
- const txData = {
- outputs: this.tx.outputs,
- feeRate: this.tx.fee_rate,
- feeValue: this.feeValue
- }
- await this.serialSignerRef.hwwSendPsbt(this.psbtBase64, txData)
- await this.serialSignerRef.isSendingPsbt()
- }
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Cannot check and sign PSBT!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.showChecking = false
- this.psbtBase64 = null
- }
- },
- showPsbtDialog: async function () {
- try {
- const valid = await this.$refs.paymentFormRef.validate()
- if (!valid) return
-
- const data = await this.createPsbt()
- if (data) {
- this.showPsbt = true
- }
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to create PSBT!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- createPsbt: async function () {
- try {
- this.tx = this.createTx()
- for (const input of this.tx.inputs) {
- input.tx_hex = await this.fetchTxHex(input.tx_id)
- }
-
- const changeOutput = this.tx.outputs.find(o => o.branch_index === 1)
- if (changeOutput) changeOutput.amount = this.changeAmount
-
- const {data} = await LNbits.api.request(
- 'POST',
- '/watchonly/api/v1/psbt',
- this.adminkey,
- this.tx
- )
-
- this.psbtBase64 = data
- return data
- } catch (err) {
- LNbits.utils.notifyApiError(err)
- }
- },
- createTx: function (excludeChange = false) {
- const tx = {
- fee_rate: this.feeRate,
- masterpubs: this.accounts.map(w => ({
- id: w.id,
- public_key: w.masterpub,
- fingerprint: w.fingerprint
- }))
- }
- tx.inputs = this.utxos
- .filter(utxo => utxo.selected)
- .map(mapUtxoToPsbtInput)
- .sort((a, b) =>
- a.tx_id < b.tx_id ? -1 : a.tx_id > b.tx_id ? 1 : a.vout - b.vout
- )
-
- tx.outputs = this.sendToList.map(out => ({
- address: out.address,
- amount: out.amount
- }))
-
- if (!excludeChange) {
- const change = this.createChangeOutput()
- const diffAmount = this.selectedAmount - this.totalPayedAmount
- if (diffAmount >= this.DUST_LIMIT) {
- tx.outputs.push(change)
- }
- }
- tx.tx_size = Math.round(txSize(tx))
- tx.inputs = _.shuffle(tx.inputs)
- tx.outputs = _.shuffle(tx.outputs)
-
- return tx
- },
- createChangeOutput: function () {
- const change = this.changeAddress
- const walletAcount =
- this.accounts.find(w => w.id === change.wallet) || {}
-
- return {
- address: change.address,
- address_index: change.addressIndex,
- branch_index: change.isChange ? 1 : 0,
- wallet: walletAcount.id
- }
- },
- selectChangeAddress: function (account) {
- if (!account) this.changeAddress = ''
- this.changeAddress =
- this.addresses.find(
- a => a.wallet === account.id && a.isChange && !a.hasActivity
- ) || {}
- },
- updateChangeAddress: function () {
- if (this.changeWallet) {
- const changeAccount = (this.accounts || []).find(
- w => w.id === this.changeWallet.id
- )
- // change account deleted
- if (!changeAccount) {
- this.changeWallet = this.accounts[0]
- }
- } else {
- this.changeWallet = this.accounts[0]
- }
- this.selectChangeAddress(this.changeWallet)
- },
- updateSignedPsbt: async function (psbtBase64) {
- try {
- this.showChecking = true
- this.psbtBase64Signed = psbtBase64
-
- const data = await this.extractTxFromPsbt(psbtBase64)
- this.showFinalTx = true
- if (data) {
- this.signedTx = JSON.parse(data.tx_json)
- this.signedTxHex = data.tx_hex
- } else {
- this.signedTx = null
- this.signedTxHex = null
- }
- } finally {
- this.showChecking = false
- }
- },
-
- fetchUtxoHexForPsbt: async function (psbtBase64) {
- if (this.tx?.inputs && this.tx?.inputs.length) return this.tx.inputs
-
- const {data: psbtUtxos} = await LNbits.api.request(
- 'PUT',
- '/watchonly/api/v1/psbt/utxos',
- this.adminkey,
- {psbtBase64}
- )
-
- const inputs = []
- for (const utxo of psbtUtxos) {
- const txHex = await this.fetchTxHex(utxo.tx_id)
- inputs.push({tx_hex: txHex})
- }
- return inputs
- },
- extractTxFromPsbt: async function (psbtBase64) {
- try {
- const inputs = await this.fetchUtxoHexForPsbt(psbtBase64)
-
- const {data} = await LNbits.api.request(
- 'PUT',
- '/watchonly/api/v1/psbt/extract',
- this.adminkey,
- {
- psbtBase64,
- inputs,
- network: this.network
- }
- )
- return data
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Cannot finalize PSBT!',
- caption: `${error}`,
- timeout: 10000
- })
- LNbits.utils.notifyApiError(error)
- }
- },
- broadcastTransaction: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'POST',
- '/watchonly/api/v1/tx',
- this.adminkey,
- {tx_hex: this.signedTxHex}
- )
- this.sentTxId = data
-
- this.$q.notify({
- type: 'positive',
- message: 'Transaction broadcasted!',
- caption: `${data}`,
- timeout: 10000
- })
-
- this.clearState()
- this.$emit('broadcast-done', this.sentTxId)
- } catch (error) {
- this.sentTxId = null
- this.$q.notify({
- type: 'warning',
- message: 'Failed to broadcast!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.showFinalTx = false
- }
- },
- fetchTxHex: async function (txId) {
- const {
- bitcoin: {transactions: transactionsAPI}
- } = mempoolJS({
- hostname: this.mempoolEndpoint
- })
-
- try {
- const response = await transactionsAPI.getTxHex({txid: txId})
- return response
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: `Failed to fetch transaction details for tx id: '${txId}'`,
- timeout: 10000
- })
- LNbits.utils.notifyApiError(error)
- throw error
- }
- },
- handleOutputsChange: function () {
- this.$refs.utxoList.refreshUtxoSelection(this.totalPayedAmount)
- },
- getTotalPaymentAmount: function () {
- return this.sendToList.reduce((t, a) => t + (a.amount || 0), 0)
- }
- },
-
- created: async function () {}
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/seed-input/seed-input.html b/lnbits/extensions/watchonly/static/components/seed-input/seed-input.html
deleted file mode 100644
index 60cdaaa8..00000000
--- a/lnbits/extensions/watchonly/static/components/seed-input/seed-input.html
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
Enter word at position: {{actualPosition}}
-
-
-
-
- Previous
-
-
-
-
-
-
- Next
- Done
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/seed-input/seed-input.js b/lnbits/extensions/watchonly/static/components/seed-input/seed-input.js
deleted file mode 100644
index 5e415abb..00000000
--- a/lnbits/extensions/watchonly/static/components/seed-input/seed-input.js
+++ /dev/null
@@ -1,102 +0,0 @@
-async function seedInput(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('seed-input', {
- name: 'seed-input',
- template,
-
- computed: {
- actualPosition: function () {
- return this.words[this.currentPosition].position
- }
- },
-
- data: function () {
- return {
- wordCountOptions: ['12', '15', '18', '21', '24'],
- wordCount: 24,
- words: [],
- currentPosition: 0,
- stringOptions: [],
- options: [],
- currentWord: '',
- done: false
- }
- },
-
- methods: {
- filterFn(val, update, abort) {
- update(() => {
- const needle = val.toLocaleLowerCase()
- this.options = this.stringOptions
- .filter(v => v.toLocaleLowerCase().indexOf(needle) != -1)
- .sort((a, b) => {
- if (a.startsWith(needle)) {
- if (b.startsWith(needle)) {
- return a - b
- }
- return -1
- } else {
- if (b.startsWith(needle)) {
- return 1
- }
- return a - b
- }
- })
- })
- },
- initWords() {
- const words = []
- for (let i = 1; i <= this.wordCount; i++) {
- words.push({
- position: i,
- value: ''
- })
- }
- this.currentPosition = 0
- this.words = _.shuffle(words)
- },
- setModel(val) {
- this.currentWord = val
- this.words[this.currentPosition].value = this.currentWord
- },
- nextPosition() {
- if (this.currentPosition < this.wordCount - 1) {
- this.currentPosition++
- }
- this.currentWord = this.words[this.currentPosition].value
- },
- previousPosition() {
- if (this.currentPosition > 0) {
- this.currentPosition--
- }
- this.currentWord = this.words[this.currentPosition].value
- },
- seedInputDone() {
- const badWordPositions = this.words
- .filter(w => !w.value || !this.stringOptions.includes(w.value))
- .map(w => w.position)
- if (badWordPositions.length) {
- this.$q.notify({
- timeout: 10000,
- type: 'warning',
- message:
- 'The seed has incorrect words. Please check at these positions: ',
- caption: 'Position: ' + badWordPositions.join(', ')
- })
- return
- }
- const mnemonic = this.words
- .sort((a, b) => a.position - b.position)
- .map(w => w.value)
- .join(' ')
- this.$emit('on-seed-input-done', mnemonic)
- this.done = true
- }
- },
-
- created: async function () {
- this.stringOptions = bip39WordList
- this.initWords()
- }
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/send-to/send-to.html b/lnbits/extensions/watchonly/static/components/send-to/send-to.html
deleted file mode 100644
index c16ebf95..00000000
--- a/lnbits/extensions/watchonly/static/components/send-to/send-to.html
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Max
-
-
-
-
-
-
-
- Add
-
-
-
- Payed Amount:
-
- {{satBtc(getTotalPaymentAmount())}}
-
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/send-to/send-to.js b/lnbits/extensions/watchonly/static/components/send-to/send-to.js
deleted file mode 100644
index 2b93cea7..00000000
--- a/lnbits/extensions/watchonly/static/components/send-to/send-to.js
+++ /dev/null
@@ -1,81 +0,0 @@
-async function sendTo(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('send-to', {
- name: 'send-to',
- template,
-
- props: [
- 'data',
- 'tx-size',
- 'selected-amount',
- 'fee-rate',
- 'sats-denominated'
- ],
-
- computed: {
- dataLocal: {
- get: function () {
- return this.data
- },
- set: function (value) {
- console.log('### computed update data', value)
- this.$emit('update:data', value)
- }
- }
- },
-
- data: function () {
- return {
- DUST_LIMIT: 546,
- paymentTable: {
- columns: [
- {
- name: 'data',
- align: 'left'
- }
- ],
- pagination: {
- rowsPerPage: 10
- },
- filter: ''
- }
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
- addPaymentAddress: function () {
- this.dataLocal.push({address: '', amount: undefined})
- this.handleOutputsChange()
- },
- deletePaymentAddress: function (v) {
- const index = this.dataLocal.indexOf(v)
- if (index !== -1) {
- this.dataLocal.splice(index, 1)
- }
- this.handleOutputsChange()
- },
-
- sendMaxToAddress: function (paymentAddress = {}) {
- const feeValue = this.feeRate * this.txSize
- const inputAmount = this.selectedAmount
- const currentAmount = Math.max(0, paymentAddress.amount || 0)
- const payedAmount = this.getTotalPaymentAmount() - currentAmount
- paymentAddress.amount = Math.max(
- 0,
- inputAmount - payedAmount - feeValue
- )
- },
- handleOutputsChange: function () {
- this.$emit('update:outputs')
- },
- getTotalPaymentAmount: function () {
- return this.dataLocal.reduce((t, a) => t + (a.amount || 0), 0)
- }
- },
-
- created: async function () {}
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/serial-port-config/serial-port-config.html b/lnbits/extensions/watchonly/static/components/serial-port-config/serial-port-config.html
deleted file mode 100644
index 18e52058..00000000
--- a/lnbits/extensions/watchonly/static/components/serial-port-config/serial-port-config.html
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/serial-port-config/serial-port-config.js b/lnbits/extensions/watchonly/static/components/serial-port-config/serial-port-config.js
deleted file mode 100644
index 87b54e38..00000000
--- a/lnbits/extensions/watchonly/static/components/serial-port-config/serial-port-config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-async function serialPortConfig(path) {
- const t = await loadTemplateAsync(path)
- Vue.component('serial-port-config', {
- name: 'serial-port-config',
- props: ['config'],
- template: t,
- data() {
- return {}
- },
- methods: {}
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.html b/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.html
deleted file mode 100644
index a95a1906..00000000
--- a/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.html
+++ /dev/null
@@ -1,544 +0,0 @@
-
-
-
-
-
- Login
- Enter password for Hardware Wallet.
-
-
-
-
-
- Logout
- Clear password for HWW.
-
-
-
-
- Config & Connect
- Set the Serial Port communication parameters.
-
-
-
-
- Paired Device ({{device.config.name || 'no-name'}})
-
- {{device.id}}
-
-
- Forget
-
-
-
-
-
- Disconnect
- Disconnect from Serial Port.
-
-
-
-
-
- Restore
- Restore wallet from existing word list.
-
-
-
-
- Show Seed
- Show seed on the Hardware Wallet display.
-
-
-
-
- Wipe
- Clean-up the wallet. New random seed.
-
-
-
-
- Help
- View available comands.
-
-
-
-
- Console
- Show the serial port communication messages
-
-
-
-
-
-
-
-
- Enter Config
-
-
-
- Connect
- Cancel
-
-
-
-
-
-
-
-
- Enter password for Hardware Wallet (8 numbers/letters)
-
-
-
-
-
-
-
-
-
-
-
-
- Login
- Cancel
-
-
-
-
-
-
-
-
-
-
-
- Output {{hww.confirm.outputIndex}}
-
- change
-
-
-
-
-
- Address:
-
-
- {{tx.outputs[hww.confirm.outputIndex].address}}
-
-
-
-
- Amount:
-
-
- {{satBtc(tx.outputs[hww.confirm.outputIndex].amount)}}
-
-
-
-
- Fee:
-
-
- {{satBtc(tx.feeValue)}}
-
-
-
-
- Fee Rate:
-
-
- {{tx.feeRate}} sats/vbyte
-
-
-
-
-
-
- Confirm then check the Hardware Device.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
- This action will remove all data from the Hardware Wallet. Please
- create a back-up for the seed!
-
- Enter new password for Hardware Wallet (8 numbers/letters)
-
-
-
-
- This action cannot be reversed!
-
-
-
- Wipe
- Cancel
-
-
-
-
-
-
-
-
-
-
- Close
-
-
-
-
-
-
-
-
-
- Open the browser Developer Console for more Details!
-
-
-
-
-
-
- Close
-
-
-
-
-
-
- Check word at position {{hww.seedWordPosition}} on device
-
-
-
-
-
- Prev
-
-
- Next
-
-
- Close
-
-
-
-
-
-
-
-
-
- For test purposes only. Do not enter word list with real funds!!!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Enter new password (8 numbers/letters)
-
-
-
-
-
-
-
-
-
-
- ALL existing data on the Hardware Device will be lost.
-
-
-
- Restore
- Cancel
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.js b/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.js
deleted file mode 100644
index 2e80b1e0..00000000
--- a/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.js
+++ /dev/null
@@ -1,1008 +0,0 @@
-async function serialSigner(path) {
- const t = await loadTemplateAsync(path)
- Vue.component('serial-signer', {
- name: 'serial-signer',
- template: t,
-
- props: ['sats-denominated', 'network'],
- data: function () {
- return {
- selectedPort: null,
- writableStreamClosed: null,
- writer: null,
- readableStreamClosed: null,
- reader: null,
- receivedData: '',
- config: {},
- decryptionKey: null,
- sharedSecret: null,
-
- hww: {
- password: null,
- showPassword: false,
- mnemonic: null,
- showMnemonic: false,
- quickMnemonicInput: false,
- passphrase: null,
- showPassphrase: false,
- hasPassphrase: false,
- authenticated: false,
- showPasswordDialog: false,
- showConfigDialog: false,
- showWipeDialog: false,
- showRestoreDialog: false,
- showConfirmationDialog: false,
- showSignedPsbt: false,
- sendingPsbt: false,
- signingPsbt: false,
- loginResolve: null,
- psbtSentResolve: null,
- xpubResolve: null,
- seedWordPosition: 1,
- seedWord: null,
- showSeedWord: false,
- showSeedDialog: false,
- // config: null,
-
- confirm: {
- outputIndex: 0,
- showFee: false
- }
- },
- tx: null, // todo: move to hww
-
- showConsole: false,
- showPairedDevices: true
- }
- },
-
- computed: {
- pairedDevices: {
- cache: false,
- get: function () {
- return (
- JSON.parse(window.localStorage.getItem('lnbits-paired-devices')) ||
- []
- )
- },
- set: function (devices) {
- window.localStorage.setItem(
- 'lnbits-paired-devices',
- JSON.stringify(devices)
- )
- }
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
- openSerialPortDialog: async function () {
- this.config = {...HWW_DEFAULT_CONFIG}
- await this.openSerialPort(this.config)
- },
- openSerialPort: async function (config = {baudRate: 9600}) {
- if (!this.checkSerialPortSupported()) return false
- if (this.selectedPort) {
- this.$q.notify({
- type: 'warning',
- message: 'Already connected. Disconnect first!',
- timeout: 10000
- })
- return true
- }
-
- try {
- this.selectedPort = await navigator.serial.requestPort()
- this.selectedPort.addEventListener('connect', event => {
- // do nothing
- })
-
- this.selectedPort.addEventListener('disconnect', () => {
- this.selectedPort = null
- this.hww.authenticated = false
- this.$q.notify({
- type: 'warning',
- message: 'Disconnected from Serial Port!',
- timeout: 10000
- })
- })
-
- // Wait for the serial port to open.
- await this.selectedPort.open(config)
- // do not await
- this.startSerialPortReading()
- // wait to init
- sleep(1000)
-
- const textEncoder = new TextEncoderStream()
- this.writableStreamClosed = textEncoder.readable.pipeTo(
- this.selectedPort.writable
- )
-
- this.writer = textEncoder.writable.getWriter()
-
- await this.hwwPing()
-
- return true
- } catch (error) {
- this.selectedPort = null
- this.$q.notify({
- type: 'warning',
- message: 'Cannot open serial port!',
- caption: `${error}`,
- timeout: 10000
- })
- return false
- }
- },
- openSerialPortConfig: async function (deviceId) {
- const device = this.getPairedDevice(deviceId)
- if (device) {
- this.config = device.config
- } else {
- this.config = {...HWW_DEFAULT_CONFIG}
- }
- this.hww.showConfigDialog = true
- },
- closeSerialPort: async function () {
- try {
- if (this.writer) this.writer.close()
- if (this.writableStreamClosed) await this.writableStreamClosed
- if (this.reader) this.reader.cancel()
- if (this.readableStreamClosed)
- await this.readableStreamClosed.catch(() => {
- /* Ignore the error */
- })
- if (this.selectedPort) await this.selectedPort.close()
- this.$q.notify({
- type: 'positive',
- message: 'Serial port disconnected!',
- timeout: 5000
- })
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Cannot close serial port!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.selectedPort = null
- this.hww.authenticated = false
- }
- },
-
- isConnected: function () {
- return !!this.selectedPort
- },
- isAuthenticated: function () {
- return this.hww.authenticated
- },
-
- seedInputDone: function (mnemonic) {
- this.hww.mnemonic = mnemonic
- },
- isAuthenticating: function () {
- if (this.isAuthenticated()) return false
- return new Promise(resolve => {
- this.loginResolve = resolve
- })
- },
-
- isSendingPsbt: async function () {
- if (!this.hww.sendingPsbt) return false
- return new Promise(resolve => {
- this.psbtSentResolve = resolve
- })
- },
-
- isFetchingXpub: async function () {
- return new Promise(resolve => {
- this.xpubResolve = resolve
- })
- },
-
- checkSerialPortSupported: function () {
- if (!navigator.serial) {
- this.$q.notify({
- type: 'warning',
- message: 'Serial port communication not supported!',
- caption:
- 'Make sure your browser supports Serial Port and that you are using HTTPS.',
- timeout: 10000
- })
- return false
- }
- return true
- },
- startSerialPortReading: async function () {
- const port = this.selectedPort
-
- while (port && port.readable) {
- const textDecoder = new TextDecoderStream()
- this.readableStreamClosed = port.readable.pipeTo(textDecoder.writable)
- this.reader = textDecoder.readable.getReader()
- const readStringUntil = readFromSerialPort(this.reader)
-
- try {
- while (true) {
- const {value, done} = await readStringUntil('\n')
- if (value) {
- const {command, commandData} = await this.extractCommand(value)
- this.handleSerialPortResponse(command, commandData)
- this.updateSerialPortConsole(command)
- }
- if (done) return
- }
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Serial port communication error!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- }
- },
- handleSerialPortResponse: async function (command, commandData) {
- this.logPublicCommandsResponse(command, commandData)
-
- switch (command) {
- case COMMAND_PING:
- this.handlePingResponse(commandData)
- break
- case COMMAND_CHECK_PAIRING:
- this.handleCheckPairingResponse(commandData)
- break
- case COMMAND_SIGN_PSBT:
- this.handleSignResponse(commandData)
- break
- case COMMAND_PASSWORD:
- this.handleLoginResponse(commandData)
- break
- case COMMAND_PASSWORD_CLEAR:
- this.handleLogoutResponse(commandData)
- break
- case COMMAND_SEND_PSBT:
- this.handleSendPsbtResponse(commandData)
- break
- case COMMAND_WIPE:
- this.handleWipeResponse(commandData)
- break
- case COMMAND_XPUB:
- this.handleXpubResponse(commandData)
- break
- case COMMAND_SEED:
- this.handleShowSeedResponse(commandData)
- break
- case COMMAND_PAIR:
- this.handlePairResponse(commandData)
- break
- case COMMAND_LOG:
- console.log(
- ` %c${commandData}`,
- 'background: #222; color: #bada55'
- )
- break
- default:
- console.log(` %c${command}`, 'background: #222; color: red')
- }
- },
- logPublicCommandsResponse: function (command, commandData) {
- switch (command) {
- case COMMAND_SIGN_PSBT:
- case COMMAND_PASSWORD:
- case COMMAND_PASSWORD_CLEAR:
- case COMMAND_SEND_PSBT:
- case COMMAND_WIPE:
- case COMMAND_XPUB:
- case COMMAND_PAIR:
- console.log(
- ` %c${command} ${commandData}`,
- 'background: #222; color: yellow'
- )
- }
- },
- updateSerialPortConsole: function (value) {
- this.receivedData += value + '\n'
- const textArea = document.getElementById('serial-port-console')
- if (textArea) textArea.scrollTop = textArea.scrollHeight
- },
- hwwPing: async function () {
- try {
- // Send an empty ping. The serial port buffer might have some jubk data. Flush it.
- await this.sendCommandClearText(COMMAND_PING)
- await this.sendCommandClearText(COMMAND_PING, [window.location.host])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to ping Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handlePingResponse: function (res = '') {
- const [status, deviceId] = res.split(' ')
- this.deviceId = deviceId
-
- if (!this.deviceId) {
- this.$q.notify({
- type: 'warning',
- message: 'Missing device ID for Hardware Wallet',
- timeout: 10000
- })
- return
- }
-
- const device = this.getPairedDevice(deviceId)
-
- if (device) {
- this.sharedSecret = nobleSecp256k1.utils.hexToBytes(
- device.sharedSecretHex
- )
- this.hwwCheckPairing()
- } else {
- this.hwwPair()
- }
- },
- hwwShowPasswordDialog: async function () {
- try {
- this.hww.showPasswordDialog = true
- await this.sendCommandSecure(COMMAND_PASSWORD)
- } catch (error) {
- console.log(error)
- this.$q.notify({
- type: 'warning',
- message: 'Failed to connect to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwShowWipeDialog: async function () {
- try {
- this.hww.showWipeDialog = true
- await this.sendCommandSecure(COMMAND_WIPE)
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to connect to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwShowRestoreDialog: async function () {
- try {
- this.hww.showRestoreDialog = true
- await this.sendCommandSecure(COMMAND_RESTORE)
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to connect to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- closeSeedDialog: function () {
- this.hww.seedWord = null
- this.hww.showSeedWord = false
- },
- hwwConfirmNext: async function () {
- this.hww.confirm.outputIndex += 1
- if (this.hww.confirm.outputIndex >= this.tx.outputs.length) {
- this.hww.confirm.showFee = true
- }
- await this.sendCommandSecure(COMMAND_CONFIRM_NEXT)
- },
- cancelOperation: async function () {
- try {
- await this.sendCommandSecure(COMMAND_CANCEL)
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to send cancel!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwConfigAndConnect: async function () {
- this.hww.showConfigDialog = false
- if (this.config.deviceId) {
- this.updatePairedDeviceConfig(this.config.deviceId, this.config)
- }
- await this.openSerialPort(this.config)
- return true
- },
- hwwLogin: async function () {
- try {
- await this.sendCommandSecure(COMMAND_PASSWORD, [
- this.hww.password,
- this.hww.passphrase
- ])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to send password to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.hww.showPasswordDialog = false
- this.hww.password = null
- this.hww.passphrase = null
- this.hww.showPassword = false
- this.hww.showPassphrase = false
- }
- },
- handleLoginResponse: function (res = '') {
- this.hww.authenticated = res.trim() === '1'
- if (this.loginResolve) {
- this.loginResolve(this.hww.authenticated)
- }
-
- if (this.hww.authenticated) {
- this.$q.notify({
- type: 'positive',
- message: 'Login successfull!',
- timeout: 10000
- })
- } else {
- this.$q.notify({
- type: 'warning',
- message: 'Wrong password, try again!',
- timeout: 10000
- })
- }
- },
- hwwLogout: async function () {
- try {
- await this.sendCommandSecure(COMMAND_PASSWORD_CLEAR)
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to logout from Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwShowAddress: async function (path, address) {
- try {
- await this.sendCommandSecure(COMMAND_ADDRESS, [
- this.network,
- path,
- address
- ])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to logout from Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleLogoutResponse: function (res = '') {
- const authenticated = !(res.trim() === '1')
- if (this.hww.authenticated && !authenticated) {
- this.$q.notify({
- type: 'positive',
- message: 'Logged Out',
- timeout: 10000
- })
- }
- this.hww.authenticated = authenticated
- },
- hwwSendPsbt: async function (psbtBase64, tx) {
- try {
- this.tx = tx
- this.hww.sendingPsbt = true
- await this.sendCommandSecure(COMMAND_SEND_PSBT, [
- this.network,
- psbtBase64
- ])
- this.$q.notify({
- type: 'positive',
- message: 'Data sent to serial port device!',
- timeout: 5000
- })
- } catch (error) {
- this.hww.sendingPsbt = false
- this.$q.notify({
- type: 'warning',
- message: 'Failed to send data to serial port!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleSendPsbtResponse: function (res = '') {
- try {
- const psbtOK = res.trim() === '1'
- if (!psbtOK) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to send PSBT!',
- caption: `${res}`,
- timeout: 10000
- })
- return
- }
- this.hww.confirm.outputIndex = 0
- this.hww.showConfirmationDialog = true
- this.hww.confirm = {
- outputIndex: 0,
- showFee: false
- }
- this.hww.sendingPsbt = false
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to send PSBT!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.psbtSentResolve()
- }
- },
- hwwSignPsbt: async function () {
- try {
- this.hww.showConfirmationDialog = false
- this.hww.signingPsbt = true
- await this.sendCommandSecure(COMMAND_SIGN_PSBT)
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to sign PSBT!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleSignResponse: function (res = '') {
- this.hww.signingPsbt = false
- const [count, psbt] = res.trim().split(' ')
- if (!psbt || !count || count.trim() === '0') {
- this.$q.notify({
- type: 'warning',
- message: 'No input signed!',
- caption: 'Are you using the right seed?',
- timeout: 10000
- })
- return
- }
- this.updateSignedPsbt(psbt)
- this.$q.notify({
- type: 'positive',
- message: 'Transaction Signed',
- message: `Inputs signed: ${count}`,
- timeout: 10000
- })
- },
- hwwCheckPairing: async function () {
- const iv = window.crypto.getRandomValues(new Uint8Array(16))
- const encrypted = await this.encryptMessage(
- this.sharedSecret, // todo: revisit
- iv,
- PAIRING_CONTROL_TEXT.length + ' ' + PAIRING_CONTROL_TEXT
- )
-
- const encryptedHex = nobleSecp256k1.utils.bytesToHex(encrypted)
- const encryptedIvHex = nobleSecp256k1.utils.bytesToHex(iv)
- try {
- await this.sendCommandClearText(COMMAND_CHECK_PAIRING, [
- encryptedHex + encryptedIvHex
- ])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to check secure connection!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleCheckPairingResponse: async function (res = '') {
- const [statusCode, message] = res.split(' ')
- switch (statusCode) {
- case '0':
- const controlText = await this.decryptData(message)
- if (controlText == PAIRING_CONTROL_TEXT) {
- this.$q.notify({
- type: 'positive',
- message: 'Re-paired with success!',
- timeout: 10000
- })
- } else {
- this.$q.notify({
- type: 'warning',
- message: 'Re-pairing failed!',
- caption: 'Remove (forget) device and try again!',
- timeout: 10000
- })
- }
- break
- case '1':
- this.closeSerialPort()
- this.$q.notify({
- type: 'warning',
- message:
- 'Re-pairing failed. Remove (forget) device and try again!',
- caption: `Error: ${message}`,
- timeout: 10000
- })
- break
- default:
- // noting to do here yet
- break
- }
- },
- hwwPair: async function () {
- try {
- this.decryptionKey = nobleSecp256k1.utils.randomPrivateKey()
- const publicKey = nobleSecp256k1.Point.fromPrivateKey(
- this.decryptionKey
- )
- const publicKeyHex = publicKey.toHex().slice(2)
-
- const args = [publicKeyHex]
- if (Number.isInteger(+this.config.buttonOnePin)) {
- args.push(this.config.buttonOnePin)
- }
- if (Number.isInteger(+this.config.buttonTwoPin)) {
- args.push(this.config.buttonTwoPin)
- }
- await this.sendCommandClearText(COMMAND_PAIR, args)
- this.$q.notify({
- type: 'positive',
- message: 'Pairing started!',
- timeout: 5000
- })
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to pair with device!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handlePairResponse: async function (res = '') {
- const [statusCode, data] = res.trim().split(' ')
- let pubKeyHex, errorMessage, captionMessage
- switch (statusCode) {
- case '0':
- pubKeyHex = data
- if (!data) errorMessage = 'Failed to exchange DH secret!'
- break
- case '1':
- errorMessage =
- 'Device pairing only possible in the first 10 seconds after start-up!'
- captionMessage = 'Restart and try again'
- break
-
- default:
- errorMessage = 'Unexpected error code'
- break
- }
-
- if (errorMessage) {
- this.$q.notify({
- type: 'warning',
- message: errorMessage,
- caption: captionMessage || '',
- timeout: 10000
- })
- this.closeSerialPort()
- return
- }
- const hwwPublicKey = nobleSecp256k1.Point.fromHex('04' + pubKeyHex)
-
- this.sharedSecret = nobleSecp256k1
- .getSharedSecret(this.decryptionKey, hwwPublicKey)
- .slice(1, 33)
-
- const sharedSecretHex = nobleSecp256k1.utils.bytesToHex(
- this.sharedSecret
- )
- const sharedSecredHash = await nobleSecp256k1.utils.sha256(
- asciiToUint8Array(sharedSecretHex)
- )
- const fingerprint = nobleSecp256k1.utils
- .bytesToHex(sharedSecredHash)
- .substring(0, 5)
- .toUpperCase()
-
- LNbits.utils
- .confirmDialog('Confirm code from display: ' + fingerprint)
- .onOk(() => {
- this.addPairedDevice(
- this.deviceId,
- nobleSecp256k1.utils.bytesToHex(this.sharedSecret),
- this.config
- )
-
- this.$q.notify({
- type: 'positive',
- message: 'Paired with device!',
- timeout: 5000
- })
- })
- .onCancel(() => {
- this.closeSerialPort()
- })
- },
- hwwHelp: async function () {
- try {
- await this.sendCommandSecure(COMMAND_HELP)
- this.$q.notify({
- type: 'positive',
- message: 'Check display or console for details!',
- timeout: 5000
- })
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to ask for help!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwWipe: async function () {
- try {
- this.hww.showWipeDialog = false
- await this.sendCommandSecure(COMMAND_WIPE, [this.hww.password])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to wipe!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.hww.password = null
- this.hww.confirmedPassword = null
- this.hww.showPassword = false
- }
- },
- handleWipeResponse: function (res = '') {
- const wiped = res.trim() === '1'
- if (wiped) {
- this.$q.notify({
- type: 'positive',
- message: 'Wallet wiped!',
- timeout: 10000
- })
- } else {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to wipe wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwXpub: async function (path) {
- try {
- await this.sendCommandSecure(COMMAND_XPUB, [this.network, path])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to fetch XPub!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleXpubResponse: function (res = '') {
- const args = res.trim().split(' ')
- if (args.length < 3 || args[0].trim() !== '1') {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to fetch XPub!',
- caption: `${res}`,
- timeout: 10000
- })
- this.xpubResolve({})
- return
- }
- const xpub = args[1].trim()
- const fingerprint = args[2].trim()
- this.xpubResolve({xpub, fingerprint})
- },
-
- hwwShowSeed: async function () {
- try {
- this.hww.showSeedDialog = true
- this.hww.seedWordPosition = 1
-
- await this.sendCommandSecure(COMMAND_SEED, [
- this.hww.seedWordPosition
- ])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to show seed!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- showNextSeedWord: async function () {
- this.hww.seedWordPosition++
- await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition])
- },
- showPrevSeedWord: async function () {
- this.hww.seedWordPosition = Math.max(1, this.hww.seedWordPosition - 1)
- await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition])
- },
- handleShowSeedResponse: function (res = '') {
- const [pos, word] = res.trim().split(' ')
- this.hww.seedWord = `${pos}. ${word}`
- this.hww.seedWordPosition = pos
- },
- hwwRestore: async function () {
- try {
- await this.sendCommandSecure(COMMAND_RESTORE, [
- this.hww.password,
- this.hww.mnemonic
- ])
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to restore from seed!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.hww.showRestoreDialog = false
- this.hww.mnemonic = null
- this.hww.showMnemonic = false
- this.hww.password = null
- this.hww.confirmedPassword = null
- this.hww.showPassword = false
- }
- },
-
- updateSignedPsbt: async function (value) {
- this.$emit('signed:psbt', value)
- },
-
- sendCommandSecure: async function (command, attrs = []) {
- const message = [command].concat(attrs).join(' ')
- const iv = window.crypto.getRandomValues(new Uint8Array(16))
- if (!this.sharedSecret || !this.sharedSecret.length) {
- throw new Error(
- `Secure connection not estabileshed. Tried to run command: ${command}`
- )
- }
- const encrypted = await this.encryptMessage(
- this.sharedSecret,
- iv,
- message.length + ' ' + message
- )
-
- const encryptedHex = nobleSecp256k1.utils.bytesToHex(encrypted)
- const encryptedIvHex = nobleSecp256k1.utils.bytesToHex(iv)
- await this.writer.write(encryptedHex + encryptedIvHex + '\n')
- },
- sendCommandClearText: async function (command, attrs = []) {
- const message = [command].concat(attrs).join(' ')
- await this.writer.write(message + '\n')
- },
- extractCommand: async function (value) {
- const command = value.split(' ')[0]
- const commandData = value.substring(command.length).trim()
-
- if (
- command === COMMAND_PAIR ||
- command === COMMAND_LOG ||
- command === COMMAND_PASSWORD_CLEAR ||
- command === COMMAND_PING ||
- command === COMMAND_CHECK_PAIRING
- )
- return {command, commandData}
-
- const decryptedValue = await this.decryptData(value)
- const decryptedCommand = decryptedValue.split(' ')[0]
- const decryptedCommandData = decryptedValue
- .substring(decryptedCommand.length)
- .trim()
- return {
- command: decryptedCommand,
- commandData: decryptedCommandData
- }
- },
- decryptData: async function (value) {
- if (!this.sharedSecret) {
- console.log('/error Secure session not established!')
- return '/error Secure session not established!'
- }
- try {
- const ivSize = 32
- const messageHex = value.substring(0, value.length - ivSize)
- const ivHex = value.substring(value.length - ivSize)
- const messageBytes = nobleSecp256k1.utils.hexToBytes(messageHex)
- const iv = nobleSecp256k1.utils.hexToBytes(ivHex)
- const decrypted1 = await this.decryptMessage(
- this.sharedSecret,
- iv,
- messageBytes
- )
- const data = new TextDecoder().decode(decrypted1)
- const [len] = data.split(' ')
- const command = data
- .substring(len.length + 1, +len + len.length + 1)
- .trim()
- return command
- } catch (error) {
- console.log('/error Failed to decrypt message from device!')
- return '/error Failed to decrypt message from device!'
- }
- },
- encryptMessage: async function (key, iv, message) {
- while (message.length % 16 !== 0) message += ' '
- const encodedMessage = asciiToUint8Array(message)
-
- const aesCbc = new aesjs.ModeOfOperation.cbc(key, iv)
- const encryptedBytes = aesCbc.encrypt(encodedMessage)
-
- return encryptedBytes
- },
- decryptMessage: async function (key, iv, encryptedBytes) {
- const aesCbc = new aesjs.ModeOfOperation.cbc(key, iv)
- const decryptedBytes = aesCbc.decrypt(encryptedBytes)
- return decryptedBytes
- },
-
- getPairedDevice: function (deviceId) {
- return this.pairedDevices.find(d => d.id === deviceId)
- },
- removePairedDevice: function (deviceId) {
- const devices = this.pairedDevices
- const deviceIndex = devices.findIndex(d => d.id === deviceId)
- if (deviceIndex !== -1) {
- devices.splice(deviceIndex, 1)
- }
- this.pairedDevices = devices
- this.showPairedDevices = false
- setTimeout(() => {
- // force UI refresh
- this.showPairedDevices = true
- })
- },
- addPairedDevice: function (deviceId, sharedSecretHex, config) {
- const devices = this.pairedDevices
- config.deviceId = deviceId
- devices.unshift({
- id: deviceId,
- sharedSecretHex: sharedSecretHex,
- pairingDate: new Date().toISOString(),
- config
- })
- this.pairedDevices = devices
- this.showPairedDevices = false
- setTimeout(() => {
- // force UI refresh
- this.showPairedDevices = true
- })
- },
- updatePairedDeviceConfig(deviceId, config) {
- const device = this.getPairedDevice(deviceId)
- if (device) {
- this.removePairedDevice(deviceId)
- this.addPairedDevice(deviceId, device.sharedSecretHex, config)
- }
- }
- },
- created: async function () {}
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/utxo-list/utxo-list.html b/lnbits/extensions/watchonly/static/components/utxo-list/utxo-list.html
deleted file mode 100644
index cb1b930b..00000000
--- a/lnbits/extensions/watchonly/static/components/utxo-list/utxo-list.html
+++ /dev/null
@@ -1,144 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Confirmed
-
-
- Pending
-
-
-
-
-
-
-
-
- {{satBtc(props.row.amount)}}
-
-
- {{ props.row.date }}
-
- {{getWalletName(props.row.wallet)}}
-
-
-
-
-
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/utxo-list/utxo-list.js b/lnbits/extensions/watchonly/static/components/utxo-list/utxo-list.js
deleted file mode 100644
index 6741ed94..00000000
--- a/lnbits/extensions/watchonly/static/components/utxo-list/utxo-list.js
+++ /dev/null
@@ -1,148 +0,0 @@
-async function utxoList(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('utxo-list', {
- name: 'utxo-list',
- template,
-
- props: [
- 'utxos',
- 'accounts',
- 'selectable',
- 'payed-amount',
- 'sats-denominated',
- 'mempool-endpoint',
- 'filter'
- ],
-
- data: function () {
- return {
- utxosTable: {
- columns: [
- {
- name: 'expand',
- align: 'left',
- label: ''
- },
- {
- name: 'selected',
- align: 'left',
- label: '',
- selectable: true
- },
- {
- name: 'status',
- align: 'center',
- label: 'Status',
- sortable: true
- },
- {
- name: 'address',
- align: 'left',
- label: 'Address',
- field: 'address',
- sortable: true
- },
- {
- name: 'amount',
- align: 'left',
- label: 'Amount',
- field: 'amount',
- sortable: true
- },
- {
- name: 'date',
- align: 'left',
- label: 'Date',
- field: 'date',
- sortable: true
- },
- {
- name: 'wallet',
- align: 'left',
- label: 'Account',
- field: 'wallet',
- sortable: true
- }
- ],
- pagination: {
- rowsPerPage: 10
- }
- },
- utxoSelectionModes: [
- 'Manual',
- 'Random',
- 'Select All',
- 'Smaller Inputs First',
- 'Larger Inputs First'
- ],
- utxoSelectionMode: 'Random',
- utxoSelectAmount: 0
- }
- },
-
- computed: {
- columns: function () {
- return this.utxosTable.columns.filter(c =>
- c.selectable ? this.selectable : true
- )
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
- getWalletName: function (walletId) {
- const wallet = (this.accounts || []).find(wl => wl.id === walletId)
- return wallet ? wallet.title : 'unknown'
- },
- getTotalSelectedUtxoAmount: function () {
- const total = (this.utxos || [])
- .filter(u => u.selected)
- .reduce((t, a) => t + (a.amount || 0), 0)
- return total
- },
- refreshUtxoSelection: function (totalPayedAmount) {
- this.utxoSelectAmount = totalPayedAmount
- this.applyUtxoSelectionMode()
- },
- updateUtxoSelection: function () {
- this.utxoSelectAmount = this.payedAmount
- this.applyUtxoSelectionMode()
- },
- applyUtxoSelectionMode: function () {
- const mode = this.utxoSelectionMode
- const isSelectAll = mode === 'Select All'
- if (isSelectAll) {
- this.utxos.forEach(u => (u.selected = true))
- return
- }
-
- const isManual = mode === 'Manual'
- if (isManual || !this.utxoSelectAmount) return
-
- this.utxos.forEach(u => (u.selected = false))
-
- const isSmallerFirst = mode === 'Smaller Inputs First'
- const isLargerFirst = mode === 'Larger Inputs First'
- let selectedUtxos = this.utxos.slice()
- if (isSmallerFirst || isLargerFirst) {
- const sortFn = isSmallerFirst
- ? (a, b) => a.amount - b.amount
- : (a, b) => b.amount - a.amount
- selectedUtxos.sort(sortFn)
- } else {
- // default to random order
- selectedUtxos = _.shuffle(selectedUtxos)
- }
- selectedUtxos.reduce((total, utxo) => {
- utxo.selected = total < this.utxoSelectAmount
- total += utxo.amount
- return total
- }, 0)
- }
- },
-
- created: async function () {}
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.html b/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.html
deleted file mode 100644
index 748d650d..00000000
--- a/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.html
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update
- Cancel
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.js b/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.js
deleted file mode 100644
index 447dc65c..00000000
--- a/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.js
+++ /dev/null
@@ -1,67 +0,0 @@
-async function walletConfig(path) {
- const t = await loadTemplateAsync(path)
- Vue.component('wallet-config', {
- name: 'wallet-config',
- template: t,
-
- props: ['total', 'config-data', 'adminkey'],
- data: function () {
- return {
- networOptions: ['Mainnet', 'Testnet'],
- internalConfig: {},
- show: false
- }
- },
-
- computed: {
- config: {
- get() {
- return this.internalConfig
- },
- set(value) {
- value.isLoaded = true
- this.internalConfig = JSON.parse(JSON.stringify(value))
- this.$emit(
- 'update:config-data',
- JSON.parse(JSON.stringify(this.internalConfig))
- )
- }
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.config.sats_denominated)
- },
- updateConfig: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'PUT',
- '/watchonly/api/v1/config',
- this.adminkey,
- this.config
- )
- this.show = false
- this.config = data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- getConfig: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/watchonly/api/v1/config',
- this.adminkey
- )
- this.config = data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- }
- },
- created: async function () {
- await this.getConfig()
- }
- })
-}
diff --git a/lnbits/extensions/watchonly/static/components/wallet-list/wallet-list.html b/lnbits/extensions/watchonly/static/components/wallet-list/wallet-list.html
deleted file mode 100644
index b656bdca..00000000
--- a/lnbits/extensions/watchonly/static/components/wallet-list/wallet-list.html
+++ /dev/null
@@ -1,285 +0,0 @@
-
-
-
-
-
-
-
-
-
- New Account
- Enter account Xpub or Descriptor
-
-
-
-
- From Hardware Device
-
- Get Xpub from a Hardware Device
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
-
-
- New Receive Address
-
-
-
-
- {{props.row.title}}
-
-
- {{getAmmountForWallet(props.row.id)}}
-
-
- {{props.row.type}}
-
-
- {{props.row.id}}
-
-
-
-
-
-
-
- New Receive Address
-
-
-
- {{getAccountDescription(props.row.type)}}
-
-
-
-
-
-
Master Pubkey:
-
-
-
-
-
-
-
-
-
-
-
-
XPub:
-
-
-
-
-
-
-
-
-
-
-
-
Last Address Index:
-
- {{props.row.address_no}}
- none
-
-
-
-
-
Fingerprint:
-
{{props.row.fingerprint}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/lnbits/extensions/watchonly/static/components/wallet-list/wallet-list.js b/lnbits/extensions/watchonly/static/components/wallet-list/wallet-list.js
deleted file mode 100644
index 27d40ce9..00000000
--- a/lnbits/extensions/watchonly/static/components/wallet-list/wallet-list.js
+++ /dev/null
@@ -1,315 +0,0 @@
-async function walletList(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('wallet-list', {
- name: 'wallet-list',
- template,
-
- props: [
- 'adminkey',
- 'inkey',
- 'sats-denominated',
- 'addresses',
- 'network',
- 'serial-signer-ref'
- ],
- data: function () {
- return {
- walletAccounts: [],
- address: {},
- showQrCodeDialog: false,
- qrCodeValue: null,
- formDialog: {
- show: false,
-
- addressType: {
- label: 'Segwit (P2WPKH)',
- id: 'wpkh',
- pathMainnet: "m/84'/0'/0'",
- pathTestnet: "m/84'/1'/0'"
- },
- useSerialPort: false,
- data: {
- title: '',
- masterpub: ''
- }
- },
- accountPath: '',
- filter: '',
- showCreating: false,
- addressTypeOptions: [
- {
- label: 'Legacy (P2PKH)',
- id: 'pkh',
- pathMainnet: "m/44'/0'/0'",
- pathTestnet: "m/44'/1'/0'"
- },
- {
- label: 'Segwit (P2WPKH)',
- id: 'wpkh',
- pathMainnet: "m/84'/0'/0'",
- pathTestnet: "m/84'/1'/0'"
- },
- {
- label: 'Wrapped Segwit (P2SH-P2WPKH)',
- id: 'sh',
- pathMainnet: "m/49'/0'/0'",
- pathTestnet: "m/49'/1'/0'"
- },
- {
- label: 'Taproot (P2TR)',
- id: 'tr',
- pathMainnet: "m/86'/0'/0'",
- pathTestnet: "m/86'/1'/0'"
- }
- ],
-
- walletsTable: {
- columns: [
- {
- name: 'new',
- align: 'left',
- label: ''
- },
- {
- name: 'title',
- align: 'left',
- label: 'Title',
- field: 'title'
- },
- {
- name: 'amount',
- align: 'left',
- label: 'Amount'
- },
- {
- name: 'type',
- align: 'left',
- label: 'Type',
- field: 'type'
- },
- {name: 'id', align: 'left', label: 'ID', field: 'id'}
- ],
- pagination: {
- rowsPerPage: 10
- },
- filter: ''
- }
- }
- },
- watch: {
- immediate: true,
- async network(newNet, oldNet) {
- if (newNet !== oldNet) {
- await this.refreshWalletAccounts()
- this.handleAddressTypeChanged(this.addressTypeOptions[1])
- }
- }
- },
-
- methods: {
- satBtc(val, showUnit = true) {
- return satOrBtc(val, showUnit, this.satsDenominated)
- },
-
- addWalletAccount: async function () {
- this.showCreating = true
- const data = _.omit(this.formDialog.data, 'wallet')
- data.network = this.network
- await this.createWalletAccount(data)
- this.showCreating = false
- },
- createWalletAccount: async function (data) {
- try {
- const meta = {accountPath: this.accountPath}
- if (this.formDialog.useSerialPort) {
- const {xpub, fingerprint} = await this.fetchXpubFromHww()
- if (!xpub) return
- meta.xpub = xpub
- const path = this.accountPath.substring(2)
- const outputType = this.formDialog.addressType.id
- if (outputType === 'sh') {
- data.masterpub = `${outputType}(wpkh([${fingerprint}/${path}]${xpub}/{0,1}/*))`
- } else {
- data.masterpub = `${outputType}([${fingerprint}/${path}]${xpub}/{0,1}/*)`
- }
- }
- data.meta = JSON.stringify(meta)
- const response = await LNbits.api.request(
- 'POST',
- '/watchonly/api/v1/wallet',
- this.adminkey,
- data
- )
- this.walletAccounts.push(mapWalletAccount(response.data))
- this.formDialog.show = false
-
- await this.refreshWalletAccounts()
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- fetchXpubFromHww: async function () {
- const error = findAccountPathIssues(this.accountPath)
- if (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Invalid derivation path.',
- caption: error,
- timeout: 10000
- })
- return
- }
- await this.serialSignerRef.hwwXpub(this.accountPath)
- return await this.serialSignerRef.isFetchingXpub()
- },
- deleteWalletAccount: function (walletAccountId) {
- LNbits.utils
- .confirmDialog(
- 'Are you sure you want to delete this watch only wallet?'
- )
- .onOk(async () => {
- try {
- await LNbits.api.request(
- 'DELETE',
- '/watchonly/api/v1/wallet/' + walletAccountId,
- this.adminkey
- )
- this.walletAccounts = _.reject(
- this.walletAccounts,
- function (obj) {
- return obj.id === walletAccountId
- }
- )
- await this.refreshWalletAccounts()
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message:
- 'Error while deleting wallet account. Please try again.',
- timeout: 10000
- })
- }
- })
- },
-
- getWatchOnlyWallets: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- `/watchonly/api/v1/wallet?network=${this.network}`,
- this.inkey
- )
- return data
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to fetch wallets.',
- timeout: 10000
- })
- LNbits.utils.notifyApiError(error)
- }
- return []
- },
- refreshWalletAccounts: async function () {
- this.walletAccounts = []
- const wallets = await this.getWatchOnlyWallets()
- this.walletAccounts = wallets.map(w => mapWalletAccount(w))
- this.$emit('accounts-update', this.walletAccounts)
- },
- getAmmountForWallet: function (walletId) {
- const amount = this.addresses
- .filter(a => a.wallet === walletId)
- .reduce((t, a) => t + a.amount || 0, 0)
- return this.satBtc(amount)
- },
- closeFormDialog: function () {
- this.formDialog.data = {
- is_unique: false
- }
- },
- getAccountDescription: function (accountType) {
- return getAccountDescription(accountType)
- },
- openGetFreshAddressDialog: async function (walletId) {
- const {data} = await LNbits.api.request(
- 'GET',
- `/watchonly/api/v1/address/${walletId}`,
- this.inkey
- )
- const addressData = mapAddressesData(data)
-
- addressData.note = `Shared on ${currentDateTime()}`
- const lastActiveAddress =
- this.addresses
- .filter(
- a =>
- a.wallet === addressData.wallet && !a.isChange && a.hasActivity
- )
- .pop() || {}
- addressData.gapLimitExceeded =
- !addressData.isChange &&
- addressData.addressIndex >
- lastActiveAddress.addressIndex + DEFAULT_RECEIVE_GAP_LIMIT
-
- const wallet = this.walletAccounts.find(w => w.id === walletId) || {}
- wallet.address_no = addressData.addressIndex
- this.$emit('new-receive-address', {addressData, wallet})
- },
- showAddAccountDialog: function () {
- this.formDialog.show = true
- this.formDialog.useSerialPort = false
- },
- getXpubFromDevice: async function () {
- try {
- if (!this.serialSignerRef.isConnected()) {
- this.$q.notify({
- type: 'warning',
- message: 'Please connect to a hardware Device first!',
- timeout: 10000
- })
- return
- }
- if (!this.serialSignerRef.isAuthenticated()) {
- await this.serialSignerRef.hwwShowPasswordDialog()
- const authenticated = await this.serialSignerRef.isAuthenticating()
- if (!authenticated) return
- }
- this.formDialog.show = true
- this.formDialog.useSerialPort = true
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Cannot fetch Xpub!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleAddressTypeChanged: function (value = {}) {
- const addressType =
- this.addressTypeOptions.find(t => t.id === value.id) || {}
- this.accountPath = addressType[`path${this.network}`]
- },
- // todo: bad. base.js not present in custom components
- copyText: function (text, message, position) {
- var notify = this.$q.notify
- Quasar.utils.copyToClipboard(text).then(function () {
- notify({
- message: message || 'Copied to clipboard!',
- position: position || 'bottom'
- })
- })
- },
- openQrCodeDialog: function (qrCodeValue) {
- this.qrCodeValue = qrCodeValue
- this.showQrCodeDialog = true
- }
- },
- created: async function () {
- if (this.inkey) {
- await this.refreshWalletAccounts()
- this.handleAddressTypeChanged(this.addressTypeOptions[1])
- }
- }
- })
-}
diff --git a/lnbits/extensions/watchonly/static/js/bip39-word-list.js b/lnbits/extensions/watchonly/static/js/bip39-word-list.js
deleted file mode 100644
index c0a5eac3..00000000
--- a/lnbits/extensions/watchonly/static/js/bip39-word-list.js
+++ /dev/null
@@ -1,2050 +0,0 @@
-const bip39WordList = Object.freeze([
- 'abandon',
- 'ability',
- 'able',
- 'about',
- 'above',
- 'absent',
- 'absorb',
- 'abstract',
- 'absurd',
- 'abuse',
- 'access',
- 'accident',
- 'account',
- 'accuse',
- 'achieve',
- 'acid',
- 'acoustic',
- 'acquire',
- 'across',
- 'act',
- 'action',
- 'actor',
- 'actress',
- 'actual',
- 'adapt',
- 'add',
- 'addict',
- 'address',
- 'adjust',
- 'admit',
- 'adult',
- 'advance',
- 'advice',
- 'aerobic',
- 'affair',
- 'afford',
- 'afraid',
- 'again',
- 'age',
- 'agent',
- 'agree',
- 'ahead',
- 'aim',
- 'air',
- 'airport',
- 'aisle',
- 'alarm',
- 'album',
- 'alcohol',
- 'alert',
- 'alien',
- 'all',
- 'alley',
- 'allow',
- 'almost',
- 'alone',
- 'alpha',
- 'already',
- 'also',
- 'alter',
- 'always',
- 'amateur',
- 'amazing',
- 'among',
- 'amount',
- 'amused',
- 'analyst',
- 'anchor',
- 'ancient',
- 'anger',
- 'angle',
- 'angry',
- 'animal',
- 'ankle',
- 'announce',
- 'annual',
- 'another',
- 'answer',
- 'antenna',
- 'antique',
- 'anxiety',
- 'any',
- 'apart',
- 'apology',
- 'appear',
- 'apple',
- 'approve',
- 'april',
- 'arch',
- 'arctic',
- 'area',
- 'arena',
- 'argue',
- 'arm',
- 'armed',
- 'armor',
- 'army',
- 'around',
- 'arrange',
- 'arrest',
- 'arrive',
- 'arrow',
- 'art',
- 'artefact',
- 'artist',
- 'artwork',
- 'ask',
- 'aspect',
- 'assault',
- 'asset',
- 'assist',
- 'assume',
- 'asthma',
- 'athlete',
- 'atom',
- 'attack',
- 'attend',
- 'attitude',
- 'attract',
- 'auction',
- 'audit',
- 'august',
- 'aunt',
- 'author',
- 'auto',
- 'autumn',
- 'average',
- 'avocado',
- 'avoid',
- 'awake',
- 'aware',
- 'away',
- 'awesome',
- 'awful',
- 'awkward',
- 'axis',
- 'baby',
- 'bachelor',
- 'bacon',
- 'badge',
- 'bag',
- 'balance',
- 'balcony',
- 'ball',
- 'bamboo',
- 'banana',
- 'banner',
- 'bar',
- 'barely',
- 'bargain',
- 'barrel',
- 'base',
- 'basic',
- 'basket',
- 'battle',
- 'beach',
- 'bean',
- 'beauty',
- 'because',
- 'become',
- 'beef',
- 'before',
- 'begin',
- 'behave',
- 'behind',
- 'believe',
- 'below',
- 'belt',
- 'bench',
- 'benefit',
- 'best',
- 'betray',
- 'better',
- 'between',
- 'beyond',
- 'bicycle',
- 'bid',
- 'bike',
- 'bind',
- 'biology',
- 'bird',
- 'birth',
- 'bitter',
- 'black',
- 'blade',
- 'blame',
- 'blanket',
- 'blast',
- 'bleak',
- 'bless',
- 'blind',
- 'blood',
- 'blossom',
- 'blouse',
- 'blue',
- 'blur',
- 'blush',
- 'board',
- 'boat',
- 'body',
- 'boil',
- 'bomb',
- 'bone',
- 'bonus',
- 'book',
- 'boost',
- 'border',
- 'boring',
- 'borrow',
- 'boss',
- 'bottom',
- 'bounce',
- 'box',
- 'boy',
- 'bracket',
- 'brain',
- 'brand',
- 'brass',
- 'brave',
- 'bread',
- 'breeze',
- 'brick',
- 'bridge',
- 'brief',
- 'bright',
- 'bring',
- 'brisk',
- 'broccoli',
- 'broken',
- 'bronze',
- 'broom',
- 'brother',
- 'brown',
- 'brush',
- 'bubble',
- 'buddy',
- 'budget',
- 'buffalo',
- 'build',
- 'bulb',
- 'bulk',
- 'bullet',
- 'bundle',
- 'bunker',
- 'burden',
- 'burger',
- 'burst',
- 'bus',
- 'business',
- 'busy',
- 'butter',
- 'buyer',
- 'buzz',
- 'cabbage',
- 'cabin',
- 'cable',
- 'cactus',
- 'cage',
- 'cake',
- 'call',
- 'calm',
- 'camera',
- 'camp',
- 'can',
- 'canal',
- 'cancel',
- 'candy',
- 'cannon',
- 'canoe',
- 'canvas',
- 'canyon',
- 'capable',
- 'capital',
- 'captain',
- 'car',
- 'carbon',
- 'card',
- 'cargo',
- 'carpet',
- 'carry',
- 'cart',
- 'case',
- 'cash',
- 'casino',
- 'castle',
- 'casual',
- 'cat',
- 'catalog',
- 'catch',
- 'category',
- 'cattle',
- 'caught',
- 'cause',
- 'caution',
- 'cave',
- 'ceiling',
- 'celery',
- 'cement',
- 'census',
- 'century',
- 'cereal',
- 'certain',
- 'chair',
- 'chalk',
- 'champion',
- 'change',
- 'chaos',
- 'chapter',
- 'charge',
- 'chase',
- 'chat',
- 'cheap',
- 'check',
- 'cheese',
- 'chef',
- 'cherry',
- 'chest',
- 'chicken',
- 'chief',
- 'child',
- 'chimney',
- 'choice',
- 'choose',
- 'chronic',
- 'chuckle',
- 'chunk',
- 'churn',
- 'cigar',
- 'cinnamon',
- 'circle',
- 'citizen',
- 'city',
- 'civil',
- 'claim',
- 'clap',
- 'clarify',
- 'claw',
- 'clay',
- 'clean',
- 'clerk',
- 'clever',
- 'click',
- 'client',
- 'cliff',
- 'climb',
- 'clinic',
- 'clip',
- 'clock',
- 'clog',
- 'close',
- 'cloth',
- 'cloud',
- 'clown',
- 'club',
- 'clump',
- 'cluster',
- 'clutch',
- 'coach',
- 'coast',
- 'coconut',
- 'code',
- 'coffee',
- 'coil',
- 'coin',
- 'collect',
- 'color',
- 'column',
- 'combine',
- 'come',
- 'comfort',
- 'comic',
- 'common',
- 'company',
- 'concert',
- 'conduct',
- 'confirm',
- 'congress',
- 'connect',
- 'consider',
- 'control',
- 'convince',
- 'cook',
- 'cool',
- 'copper',
- 'copy',
- 'coral',
- 'core',
- 'corn',
- 'correct',
- 'cost',
- 'cotton',
- 'couch',
- 'country',
- 'couple',
- 'course',
- 'cousin',
- 'cover',
- 'coyote',
- 'crack',
- 'cradle',
- 'craft',
- 'cram',
- 'crane',
- 'crash',
- 'crater',
- 'crawl',
- 'crazy',
- 'cream',
- 'credit',
- 'creek',
- 'crew',
- 'cricket',
- 'crime',
- 'crisp',
- 'critic',
- 'crop',
- 'cross',
- 'crouch',
- 'crowd',
- 'crucial',
- 'cruel',
- 'cruise',
- 'crumble',
- 'crunch',
- 'crush',
- 'cry',
- 'crystal',
- 'cube',
- 'culture',
- 'cup',
- 'cupboard',
- 'curious',
- 'current',
- 'curtain',
- 'curve',
- 'cushion',
- 'custom',
- 'cute',
- 'cycle',
- 'dad',
- 'damage',
- 'damp',
- 'dance',
- 'danger',
- 'daring',
- 'dash',
- 'daughter',
- 'dawn',
- 'day',
- 'deal',
- 'debate',
- 'debris',
- 'decade',
- 'december',
- 'decide',
- 'decline',
- 'decorate',
- 'decrease',
- 'deer',
- 'defense',
- 'define',
- 'defy',
- 'degree',
- 'delay',
- 'deliver',
- 'demand',
- 'demise',
- 'denial',
- 'dentist',
- 'deny',
- 'depart',
- 'depend',
- 'deposit',
- 'depth',
- 'deputy',
- 'derive',
- 'describe',
- 'desert',
- 'design',
- 'desk',
- 'despair',
- 'destroy',
- 'detail',
- 'detect',
- 'develop',
- 'device',
- 'devote',
- 'diagram',
- 'dial',
- 'diamond',
- 'diary',
- 'dice',
- 'diesel',
- 'diet',
- 'differ',
- 'digital',
- 'dignity',
- 'dilemma',
- 'dinner',
- 'dinosaur',
- 'direct',
- 'dirt',
- 'disagree',
- 'discover',
- 'disease',
- 'dish',
- 'dismiss',
- 'disorder',
- 'display',
- 'distance',
- 'divert',
- 'divide',
- 'divorce',
- 'dizzy',
- 'doctor',
- 'document',
- 'dog',
- 'doll',
- 'dolphin',
- 'domain',
- 'donate',
- 'donkey',
- 'donor',
- 'door',
- 'dose',
- 'double',
- 'dove',
- 'draft',
- 'dragon',
- 'drama',
- 'drastic',
- 'draw',
- 'dream',
- 'dress',
- 'drift',
- 'drill',
- 'drink',
- 'drip',
- 'drive',
- 'drop',
- 'drum',
- 'dry',
- 'duck',
- 'dumb',
- 'dune',
- 'during',
- 'dust',
- 'dutch',
- 'duty',
- 'dwarf',
- 'dynamic',
- 'eager',
- 'eagle',
- 'early',
- 'earn',
- 'earth',
- 'easily',
- 'east',
- 'easy',
- 'echo',
- 'ecology',
- 'economy',
- 'edge',
- 'edit',
- 'educate',
- 'effort',
- 'egg',
- 'eight',
- 'either',
- 'elbow',
- 'elder',
- 'electric',
- 'elegant',
- 'element',
- 'elephant',
- 'elevator',
- 'elite',
- 'else',
- 'embark',
- 'embody',
- 'embrace',
- 'emerge',
- 'emotion',
- 'employ',
- 'empower',
- 'empty',
- 'enable',
- 'enact',
- 'end',
- 'endless',
- 'endorse',
- 'enemy',
- 'energy',
- 'enforce',
- 'engage',
- 'engine',
- 'enhance',
- 'enjoy',
- 'enlist',
- 'enough',
- 'enrich',
- 'enroll',
- 'ensure',
- 'enter',
- 'entire',
- 'entry',
- 'envelope',
- 'episode',
- 'equal',
- 'equip',
- 'era',
- 'erase',
- 'erode',
- 'erosion',
- 'error',
- 'erupt',
- 'escape',
- 'essay',
- 'essence',
- 'estate',
- 'eternal',
- 'ethics',
- 'evidence',
- 'evil',
- 'evoke',
- 'evolve',
- 'exact',
- 'example',
- 'excess',
- 'exchange',
- 'excite',
- 'exclude',
- 'excuse',
- 'execute',
- 'exercise',
- 'exhaust',
- 'exhibit',
- 'exile',
- 'exist',
- 'exit',
- 'exotic',
- 'expand',
- 'expect',
- 'expire',
- 'explain',
- 'expose',
- 'express',
- 'extend',
- 'extra',
- 'eye',
- 'eyebrow',
- 'fabric',
- 'face',
- 'faculty',
- 'fade',
- 'faint',
- 'faith',
- 'fall',
- 'false',
- 'fame',
- 'family',
- 'famous',
- 'fan',
- 'fancy',
- 'fantasy',
- 'farm',
- 'fashion',
- 'fat',
- 'fatal',
- 'father',
- 'fatigue',
- 'fault',
- 'favorite',
- 'feature',
- 'february',
- 'federal',
- 'fee',
- 'feed',
- 'feel',
- 'female',
- 'fence',
- 'festival',
- 'fetch',
- 'fever',
- 'few',
- 'fiber',
- 'fiction',
- 'field',
- 'figure',
- 'file',
- 'film',
- 'filter',
- 'final',
- 'find',
- 'fine',
- 'finger',
- 'finish',
- 'fire',
- 'firm',
- 'first',
- 'fiscal',
- 'fish',
- 'fit',
- 'fitness',
- 'fix',
- 'flag',
- 'flame',
- 'flash',
- 'flat',
- 'flavor',
- 'flee',
- 'flight',
- 'flip',
- 'float',
- 'flock',
- 'floor',
- 'flower',
- 'fluid',
- 'flush',
- 'fly',
- 'foam',
- 'focus',
- 'fog',
- 'foil',
- 'fold',
- 'follow',
- 'food',
- 'foot',
- 'force',
- 'forest',
- 'forget',
- 'fork',
- 'fortune',
- 'forum',
- 'forward',
- 'fossil',
- 'foster',
- 'found',
- 'fox',
- 'fragile',
- 'frame',
- 'frequent',
- 'fresh',
- 'friend',
- 'fringe',
- 'frog',
- 'front',
- 'frost',
- 'frown',
- 'frozen',
- 'fruit',
- 'fuel',
- 'fun',
- 'funny',
- 'furnace',
- 'fury',
- 'future',
- 'gadget',
- 'gain',
- 'galaxy',
- 'gallery',
- 'game',
- 'gap',
- 'garage',
- 'garbage',
- 'garden',
- 'garlic',
- 'garment',
- 'gas',
- 'gasp',
- 'gate',
- 'gather',
- 'gauge',
- 'gaze',
- 'general',
- 'genius',
- 'genre',
- 'gentle',
- 'genuine',
- 'gesture',
- 'ghost',
- 'giant',
- 'gift',
- 'giggle',
- 'ginger',
- 'giraffe',
- 'girl',
- 'give',
- 'glad',
- 'glance',
- 'glare',
- 'glass',
- 'glide',
- 'glimpse',
- 'globe',
- 'gloom',
- 'glory',
- 'glove',
- 'glow',
- 'glue',
- 'goat',
- 'goddess',
- 'gold',
- 'good',
- 'goose',
- 'gorilla',
- 'gospel',
- 'gossip',
- 'govern',
- 'gown',
- 'grab',
- 'grace',
- 'grain',
- 'grant',
- 'grape',
- 'grass',
- 'gravity',
- 'great',
- 'green',
- 'grid',
- 'grief',
- 'grit',
- 'grocery',
- 'group',
- 'grow',
- 'grunt',
- 'guard',
- 'guess',
- 'guide',
- 'guilt',
- 'guitar',
- 'gun',
- 'gym',
- 'habit',
- 'hair',
- 'half',
- 'hammer',
- 'hamster',
- 'hand',
- 'happy',
- 'harbor',
- 'hard',
- 'harsh',
- 'harvest',
- 'hat',
- 'have',
- 'hawk',
- 'hazard',
- 'head',
- 'health',
- 'heart',
- 'heavy',
- 'hedgehog',
- 'height',
- 'hello',
- 'helmet',
- 'help',
- 'hen',
- 'hero',
- 'hidden',
- 'high',
- 'hill',
- 'hint',
- 'hip',
- 'hire',
- 'history',
- 'hobby',
- 'hockey',
- 'hold',
- 'hole',
- 'holiday',
- 'hollow',
- 'home',
- 'honey',
- 'hood',
- 'hope',
- 'horn',
- 'horror',
- 'horse',
- 'hospital',
- 'host',
- 'hotel',
- 'hour',
- 'hover',
- 'hub',
- 'huge',
- 'human',
- 'humble',
- 'humor',
- 'hundred',
- 'hungry',
- 'hunt',
- 'hurdle',
- 'hurry',
- 'hurt',
- 'husband',
- 'hybrid',
- 'ice',
- 'icon',
- 'idea',
- 'identify',
- 'idle',
- 'ignore',
- 'ill',
- 'illegal',
- 'illness',
- 'image',
- 'imitate',
- 'immense',
- 'immune',
- 'impact',
- 'impose',
- 'improve',
- 'impulse',
- 'inch',
- 'include',
- 'income',
- 'increase',
- 'index',
- 'indicate',
- 'indoor',
- 'industry',
- 'infant',
- 'inflict',
- 'inform',
- 'inhale',
- 'inherit',
- 'initial',
- 'inject',
- 'injury',
- 'inmate',
- 'inner',
- 'innocent',
- 'input',
- 'inquiry',
- 'insane',
- 'insect',
- 'inside',
- 'inspire',
- 'install',
- 'intact',
- 'interest',
- 'into',
- 'invest',
- 'invite',
- 'involve',
- 'iron',
- 'island',
- 'isolate',
- 'issue',
- 'item',
- 'ivory',
- 'jacket',
- 'jaguar',
- 'jar',
- 'jazz',
- 'jealous',
- 'jeans',
- 'jelly',
- 'jewel',
- 'job',
- 'join',
- 'joke',
- 'journey',
- 'joy',
- 'judge',
- 'juice',
- 'jump',
- 'jungle',
- 'junior',
- 'junk',
- 'just',
- 'kangaroo',
- 'keen',
- 'keep',
- 'ketchup',
- 'key',
- 'kick',
- 'kid',
- 'kidney',
- 'kind',
- 'kingdom',
- 'kiss',
- 'kit',
- 'kitchen',
- 'kite',
- 'kitten',
- 'kiwi',
- 'knee',
- 'knife',
- 'knock',
- 'know',
- 'lab',
- 'label',
- 'labor',
- 'ladder',
- 'lady',
- 'lake',
- 'lamp',
- 'language',
- 'laptop',
- 'large',
- 'later',
- 'latin',
- 'laugh',
- 'laundry',
- 'lava',
- 'law',
- 'lawn',
- 'lawsuit',
- 'layer',
- 'lazy',
- 'leader',
- 'leaf',
- 'learn',
- 'leave',
- 'lecture',
- 'left',
- 'leg',
- 'legal',
- 'legend',
- 'leisure',
- 'lemon',
- 'lend',
- 'length',
- 'lens',
- 'leopard',
- 'lesson',
- 'letter',
- 'level',
- 'liar',
- 'liberty',
- 'library',
- 'license',
- 'life',
- 'lift',
- 'light',
- 'like',
- 'limb',
- 'limit',
- 'link',
- 'lion',
- 'liquid',
- 'list',
- 'little',
- 'live',
- 'lizard',
- 'load',
- 'loan',
- 'lobster',
- 'local',
- 'lock',
- 'logic',
- 'lonely',
- 'long',
- 'loop',
- 'lottery',
- 'loud',
- 'lounge',
- 'love',
- 'loyal',
- 'lucky',
- 'luggage',
- 'lumber',
- 'lunar',
- 'lunch',
- 'luxury',
- 'lyrics',
- 'machine',
- 'mad',
- 'magic',
- 'magnet',
- 'maid',
- 'mail',
- 'main',
- 'major',
- 'make',
- 'mammal',
- 'man',
- 'manage',
- 'mandate',
- 'mango',
- 'mansion',
- 'manual',
- 'maple',
- 'marble',
- 'march',
- 'margin',
- 'marine',
- 'market',
- 'marriage',
- 'mask',
- 'mass',
- 'master',
- 'match',
- 'material',
- 'math',
- 'matrix',
- 'matter',
- 'maximum',
- 'maze',
- 'meadow',
- 'mean',
- 'measure',
- 'meat',
- 'mechanic',
- 'medal',
- 'media',
- 'melody',
- 'melt',
- 'member',
- 'memory',
- 'mention',
- 'menu',
- 'mercy',
- 'merge',
- 'merit',
- 'merry',
- 'mesh',
- 'message',
- 'metal',
- 'method',
- 'middle',
- 'midnight',
- 'milk',
- 'million',
- 'mimic',
- 'mind',
- 'minimum',
- 'minor',
- 'minute',
- 'miracle',
- 'mirror',
- 'misery',
- 'miss',
- 'mistake',
- 'mix',
- 'mixed',
- 'mixture',
- 'mobile',
- 'model',
- 'modify',
- 'mom',
- 'moment',
- 'monitor',
- 'monkey',
- 'monster',
- 'month',
- 'moon',
- 'moral',
- 'more',
- 'morning',
- 'mosquito',
- 'mother',
- 'motion',
- 'motor',
- 'mountain',
- 'mouse',
- 'move',
- 'movie',
- 'much',
- 'muffin',
- 'mule',
- 'multiply',
- 'muscle',
- 'museum',
- 'mushroom',
- 'music',
- 'must',
- 'mutual',
- 'myself',
- 'mystery',
- 'myth',
- 'naive',
- 'name',
- 'napkin',
- 'narrow',
- 'nasty',
- 'nation',
- 'nature',
- 'near',
- 'neck',
- 'need',
- 'negative',
- 'neglect',
- 'neither',
- 'nephew',
- 'nerve',
- 'nest',
- 'net',
- 'network',
- 'neutral',
- 'never',
- 'news',
- 'next',
- 'nice',
- 'night',
- 'noble',
- 'noise',
- 'nominee',
- 'noodle',
- 'normal',
- 'north',
- 'nose',
- 'notable',
- 'note',
- 'nothing',
- 'notice',
- 'novel',
- 'now',
- 'nuclear',
- 'number',
- 'nurse',
- 'nut',
- 'oak',
- 'obey',
- 'object',
- 'oblige',
- 'obscure',
- 'observe',
- 'obtain',
- 'obvious',
- 'occur',
- 'ocean',
- 'october',
- 'odor',
- 'off',
- 'offer',
- 'office',
- 'often',
- 'oil',
- 'okay',
- 'old',
- 'olive',
- 'olympic',
- 'omit',
- 'once',
- 'one',
- 'onion',
- 'online',
- 'only',
- 'open',
- 'opera',
- 'opinion',
- 'oppose',
- 'option',
- 'orange',
- 'orbit',
- 'orchard',
- 'order',
- 'ordinary',
- 'organ',
- 'orient',
- 'original',
- 'orphan',
- 'ostrich',
- 'other',
- 'outdoor',
- 'outer',
- 'output',
- 'outside',
- 'oval',
- 'oven',
- 'over',
- 'own',
- 'owner',
- 'oxygen',
- 'oyster',
- 'ozone',
- 'pact',
- 'paddle',
- 'page',
- 'pair',
- 'palace',
- 'palm',
- 'panda',
- 'panel',
- 'panic',
- 'panther',
- 'paper',
- 'parade',
- 'parent',
- 'park',
- 'parrot',
- 'party',
- 'pass',
- 'patch',
- 'path',
- 'patient',
- 'patrol',
- 'pattern',
- 'pause',
- 'pave',
- 'payment',
- 'peace',
- 'peanut',
- 'pear',
- 'peasant',
- 'pelican',
- 'pen',
- 'penalty',
- 'pencil',
- 'people',
- 'pepper',
- 'perfect',
- 'permit',
- 'person',
- 'pet',
- 'phone',
- 'photo',
- 'phrase',
- 'physical',
- 'piano',
- 'picnic',
- 'picture',
- 'piece',
- 'pig',
- 'pigeon',
- 'pill',
- 'pilot',
- 'pink',
- 'pioneer',
- 'pipe',
- 'pistol',
- 'pitch',
- 'pizza',
- 'place',
- 'planet',
- 'plastic',
- 'plate',
- 'play',
- 'please',
- 'pledge',
- 'pluck',
- 'plug',
- 'plunge',
- 'poem',
- 'poet',
- 'point',
- 'polar',
- 'pole',
- 'police',
- 'pond',
- 'pony',
- 'pool',
- 'popular',
- 'portion',
- 'position',
- 'possible',
- 'post',
- 'potato',
- 'pottery',
- 'poverty',
- 'powder',
- 'power',
- 'practice',
- 'praise',
- 'predict',
- 'prefer',
- 'prepare',
- 'present',
- 'pretty',
- 'prevent',
- 'price',
- 'pride',
- 'primary',
- 'print',
- 'priority',
- 'prison',
- 'private',
- 'prize',
- 'problem',
- 'process',
- 'produce',
- 'profit',
- 'program',
- 'project',
- 'promote',
- 'proof',
- 'property',
- 'prosper',
- 'protect',
- 'proud',
- 'provide',
- 'public',
- 'pudding',
- 'pull',
- 'pulp',
- 'pulse',
- 'pumpkin',
- 'punch',
- 'pupil',
- 'puppy',
- 'purchase',
- 'purity',
- 'purpose',
- 'purse',
- 'push',
- 'put',
- 'puzzle',
- 'pyramid',
- 'quality',
- 'quantum',
- 'quarter',
- 'question',
- 'quick',
- 'quit',
- 'quiz',
- 'quote',
- 'rabbit',
- 'raccoon',
- 'race',
- 'rack',
- 'radar',
- 'radio',
- 'rail',
- 'rain',
- 'raise',
- 'rally',
- 'ramp',
- 'ranch',
- 'random',
- 'range',
- 'rapid',
- 'rare',
- 'rate',
- 'rather',
- 'raven',
- 'raw',
- 'razor',
- 'ready',
- 'real',
- 'reason',
- 'rebel',
- 'rebuild',
- 'recall',
- 'receive',
- 'recipe',
- 'record',
- 'recycle',
- 'reduce',
- 'reflect',
- 'reform',
- 'refuse',
- 'region',
- 'regret',
- 'regular',
- 'reject',
- 'relax',
- 'release',
- 'relief',
- 'rely',
- 'remain',
- 'remember',
- 'remind',
- 'remove',
- 'render',
- 'renew',
- 'rent',
- 'reopen',
- 'repair',
- 'repeat',
- 'replace',
- 'report',
- 'require',
- 'rescue',
- 'resemble',
- 'resist',
- 'resource',
- 'response',
- 'result',
- 'retire',
- 'retreat',
- 'return',
- 'reunion',
- 'reveal',
- 'review',
- 'reward',
- 'rhythm',
- 'rib',
- 'ribbon',
- 'rice',
- 'rich',
- 'ride',
- 'ridge',
- 'rifle',
- 'right',
- 'rigid',
- 'ring',
- 'riot',
- 'ripple',
- 'risk',
- 'ritual',
- 'rival',
- 'river',
- 'road',
- 'roast',
- 'robot',
- 'robust',
- 'rocket',
- 'romance',
- 'roof',
- 'rookie',
- 'room',
- 'rose',
- 'rotate',
- 'rough',
- 'round',
- 'route',
- 'royal',
- 'rubber',
- 'rude',
- 'rug',
- 'rule',
- 'run',
- 'runway',
- 'rural',
- 'sad',
- 'saddle',
- 'sadness',
- 'safe',
- 'sail',
- 'salad',
- 'salmon',
- 'salon',
- 'salt',
- 'salute',
- 'same',
- 'sample',
- 'sand',
- 'satisfy',
- 'satoshi',
- 'sauce',
- 'sausage',
- 'save',
- 'say',
- 'scale',
- 'scan',
- 'scare',
- 'scatter',
- 'scene',
- 'scheme',
- 'school',
- 'science',
- 'scissors',
- 'scorpion',
- 'scout',
- 'scrap',
- 'screen',
- 'script',
- 'scrub',
- 'sea',
- 'search',
- 'season',
- 'seat',
- 'second',
- 'secret',
- 'section',
- 'security',
- 'seed',
- 'seek',
- 'segment',
- 'select',
- 'sell',
- 'seminar',
- 'senior',
- 'sense',
- 'sentence',
- 'series',
- 'service',
- 'session',
- 'settle',
- 'setup',
- 'seven',
- 'shadow',
- 'shaft',
- 'shallow',
- 'share',
- 'shed',
- 'shell',
- 'sheriff',
- 'shield',
- 'shift',
- 'shine',
- 'ship',
- 'shiver',
- 'shock',
- 'shoe',
- 'shoot',
- 'shop',
- 'short',
- 'shoulder',
- 'shove',
- 'shrimp',
- 'shrug',
- 'shuffle',
- 'shy',
- 'sibling',
- 'sick',
- 'side',
- 'siege',
- 'sight',
- 'sign',
- 'silent',
- 'silk',
- 'silly',
- 'silver',
- 'similar',
- 'simple',
- 'since',
- 'sing',
- 'siren',
- 'sister',
- 'situate',
- 'six',
- 'size',
- 'skate',
- 'sketch',
- 'ski',
- 'skill',
- 'skin',
- 'skirt',
- 'skull',
- 'slab',
- 'slam',
- 'sleep',
- 'slender',
- 'slice',
- 'slide',
- 'slight',
- 'slim',
- 'slogan',
- 'slot',
- 'slow',
- 'slush',
- 'small',
- 'smart',
- 'smile',
- 'smoke',
- 'smooth',
- 'snack',
- 'snake',
- 'snap',
- 'sniff',
- 'snow',
- 'soap',
- 'soccer',
- 'social',
- 'sock',
- 'soda',
- 'soft',
- 'solar',
- 'soldier',
- 'solid',
- 'solution',
- 'solve',
- 'someone',
- 'song',
- 'soon',
- 'sorry',
- 'sort',
- 'soul',
- 'sound',
- 'soup',
- 'source',
- 'south',
- 'space',
- 'spare',
- 'spatial',
- 'spawn',
- 'speak',
- 'special',
- 'speed',
- 'spell',
- 'spend',
- 'sphere',
- 'spice',
- 'spider',
- 'spike',
- 'spin',
- 'spirit',
- 'split',
- 'spoil',
- 'sponsor',
- 'spoon',
- 'sport',
- 'spot',
- 'spray',
- 'spread',
- 'spring',
- 'spy',
- 'square',
- 'squeeze',
- 'squirrel',
- 'stable',
- 'stadium',
- 'staff',
- 'stage',
- 'stairs',
- 'stamp',
- 'stand',
- 'start',
- 'state',
- 'stay',
- 'steak',
- 'steel',
- 'stem',
- 'step',
- 'stereo',
- 'stick',
- 'still',
- 'sting',
- 'stock',
- 'stomach',
- 'stone',
- 'stool',
- 'story',
- 'stove',
- 'strategy',
- 'street',
- 'strike',
- 'strong',
- 'struggle',
- 'student',
- 'stuff',
- 'stumble',
- 'style',
- 'subject',
- 'submit',
- 'subway',
- 'success',
- 'such',
- 'sudden',
- 'suffer',
- 'sugar',
- 'suggest',
- 'suit',
- 'summer',
- 'sun',
- 'sunny',
- 'sunset',
- 'super',
- 'supply',
- 'supreme',
- 'sure',
- 'surface',
- 'surge',
- 'surprise',
- 'surround',
- 'survey',
- 'suspect',
- 'sustain',
- 'swallow',
- 'swamp',
- 'swap',
- 'swarm',
- 'swear',
- 'sweet',
- 'swift',
- 'swim',
- 'swing',
- 'switch',
- 'sword',
- 'symbol',
- 'symptom',
- 'syrup',
- 'system',
- 'table',
- 'tackle',
- 'tag',
- 'tail',
- 'talent',
- 'talk',
- 'tank',
- 'tape',
- 'target',
- 'task',
- 'taste',
- 'tattoo',
- 'taxi',
- 'teach',
- 'team',
- 'tell',
- 'ten',
- 'tenant',
- 'tennis',
- 'tent',
- 'term',
- 'test',
- 'text',
- 'thank',
- 'that',
- 'theme',
- 'then',
- 'theory',
- 'there',
- 'they',
- 'thing',
- 'this',
- 'thought',
- 'three',
- 'thrive',
- 'throw',
- 'thumb',
- 'thunder',
- 'ticket',
- 'tide',
- 'tiger',
- 'tilt',
- 'timber',
- 'time',
- 'tiny',
- 'tip',
- 'tired',
- 'tissue',
- 'title',
- 'toast',
- 'tobacco',
- 'today',
- 'toddler',
- 'toe',
- 'together',
- 'toilet',
- 'token',
- 'tomato',
- 'tomorrow',
- 'tone',
- 'tongue',
- 'tonight',
- 'tool',
- 'tooth',
- 'top',
- 'topic',
- 'topple',
- 'torch',
- 'tornado',
- 'tortoise',
- 'toss',
- 'total',
- 'tourist',
- 'toward',
- 'tower',
- 'town',
- 'toy',
- 'track',
- 'trade',
- 'traffic',
- 'tragic',
- 'train',
- 'transfer',
- 'trap',
- 'trash',
- 'travel',
- 'tray',
- 'treat',
- 'tree',
- 'trend',
- 'trial',
- 'tribe',
- 'trick',
- 'trigger',
- 'trim',
- 'trip',
- 'trophy',
- 'trouble',
- 'truck',
- 'true',
- 'truly',
- 'trumpet',
- 'trust',
- 'truth',
- 'try',
- 'tube',
- 'tuition',
- 'tumble',
- 'tuna',
- 'tunnel',
- 'turkey',
- 'turn',
- 'turtle',
- 'twelve',
- 'twenty',
- 'twice',
- 'twin',
- 'twist',
- 'two',
- 'type',
- 'typical',
- 'ugly',
- 'umbrella',
- 'unable',
- 'unaware',
- 'uncle',
- 'uncover',
- 'under',
- 'undo',
- 'unfair',
- 'unfold',
- 'unhappy',
- 'uniform',
- 'unique',
- 'unit',
- 'universe',
- 'unknown',
- 'unlock',
- 'until',
- 'unusual',
- 'unveil',
- 'update',
- 'upgrade',
- 'uphold',
- 'upon',
- 'upper',
- 'upset',
- 'urban',
- 'urge',
- 'usage',
- 'use',
- 'used',
- 'useful',
- 'useless',
- 'usual',
- 'utility',
- 'vacant',
- 'vacuum',
- 'vague',
- 'valid',
- 'valley',
- 'valve',
- 'van',
- 'vanish',
- 'vapor',
- 'various',
- 'vast',
- 'vault',
- 'vehicle',
- 'velvet',
- 'vendor',
- 'venture',
- 'venue',
- 'verb',
- 'verify',
- 'version',
- 'very',
- 'vessel',
- 'veteran',
- 'viable',
- 'vibrant',
- 'vicious',
- 'victory',
- 'video',
- 'view',
- 'village',
- 'vintage',
- 'violin',
- 'virtual',
- 'virus',
- 'visa',
- 'visit',
- 'visual',
- 'vital',
- 'vivid',
- 'vocal',
- 'voice',
- 'void',
- 'volcano',
- 'volume',
- 'vote',
- 'voyage',
- 'wage',
- 'wagon',
- 'wait',
- 'walk',
- 'wall',
- 'walnut',
- 'want',
- 'warfare',
- 'warm',
- 'warrior',
- 'wash',
- 'wasp',
- 'waste',
- 'water',
- 'wave',
- 'way',
- 'wealth',
- 'weapon',
- 'wear',
- 'weasel',
- 'weather',
- 'web',
- 'wedding',
- 'weekend',
- 'weird',
- 'welcome',
- 'west',
- 'wet',
- 'whale',
- 'what',
- 'wheat',
- 'wheel',
- 'when',
- 'where',
- 'whip',
- 'whisper',
- 'wide',
- 'width',
- 'wife',
- 'wild',
- 'will',
- 'win',
- 'window',
- 'wine',
- 'wing',
- 'wink',
- 'winner',
- 'winter',
- 'wire',
- 'wisdom',
- 'wise',
- 'wish',
- 'witness',
- 'wolf',
- 'woman',
- 'wonder',
- 'wood',
- 'wool',
- 'word',
- 'work',
- 'world',
- 'worry',
- 'worth',
- 'wrap',
- 'wreck',
- 'wrestle',
- 'wrist',
- 'write',
- 'wrong',
- 'yard',
- 'year',
- 'yellow',
- 'you',
- 'young',
- 'youth',
- 'zebra',
- 'zero',
- 'zone',
- 'zoo'
-])
diff --git a/lnbits/extensions/watchonly/static/js/crypto/aes.js b/lnbits/extensions/watchonly/static/js/crypto/aes.js
deleted file mode 100644
index 92a17ca2..00000000
--- a/lnbits/extensions/watchonly/static/js/crypto/aes.js
+++ /dev/null
@@ -1,802 +0,0 @@
-/*! MIT License. Copyright 2015-2018 Richard Moore . See LICENSE.txt. */
-(function(root) {
- "use strict";
-
- function checkInt(value) {
- return (parseInt(value) === value);
- }
-
- function checkInts(arrayish) {
- if (!checkInt(arrayish.length)) { return false; }
-
- for (var i = 0; i < arrayish.length; i++) {
- if (!checkInt(arrayish[i]) || arrayish[i] < 0 || arrayish[i] > 255) {
- return false;
- }
- }
-
- return true;
- }
-
- function coerceArray(arg, copy) {
-
- // ArrayBuffer view
- if (arg.buffer && arg.name === 'Uint8Array') {
-
- if (copy) {
- if (arg.slice) {
- arg = arg.slice();
- } else {
- arg = Array.prototype.slice.call(arg);
- }
- }
-
- return arg;
- }
-
- // It's an array; check it is a valid representation of a byte
- if (Array.isArray(arg)) {
- if (!checkInts(arg)) {
- throw new Error('Array contains invalid value: ' + arg);
- }
-
- return new Uint8Array(arg);
- }
-
- // Something else, but behaves like an array (maybe a Buffer? Arguments?)
- if (checkInt(arg.length) && checkInts(arg)) {
- return new Uint8Array(arg);
- }
- throw new Error('unsupported array-like object');
- }
-
- function createArray(length) {
- return new Uint8Array(length);
- }
-
- function copyArray(sourceArray, targetArray, targetStart, sourceStart, sourceEnd) {
- if (sourceStart != null || sourceEnd != null) {
- if (sourceArray.slice) {
- sourceArray = sourceArray.slice(sourceStart, sourceEnd);
- } else {
- sourceArray = Array.prototype.slice.call(sourceArray, sourceStart, sourceEnd);
- }
- }
- targetArray.set(sourceArray, targetStart);
- }
-
-
-
- var convertUtf8 = (function() {
- function toBytes(text) {
- var result = [], i = 0;
- text = encodeURI(text);
- while (i < text.length) {
- var c = text.charCodeAt(i++);
-
- // if it is a % sign, encode the following 2 bytes as a hex value
- if (c === 37) {
- result.push(parseInt(text.substr(i, 2), 16))
- i += 2;
-
- // otherwise, just the actual byte
- } else {
- result.push(c)
- }
- }
-
- return coerceArray(result);
- }
-
- function fromBytes(bytes) {
- var result = [], i = 0;
-
- while (i < bytes.length) {
- var c = bytes[i];
-
- if (c < 128) {
- result.push(String.fromCharCode(c));
- i++;
- } else if (c > 191 && c < 224) {
- result.push(String.fromCharCode(((c & 0x1f) << 6) | (bytes[i + 1] & 0x3f)));
- i += 2;
- } else {
- result.push(String.fromCharCode(((c & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)));
- i += 3;
- }
- }
-
- return result.join('');
- }
-
- return {
- toBytes: toBytes,
- fromBytes: fromBytes,
- }
- })();
-
- var convertHex = (function() {
- function toBytes(text) {
- var result = [];
- for (var i = 0; i < text.length; i += 2) {
- result.push(parseInt(text.substr(i, 2), 16));
- }
-
- return result;
- }
-
- // http://ixti.net/development/javascript/2011/11/11/base64-encodedecode-of-utf8-in-browser-with-js.html
- var Hex = '0123456789abcdef';
-
- function fromBytes(bytes) {
- var result = [];
- for (var i = 0; i < bytes.length; i++) {
- var v = bytes[i];
- result.push(Hex[(v & 0xf0) >> 4] + Hex[v & 0x0f]);
- }
- return result.join('');
- }
-
- return {
- toBytes: toBytes,
- fromBytes: fromBytes,
- }
- })();
-
-
- // Number of rounds by keysize
- var numberOfRounds = {16: 10, 24: 12, 32: 14}
-
- // Round constant words
- var rcon = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91];
-
- // S-box and Inverse S-box (S is for Substitution)
- var S = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16];
- var Si =[0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d];
-
- // Transformations for encryption
- var T1 = [0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a];
- var T2 = [0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616];
- var T3 = [0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16];
- var T4 = [0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c];
-
- // Transformations for decryption
- var T5 = [0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742];
- var T6 = [0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857];
- var T7 = [0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8];
- var T8 = [0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0];
-
- // Transformations for decryption key expansion
- var U1 = [0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3];
- var U2 = [0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697];
- var U3 = [0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46];
- var U4 = [0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d];
-
- function convertToInt32(bytes) {
- var result = [];
- for (var i = 0; i < bytes.length; i += 4) {
- result.push(
- (bytes[i ] << 24) |
- (bytes[i + 1] << 16) |
- (bytes[i + 2] << 8) |
- bytes[i + 3]
- );
- }
- return result;
- }
-
- var AES = function(key) {
- if (!(this instanceof AES)) {
- throw Error('AES must be instanitated with `new`');
- }
-
- Object.defineProperty(this, 'key', {
- value: coerceArray(key, true)
- });
-
- this._prepare();
- }
-
-
- AES.prototype._prepare = function() {
-
- var rounds = numberOfRounds[this.key.length];
- if (rounds == null) {
- throw new Error('invalid key size (must be 16, 24 or 32 bytes)');
- }
-
- // encryption round keys
- this._Ke = [];
-
- // decryption round keys
- this._Kd = [];
-
- for (var i = 0; i <= rounds; i++) {
- this._Ke.push([0, 0, 0, 0]);
- this._Kd.push([0, 0, 0, 0]);
- }
-
- var roundKeyCount = (rounds + 1) * 4;
- var KC = this.key.length / 4;
-
- // convert the key into ints
- var tk = convertToInt32(this.key);
-
- // copy values into round key arrays
- var index;
- for (var i = 0; i < KC; i++) {
- index = i >> 2;
- this._Ke[index][i % 4] = tk[i];
- this._Kd[rounds - index][i % 4] = tk[i];
- }
-
- // key expansion (fips-197 section 5.2)
- var rconpointer = 0;
- var t = KC, tt;
- while (t < roundKeyCount) {
- tt = tk[KC - 1];
- tk[0] ^= ((S[(tt >> 16) & 0xFF] << 24) ^
- (S[(tt >> 8) & 0xFF] << 16) ^
- (S[ tt & 0xFF] << 8) ^
- S[(tt >> 24) & 0xFF] ^
- (rcon[rconpointer] << 24));
- rconpointer += 1;
-
- // key expansion (for non-256 bit)
- if (KC != 8) {
- for (var i = 1; i < KC; i++) {
- tk[i] ^= tk[i - 1];
- }
-
- // key expansion for 256-bit keys is "slightly different" (fips-197)
- } else {
- for (var i = 1; i < (KC / 2); i++) {
- tk[i] ^= tk[i - 1];
- }
- tt = tk[(KC / 2) - 1];
-
- tk[KC / 2] ^= (S[ tt & 0xFF] ^
- (S[(tt >> 8) & 0xFF] << 8) ^
- (S[(tt >> 16) & 0xFF] << 16) ^
- (S[(tt >> 24) & 0xFF] << 24));
-
- for (var i = (KC / 2) + 1; i < KC; i++) {
- tk[i] ^= tk[i - 1];
- }
- }
-
- // copy values into round key arrays
- var i = 0, r, c;
- while (i < KC && t < roundKeyCount) {
- r = t >> 2;
- c = t % 4;
- this._Ke[r][c] = tk[i];
- this._Kd[rounds - r][c] = tk[i++];
- t++;
- }
- }
-
- // inverse-cipher-ify the decryption round key (fips-197 section 5.3)
- for (var r = 1; r < rounds; r++) {
- for (var c = 0; c < 4; c++) {
- tt = this._Kd[r][c];
- this._Kd[r][c] = (U1[(tt >> 24) & 0xFF] ^
- U2[(tt >> 16) & 0xFF] ^
- U3[(tt >> 8) & 0xFF] ^
- U4[ tt & 0xFF]);
- }
- }
- }
-
- AES.prototype.encrypt = function(plaintext) {
- if (plaintext.length != 16) {
- throw new Error('invalid plaintext size (must be 16 bytes)');
- }
-
- var rounds = this._Ke.length - 1;
- var a = [0, 0, 0, 0];
-
- // convert plaintext to (ints ^ key)
- var t = convertToInt32(plaintext);
- for (var i = 0; i < 4; i++) {
- t[i] ^= this._Ke[0][i];
- }
-
- // apply round transforms
- for (var r = 1; r < rounds; r++) {
- for (var i = 0; i < 4; i++) {
- a[i] = (T1[(t[ i ] >> 24) & 0xff] ^
- T2[(t[(i + 1) % 4] >> 16) & 0xff] ^
- T3[(t[(i + 2) % 4] >> 8) & 0xff] ^
- T4[ t[(i + 3) % 4] & 0xff] ^
- this._Ke[r][i]);
- }
- t = a.slice();
- }
-
- // the last round is special
- var result = createArray(16), tt;
- for (var i = 0; i < 4; i++) {
- tt = this._Ke[rounds][i];
- result[4 * i ] = (S[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
- result[4 * i + 1] = (S[(t[(i + 1) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
- result[4 * i + 2] = (S[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
- result[4 * i + 3] = (S[ t[(i + 3) % 4] & 0xff] ^ tt ) & 0xff;
- }
-
- return result;
- }
-
- AES.prototype.decrypt = function(ciphertext) {
- if (ciphertext.length != 16) {
- throw new Error('invalid ciphertext size (must be 16 bytes)');
- }
-
- var rounds = this._Kd.length - 1;
- var a = [0, 0, 0, 0];
-
- // convert plaintext to (ints ^ key)
- var t = convertToInt32(ciphertext);
- for (var i = 0; i < 4; i++) {
- t[i] ^= this._Kd[0][i];
- }
-
- // apply round transforms
- for (var r = 1; r < rounds; r++) {
- for (var i = 0; i < 4; i++) {
- a[i] = (T5[(t[ i ] >> 24) & 0xff] ^
- T6[(t[(i + 3) % 4] >> 16) & 0xff] ^
- T7[(t[(i + 2) % 4] >> 8) & 0xff] ^
- T8[ t[(i + 1) % 4] & 0xff] ^
- this._Kd[r][i]);
- }
- t = a.slice();
- }
-
- // the last round is special
- var result = createArray(16), tt;
- for (var i = 0; i < 4; i++) {
- tt = this._Kd[rounds][i];
- result[4 * i ] = (Si[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
- result[4 * i + 1] = (Si[(t[(i + 3) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
- result[4 * i + 2] = (Si[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
- result[4 * i + 3] = (Si[ t[(i + 1) % 4] & 0xff] ^ tt ) & 0xff;
- }
-
- return result;
- }
-
-
- /**
- * Mode Of Operation - Electonic Codebook (ECB)
- */
- var ModeOfOperationECB = function(key) {
- if (!(this instanceof ModeOfOperationECB)) {
- throw Error('AES must be instanitated with `new`');
- }
-
- this.description = "Electronic Code Block";
- this.name = "ecb";
-
- this._aes = new AES(key);
- }
-
- ModeOfOperationECB.prototype.encrypt = function(plaintext) {
- plaintext = coerceArray(plaintext);
-
- if ((plaintext.length % 16) !== 0) {
- throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
- }
-
- var ciphertext = createArray(plaintext.length);
- var block = createArray(16);
-
- for (var i = 0; i < plaintext.length; i += 16) {
- copyArray(plaintext, block, 0, i, i + 16);
- block = this._aes.encrypt(block);
- copyArray(block, ciphertext, i);
- }
-
- return ciphertext;
- }
-
- ModeOfOperationECB.prototype.decrypt = function(ciphertext) {
- ciphertext = coerceArray(ciphertext);
-
- if ((ciphertext.length % 16) !== 0) {
- throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
- }
-
- var plaintext = createArray(ciphertext.length);
- var block = createArray(16);
-
- for (var i = 0; i < ciphertext.length; i += 16) {
- copyArray(ciphertext, block, 0, i, i + 16);
- block = this._aes.decrypt(block);
- copyArray(block, plaintext, i);
- }
-
- return plaintext;
- }
-
-
- /**
- * Mode Of Operation - Cipher Block Chaining (CBC)
- */
- var ModeOfOperationCBC = function(key, iv) {
- if (!(this instanceof ModeOfOperationCBC)) {
- throw Error('AES must be instanitated with `new`');
- }
-
- this.description = "Cipher Block Chaining";
- this.name = "cbc";
-
- if (!iv) {
- iv = createArray(16);
-
- } else if (iv.length != 16) {
- throw new Error('invalid initialation vector size (must be 16 bytes)');
- }
-
- this._lastCipherblock = coerceArray(iv, true);
-
- this._aes = new AES(key);
- }
-
- ModeOfOperationCBC.prototype.encrypt = function(plaintext) {
- plaintext = coerceArray(plaintext);
-
- if ((plaintext.length % 16) !== 0) {
- throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
- }
-
- var ciphertext = createArray(plaintext.length);
- var block = createArray(16);
-
- for (var i = 0; i < plaintext.length; i += 16) {
- copyArray(plaintext, block, 0, i, i + 16);
-
- for (var j = 0; j < 16; j++) {
- block[j] ^= this._lastCipherblock[j];
- }
-
- this._lastCipherblock = this._aes.encrypt(block);
- copyArray(this._lastCipherblock, ciphertext, i);
- }
-
- return ciphertext;
- }
-
- ModeOfOperationCBC.prototype.decrypt = function(ciphertext) {
- ciphertext = coerceArray(ciphertext);
-
- if ((ciphertext.length % 16) !== 0) {
- throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
- }
-
- var plaintext = createArray(ciphertext.length);
- var block = createArray(16);
-
- for (var i = 0; i < ciphertext.length; i += 16) {
- copyArray(ciphertext, block, 0, i, i + 16);
- block = this._aes.decrypt(block);
-
- for (var j = 0; j < 16; j++) {
- plaintext[i + j] = block[j] ^ this._lastCipherblock[j];
- }
-
- copyArray(ciphertext, this._lastCipherblock, 0, i, i + 16);
- }
-
- return plaintext;
- }
-
-
- /**
- * Mode Of Operation - Cipher Feedback (CFB)
- */
- var ModeOfOperationCFB = function(key, iv, segmentSize) {
- if (!(this instanceof ModeOfOperationCFB)) {
- throw Error('AES must be instanitated with `new`');
- }
-
- this.description = "Cipher Feedback";
- this.name = "cfb";
-
- if (!iv) {
- iv = createArray(16);
-
- } else if (iv.length != 16) {
- throw new Error('invalid initialation vector size (must be 16 size)');
- }
-
- if (!segmentSize) { segmentSize = 1; }
-
- this.segmentSize = segmentSize;
-
- this._shiftRegister = coerceArray(iv, true);
-
- this._aes = new AES(key);
- }
-
- ModeOfOperationCFB.prototype.encrypt = function(plaintext) {
- if ((plaintext.length % this.segmentSize) != 0) {
- throw new Error('invalid plaintext size (must be segmentSize bytes)');
- }
-
- var encrypted = coerceArray(plaintext, true);
-
- var xorSegment;
- for (var i = 0; i < encrypted.length; i += this.segmentSize) {
- xorSegment = this._aes.encrypt(this._shiftRegister);
- for (var j = 0; j < this.segmentSize; j++) {
- encrypted[i + j] ^= xorSegment[j];
- }
-
- // Shift the register
- copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
- copyArray(encrypted, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
- }
-
- return encrypted;
- }
-
- ModeOfOperationCFB.prototype.decrypt = function(ciphertext) {
- if ((ciphertext.length % this.segmentSize) != 0) {
- throw new Error('invalid ciphertext size (must be segmentSize bytes)');
- }
-
- var plaintext = coerceArray(ciphertext, true);
-
- var xorSegment;
- for (var i = 0; i < plaintext.length; i += this.segmentSize) {
- xorSegment = this._aes.encrypt(this._shiftRegister);
-
- for (var j = 0; j < this.segmentSize; j++) {
- plaintext[i + j] ^= xorSegment[j];
- }
-
- // Shift the register
- copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
- copyArray(ciphertext, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
- }
-
- return plaintext;
- }
-
- /**
- * Mode Of Operation - Output Feedback (OFB)
- */
- var ModeOfOperationOFB = function(key, iv) {
- if (!(this instanceof ModeOfOperationOFB)) {
- throw Error('AES must be instanitated with `new`');
- }
-
- this.description = "Output Feedback";
- this.name = "ofb";
-
- if (!iv) {
- iv = createArray(16);
-
- } else if (iv.length != 16) {
- throw new Error('invalid initialation vector size (must be 16 bytes)');
- }
-
- this._lastPrecipher = coerceArray(iv, true);
- this._lastPrecipherIndex = 16;
-
- this._aes = new AES(key);
- }
-
- ModeOfOperationOFB.prototype.encrypt = function(plaintext) {
- var encrypted = coerceArray(plaintext, true);
-
- for (var i = 0; i < encrypted.length; i++) {
- if (this._lastPrecipherIndex === 16) {
- this._lastPrecipher = this._aes.encrypt(this._lastPrecipher);
- this._lastPrecipherIndex = 0;
- }
- encrypted[i] ^= this._lastPrecipher[this._lastPrecipherIndex++];
- }
-
- return encrypted;
- }
-
- // Decryption is symetric
- ModeOfOperationOFB.prototype.decrypt = ModeOfOperationOFB.prototype.encrypt;
-
-
- /**
- * Counter object for CTR common mode of operation
- */
- var Counter = function(initialValue) {
- if (!(this instanceof Counter)) {
- throw Error('Counter must be instanitated with `new`');
- }
-
- // We allow 0, but anything false-ish uses the default 1
- if (initialValue !== 0 && !initialValue) { initialValue = 1; }
-
- if (typeof(initialValue) === 'number') {
- this._counter = createArray(16);
- this.setValue(initialValue);
-
- } else {
- this.setBytes(initialValue);
- }
- }
-
- Counter.prototype.setValue = function(value) {
- if (typeof(value) !== 'number' || parseInt(value) != value) {
- throw new Error('invalid counter value (must be an integer)');
- }
-
- // We cannot safely handle numbers beyond the safe range for integers
- if (value > Number.MAX_SAFE_INTEGER) {
- throw new Error('integer value out of safe range');
- }
-
- for (var index = 15; index >= 0; --index) {
- this._counter[index] = value % 256;
- value = parseInt(value / 256);
- }
- }
-
- Counter.prototype.setBytes = function(bytes) {
- bytes = coerceArray(bytes, true);
-
- if (bytes.length != 16) {
- throw new Error('invalid counter bytes size (must be 16 bytes)');
- }
-
- this._counter = bytes;
- };
-
- Counter.prototype.increment = function() {
- for (var i = 15; i >= 0; i--) {
- if (this._counter[i] === 255) {
- this._counter[i] = 0;
- } else {
- this._counter[i]++;
- break;
- }
- }
- }
-
-
- /**
- * Mode Of Operation - Counter (CTR)
- */
- var ModeOfOperationCTR = function(key, counter) {
- if (!(this instanceof ModeOfOperationCTR)) {
- throw Error('AES must be instanitated with `new`');
- }
-
- this.description = "Counter";
- this.name = "ctr";
-
- if (!(counter instanceof Counter)) {
- counter = new Counter(counter)
- }
-
- this._counter = counter;
-
- this._remainingCounter = null;
- this._remainingCounterIndex = 16;
-
- this._aes = new AES(key);
- }
-
- ModeOfOperationCTR.prototype.encrypt = function(plaintext) {
- var encrypted = coerceArray(plaintext, true);
-
- for (var i = 0; i < encrypted.length; i++) {
- if (this._remainingCounterIndex === 16) {
- this._remainingCounter = this._aes.encrypt(this._counter._counter);
- this._remainingCounterIndex = 0;
- this._counter.increment();
- }
- encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++];
- }
-
- return encrypted;
- }
-
- // Decryption is symetric
- ModeOfOperationCTR.prototype.decrypt = ModeOfOperationCTR.prototype.encrypt;
-
-
- ///////////////////////
- // Padding
-
- // See:https://tools.ietf.org/html/rfc2315
- function pkcs7pad(data) {
- data = coerceArray(data, true);
- var padder = 16 - (data.length % 16);
- var result = createArray(data.length + padder);
- copyArray(data, result);
- for (var i = data.length; i < result.length; i++) {
- result[i] = padder;
- }
- return result;
- }
-
- function pkcs7strip(data) {
- data = coerceArray(data, true);
- if (data.length < 16) { throw new Error('PKCS#7 invalid length'); }
-
- var padder = data[data.length - 1];
- if (padder > 16) { throw new Error('PKCS#7 padding byte out of range'); }
-
- var length = data.length - padder;
- for (var i = 0; i < padder; i++) {
- if (data[length + i] !== padder) {
- throw new Error('PKCS#7 invalid padding byte');
- }
- }
-
- var result = createArray(length);
- copyArray(data, result, 0, 0, length);
- return result;
- }
-
- ///////////////////////
- // Exporting
-
-
- // The block cipher
- var aesjs = {
- AES: AES,
- Counter: Counter,
-
- ModeOfOperation: {
- ecb: ModeOfOperationECB,
- cbc: ModeOfOperationCBC,
- cfb: ModeOfOperationCFB,
- ofb: ModeOfOperationOFB,
- ctr: ModeOfOperationCTR
- },
-
- utils: {
- hex: convertHex,
- utf8: convertUtf8
- },
-
- padding: {
- pkcs7: {
- pad: pkcs7pad,
- strip: pkcs7strip
- }
- },
-
- _arrayTest: {
- coerceArray: coerceArray,
- createArray: createArray,
- copyArray: copyArray,
- }
- };
-
-
- // node.js
- if (typeof exports !== 'undefined') {
- module.exports = aesjs
-
- // RequireJS/AMD
- // http://www.requirejs.org/docs/api.html
- // https://github.com/amdjs/amdjs-api/wiki/AMD
- } else if (typeof(define) === 'function' && define.amd) {
- define([], function() { return aesjs; });
-
- // Web Browsers
- } else {
-
- // If there was an existing library at "aesjs" make sure it's still available
- if (root.aesjs) {
- aesjs._aesjs = root.aesjs;
- }
-
- root.aesjs = aesjs;
- }
-
-
-})(this);
\ No newline at end of file
diff --git a/lnbits/extensions/watchonly/static/js/crypto/noble-secp256k1.js b/lnbits/extensions/watchonly/static/js/crypto/noble-secp256k1.js
deleted file mode 100644
index 8be86729..00000000
--- a/lnbits/extensions/watchonly/static/js/crypto/noble-secp256k1.js
+++ /dev/null
@@ -1,1177 +0,0 @@
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.nobleSecp256k1 = {}));
-})(this, (function (exports) { 'use strict';
-
- const _nodeResolve_empty = {};
-
- const nodeCrypto = /*#__PURE__*/Object.freeze({
- __proto__: null,
- 'default': _nodeResolve_empty
- });
-
- /*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */
- const _0n = BigInt(0);
- const _1n = BigInt(1);
- const _2n = BigInt(2);
- const _3n = BigInt(3);
- const _8n = BigInt(8);
- const POW_2_256 = _2n ** BigInt(256);
- const CURVE = {
- a: _0n,
- b: BigInt(7),
- P: POW_2_256 - _2n ** BigInt(32) - BigInt(977),
- n: POW_2_256 - BigInt('432420386565659656852420866394968145599'),
- h: _1n,
- Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
- Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'),
- beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'),
- };
- function weistrass(x) {
- const { a, b } = CURVE;
- const x2 = mod(x * x);
- const x3 = mod(x2 * x);
- return mod(x3 + a * x + b);
- }
- const USE_ENDOMORPHISM = CURVE.a === _0n;
- class JacobianPoint {
- constructor(x, y, z) {
- this.x = x;
- this.y = y;
- this.z = z;
- }
- static fromAffine(p) {
- if (!(p instanceof Point)) {
- throw new TypeError('JacobianPoint#fromAffine: expected Point');
- }
- return new JacobianPoint(p.x, p.y, _1n);
- }
- static toAffineBatch(points) {
- const toInv = invertBatch(points.map((p) => p.z));
- return points.map((p, i) => p.toAffine(toInv[i]));
- }
- static normalizeZ(points) {
- return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine);
- }
- equals(other) {
- if (!(other instanceof JacobianPoint))
- throw new TypeError('JacobianPoint expected');
- const { x: X1, y: Y1, z: Z1 } = this;
- const { x: X2, y: Y2, z: Z2 } = other;
- const Z1Z1 = mod(Z1 ** _2n);
- const Z2Z2 = mod(Z2 ** _2n);
- const U1 = mod(X1 * Z2Z2);
- const U2 = mod(X2 * Z1Z1);
- const S1 = mod(mod(Y1 * Z2) * Z2Z2);
- const S2 = mod(mod(Y2 * Z1) * Z1Z1);
- return U1 === U2 && S1 === S2;
- }
- negate() {
- return new JacobianPoint(this.x, mod(-this.y), this.z);
- }
- double() {
- const { x: X1, y: Y1, z: Z1 } = this;
- const A = mod(X1 ** _2n);
- const B = mod(Y1 ** _2n);
- const C = mod(B ** _2n);
- const D = mod(_2n * (mod((X1 + B) ** _2n) - A - C));
- const E = mod(_3n * A);
- const F = mod(E ** _2n);
- const X3 = mod(F - _2n * D);
- const Y3 = mod(E * (D - X3) - _8n * C);
- const Z3 = mod(_2n * Y1 * Z1);
- return new JacobianPoint(X3, Y3, Z3);
- }
- add(other) {
- if (!(other instanceof JacobianPoint))
- throw new TypeError('JacobianPoint expected');
- const { x: X1, y: Y1, z: Z1 } = this;
- const { x: X2, y: Y2, z: Z2 } = other;
- if (X2 === _0n || Y2 === _0n)
- return this;
- if (X1 === _0n || Y1 === _0n)
- return other;
- const Z1Z1 = mod(Z1 ** _2n);
- const Z2Z2 = mod(Z2 ** _2n);
- const U1 = mod(X1 * Z2Z2);
- const U2 = mod(X2 * Z1Z1);
- const S1 = mod(mod(Y1 * Z2) * Z2Z2);
- const S2 = mod(mod(Y2 * Z1) * Z1Z1);
- const H = mod(U2 - U1);
- const r = mod(S2 - S1);
- if (H === _0n) {
- if (r === _0n) {
- return this.double();
- }
- else {
- return JacobianPoint.ZERO;
- }
- }
- const HH = mod(H ** _2n);
- const HHH = mod(H * HH);
- const V = mod(U1 * HH);
- const X3 = mod(r ** _2n - HHH - _2n * V);
- const Y3 = mod(r * (V - X3) - S1 * HHH);
- const Z3 = mod(Z1 * Z2 * H);
- return new JacobianPoint(X3, Y3, Z3);
- }
- subtract(other) {
- return this.add(other.negate());
- }
- multiplyUnsafe(scalar) {
- const P0 = JacobianPoint.ZERO;
- if (typeof scalar === 'bigint' && scalar === _0n)
- return P0;
- let n = normalizeScalar(scalar);
- if (n === _1n)
- return this;
- if (!USE_ENDOMORPHISM) {
- let p = P0;
- let d = this;
- while (n > _0n) {
- if (n & _1n)
- p = p.add(d);
- d = d.double();
- n >>= _1n;
- }
- return p;
- }
- let { k1neg, k1, k2neg, k2 } = splitScalarEndo(n);
- let k1p = P0;
- let k2p = P0;
- let d = this;
- while (k1 > _0n || k2 > _0n) {
- if (k1 & _1n)
- k1p = k1p.add(d);
- if (k2 & _1n)
- k2p = k2p.add(d);
- d = d.double();
- k1 >>= _1n;
- k2 >>= _1n;
- }
- if (k1neg)
- k1p = k1p.negate();
- if (k2neg)
- k2p = k2p.negate();
- k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z);
- return k1p.add(k2p);
- }
- precomputeWindow(W) {
- const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1;
- const points = [];
- let p = this;
- let base = p;
- for (let window = 0; window < windows; window++) {
- base = p;
- points.push(base);
- for (let i = 1; i < 2 ** (W - 1); i++) {
- base = base.add(p);
- points.push(base);
- }
- p = base.double();
- }
- return points;
- }
- wNAF(n, affinePoint) {
- if (!affinePoint && this.equals(JacobianPoint.BASE))
- affinePoint = Point.BASE;
- const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1;
- if (256 % W) {
- throw new Error('Point#wNAF: Invalid precomputation window, must be power of 2');
- }
- let precomputes = affinePoint && pointPrecomputes.get(affinePoint);
- if (!precomputes) {
- precomputes = this.precomputeWindow(W);
- if (affinePoint && W !== 1) {
- precomputes = JacobianPoint.normalizeZ(precomputes);
- pointPrecomputes.set(affinePoint, precomputes);
- }
- }
- let p = JacobianPoint.ZERO;
- let f = JacobianPoint.ZERO;
- const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W);
- const windowSize = 2 ** (W - 1);
- const mask = BigInt(2 ** W - 1);
- const maxNumber = 2 ** W;
- const shiftBy = BigInt(W);
- for (let window = 0; window < windows; window++) {
- const offset = window * windowSize;
- let wbits = Number(n & mask);
- n >>= shiftBy;
- if (wbits > windowSize) {
- wbits -= maxNumber;
- n += _1n;
- }
- if (wbits === 0) {
- let pr = precomputes[offset];
- if (window % 2)
- pr = pr.negate();
- f = f.add(pr);
- }
- else {
- let cached = precomputes[offset + Math.abs(wbits) - 1];
- if (wbits < 0)
- cached = cached.negate();
- p = p.add(cached);
- }
- }
- return { p, f };
- }
- multiply(scalar, affinePoint) {
- let n = normalizeScalar(scalar);
- let point;
- let fake;
- if (USE_ENDOMORPHISM) {
- const { k1neg, k1, k2neg, k2 } = splitScalarEndo(n);
- let { p: k1p, f: f1p } = this.wNAF(k1, affinePoint);
- let { p: k2p, f: f2p } = this.wNAF(k2, affinePoint);
- if (k1neg)
- k1p = k1p.negate();
- if (k2neg)
- k2p = k2p.negate();
- k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z);
- point = k1p.add(k2p);
- fake = f1p.add(f2p);
- }
- else {
- const { p, f } = this.wNAF(n, affinePoint);
- point = p;
- fake = f;
- }
- return JacobianPoint.normalizeZ([point, fake])[0];
- }
- toAffine(invZ = invert(this.z)) {
- const { x, y, z } = this;
- const iz1 = invZ;
- const iz2 = mod(iz1 * iz1);
- const iz3 = mod(iz2 * iz1);
- const ax = mod(x * iz2);
- const ay = mod(y * iz3);
- const zz = mod(z * iz1);
- if (zz !== _1n)
- throw new Error('invZ was invalid');
- return new Point(ax, ay);
- }
- }
- JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n);
- JacobianPoint.ZERO = new JacobianPoint(_0n, _1n, _0n);
- const pointPrecomputes = new WeakMap();
- class Point {
- constructor(x, y) {
- this.x = x;
- this.y = y;
- }
- _setWindowSize(windowSize) {
- this._WINDOW_SIZE = windowSize;
- pointPrecomputes.delete(this);
- }
- static fromCompressedHex(bytes) {
- const isShort = bytes.length === 32;
- const x = bytesToNumber(isShort ? bytes : bytes.subarray(1));
- if (!isValidFieldElement(x))
- throw new Error('Point is not on curve');
- const y2 = weistrass(x);
- let y = sqrtMod(y2);
- const isYOdd = (y & _1n) === _1n;
- if (isShort) {
- if (isYOdd)
- y = mod(-y);
- }
- else {
- const isFirstByteOdd = (bytes[0] & 1) === 1;
- if (isFirstByteOdd !== isYOdd)
- y = mod(-y);
- }
- const point = new Point(x, y);
- point.assertValidity();
- return point;
- }
- static fromUncompressedHex(bytes) {
- const x = bytesToNumber(bytes.subarray(1, 33));
- const y = bytesToNumber(bytes.subarray(33, 65));
- const point = new Point(x, y);
- point.assertValidity();
- return point;
- }
- static fromHex(hex) {
- const bytes = ensureBytes(hex);
- const len = bytes.length;
- const header = bytes[0];
- if (len === 32 || (len === 33 && (header === 0x02 || header === 0x03))) {
- return this.fromCompressedHex(bytes);
- }
- if (len === 65 && header === 0x04)
- return this.fromUncompressedHex(bytes);
- throw new Error(`Point.fromHex: received invalid point. Expected 32-33 compressed bytes or 65 uncompressed bytes, not ${len}`);
- }
- static fromPrivateKey(privateKey) {
- return Point.BASE.multiply(normalizePrivateKey(privateKey));
- }
- static fromSignature(msgHash, signature, recovery) {
- msgHash = ensureBytes(msgHash);
- const h = truncateHash(msgHash);
- const { r, s } = normalizeSignature(signature);
- if (recovery !== 0 && recovery !== 1) {
- throw new Error('Cannot recover signature: invalid recovery bit');
- }
- const prefix = recovery & 1 ? '03' : '02';
- const R = Point.fromHex(prefix + numTo32bStr(r));
- const { n } = CURVE;
- const rinv = invert(r, n);
- const u1 = mod(-h * rinv, n);
- const u2 = mod(s * rinv, n);
- const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2);
- if (!Q)
- throw new Error('Cannot recover signature: point at infinify');
- Q.assertValidity();
- return Q;
- }
- toRawBytes(isCompressed = false) {
- return hexToBytes(this.toHex(isCompressed));
- }
- toHex(isCompressed = false) {
- const x = numTo32bStr(this.x);
- if (isCompressed) {
- const prefix = this.y & _1n ? '03' : '02';
- return `${prefix}${x}`;
- }
- else {
- return `04${x}${numTo32bStr(this.y)}`;
- }
- }
- toHexX() {
- return this.toHex(true).slice(2);
- }
- toRawX() {
- return this.toRawBytes(true).slice(1);
- }
- assertValidity() {
- const msg = 'Point is not on elliptic curve';
- const { x, y } = this;
- if (!isValidFieldElement(x) || !isValidFieldElement(y))
- throw new Error(msg);
- const left = mod(y * y);
- const right = weistrass(x);
- if (mod(left - right) !== _0n)
- throw new Error(msg);
- }
- equals(other) {
- return this.x === other.x && this.y === other.y;
- }
- negate() {
- return new Point(this.x, mod(-this.y));
- }
- double() {
- return JacobianPoint.fromAffine(this).double().toAffine();
- }
- add(other) {
- return JacobianPoint.fromAffine(this).add(JacobianPoint.fromAffine(other)).toAffine();
- }
- subtract(other) {
- return this.add(other.negate());
- }
- multiply(scalar) {
- return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine();
- }
- multiplyAndAddUnsafe(Q, a, b) {
- const P = JacobianPoint.fromAffine(this);
- const aP = a === _0n || a === _1n || this !== Point.BASE ? P.multiplyUnsafe(a) : P.multiply(a);
- const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b);
- const sum = aP.add(bQ);
- return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine();
- }
- }
- Point.BASE = new Point(CURVE.Gx, CURVE.Gy);
- Point.ZERO = new Point(_0n, _0n);
- function sliceDER(s) {
- return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s;
- }
- function parseDERInt(data) {
- if (data.length < 2 || data[0] !== 0x02) {
- throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`);
- }
- const len = data[1];
- const res = data.subarray(2, len + 2);
- if (!len || res.length !== len) {
- throw new Error(`Invalid signature integer: wrong length`);
- }
- if (res[0] === 0x00 && res[1] <= 0x7f) {
- throw new Error('Invalid signature integer: trailing length');
- }
- return { data: bytesToNumber(res), left: data.subarray(len + 2) };
- }
- function parseDERSignature(data) {
- if (data.length < 2 || data[0] != 0x30) {
- throw new Error(`Invalid signature tag: ${bytesToHex(data)}`);
- }
- if (data[1] !== data.length - 2) {
- throw new Error('Invalid signature: incorrect length');
- }
- const { data: r, left: sBytes } = parseDERInt(data.subarray(2));
- const { data: s, left: rBytesLeft } = parseDERInt(sBytes);
- if (rBytesLeft.length) {
- throw new Error(`Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`);
- }
- return { r, s };
- }
- class Signature {
- constructor(r, s) {
- this.r = r;
- this.s = s;
- this.assertValidity();
- }
- static fromCompact(hex) {
- const arr = isUint8a(hex);
- const name = 'Signature.fromCompact';
- if (typeof hex !== 'string' && !arr)
- throw new TypeError(`${name}: Expected string or Uint8Array`);
- const str = arr ? bytesToHex(hex) : hex;
- if (str.length !== 128)
- throw new Error(`${name}: Expected 64-byte hex`);
- return new Signature(hexToNumber(str.slice(0, 64)), hexToNumber(str.slice(64, 128)));
- }
- static fromDER(hex) {
- const arr = isUint8a(hex);
- if (typeof hex !== 'string' && !arr)
- throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`);
- const { r, s } = parseDERSignature(arr ? hex : hexToBytes(hex));
- return new Signature(r, s);
- }
- static fromHex(hex) {
- return this.fromDER(hex);
- }
- assertValidity() {
- const { r, s } = this;
- if (!isWithinCurveOrder(r))
- throw new Error('Invalid Signature: r must be 0 < r < n');
- if (!isWithinCurveOrder(s))
- throw new Error('Invalid Signature: s must be 0 < s < n');
- }
- hasHighS() {
- const HALF = CURVE.n >> _1n;
- return this.s > HALF;
- }
- normalizeS() {
- return this.hasHighS() ? new Signature(this.r, CURVE.n - this.s) : this;
- }
- toDERRawBytes(isCompressed = false) {
- return hexToBytes(this.toDERHex(isCompressed));
- }
- toDERHex(isCompressed = false) {
- const sHex = sliceDER(numberToHexUnpadded(this.s));
- if (isCompressed)
- return sHex;
- const rHex = sliceDER(numberToHexUnpadded(this.r));
- const rLen = numberToHexUnpadded(rHex.length / 2);
- const sLen = numberToHexUnpadded(sHex.length / 2);
- const length = numberToHexUnpadded(rHex.length / 2 + sHex.length / 2 + 4);
- return `30${length}02${rLen}${rHex}02${sLen}${sHex}`;
- }
- toRawBytes() {
- return this.toDERRawBytes();
- }
- toHex() {
- return this.toDERHex();
- }
- toCompactRawBytes() {
- return hexToBytes(this.toCompactHex());
- }
- toCompactHex() {
- return numTo32bStr(this.r) + numTo32bStr(this.s);
- }
- }
- function concatBytes(...arrays) {
- if (!arrays.every(isUint8a))
- throw new Error('Uint8Array list expected');
- if (arrays.length === 1)
- return arrays[0];
- const length = arrays.reduce((a, arr) => a + arr.length, 0);
- const result = new Uint8Array(length);
- for (let i = 0, pad = 0; i < arrays.length; i++) {
- const arr = arrays[i];
- result.set(arr, pad);
- pad += arr.length;
- }
- return result;
- }
- function isUint8a(bytes) {
- return bytes instanceof Uint8Array;
- }
- const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'));
- function bytesToHex(uint8a) {
- if (!(uint8a instanceof Uint8Array))
- throw new Error('Expected Uint8Array');
- let hex = '';
- for (let i = 0; i < uint8a.length; i++) {
- hex += hexes[uint8a[i]];
- }
- return hex;
- }
- function numTo32bStr(num) {
- if (num > POW_2_256)
- throw new Error('Expected number < 2^256');
- return num.toString(16).padStart(64, '0');
- }
- function numTo32b(num) {
- return hexToBytes(numTo32bStr(num));
- }
- function numberToHexUnpadded(num) {
- const hex = num.toString(16);
- return hex.length & 1 ? `0${hex}` : hex;
- }
- function hexToNumber(hex) {
- if (typeof hex !== 'string') {
- throw new TypeError('hexToNumber: expected string, got ' + typeof hex);
- }
- return BigInt(`0x${hex}`);
- }
- function hexToBytes(hex) {
- if (typeof hex !== 'string') {
- throw new TypeError('hexToBytes: expected string, got ' + typeof hex);
- }
- if (hex.length % 2)
- throw new Error('hexToBytes: received invalid unpadded hex' + hex.length);
- const array = new Uint8Array(hex.length / 2);
- for (let i = 0; i < array.length; i++) {
- const j = i * 2;
- const hexByte = hex.slice(j, j + 2);
- const byte = Number.parseInt(hexByte, 16);
- if (Number.isNaN(byte) || byte < 0)
- throw new Error('Invalid byte sequence');
- array[i] = byte;
- }
- return array;
- }
- function bytesToNumber(bytes) {
- return hexToNumber(bytesToHex(bytes));
- }
- function ensureBytes(hex) {
- return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex);
- }
- function normalizeScalar(num) {
- if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0)
- return BigInt(num);
- if (typeof num === 'bigint' && isWithinCurveOrder(num))
- return num;
- throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n');
- }
- function mod(a, b = CURVE.P) {
- const result = a % b;
- return result >= _0n ? result : b + result;
- }
- function pow2(x, power) {
- const { P } = CURVE;
- let res = x;
- while (power-- > _0n) {
- res *= res;
- res %= P;
- }
- return res;
- }
- function sqrtMod(x) {
- const { P } = CURVE;
- const _6n = BigInt(6);
- const _11n = BigInt(11);
- const _22n = BigInt(22);
- const _23n = BigInt(23);
- const _44n = BigInt(44);
- const _88n = BigInt(88);
- const b2 = (x * x * x) % P;
- const b3 = (b2 * b2 * x) % P;
- const b6 = (pow2(b3, _3n) * b3) % P;
- const b9 = (pow2(b6, _3n) * b3) % P;
- const b11 = (pow2(b9, _2n) * b2) % P;
- const b22 = (pow2(b11, _11n) * b11) % P;
- const b44 = (pow2(b22, _22n) * b22) % P;
- const b88 = (pow2(b44, _44n) * b44) % P;
- const b176 = (pow2(b88, _88n) * b88) % P;
- const b220 = (pow2(b176, _44n) * b44) % P;
- const b223 = (pow2(b220, _3n) * b3) % P;
- const t1 = (pow2(b223, _23n) * b22) % P;
- const t2 = (pow2(t1, _6n) * b2) % P;
- return pow2(t2, _2n);
- }
- function invert(number, modulo = CURVE.P) {
- if (number === _0n || modulo <= _0n) {
- throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`);
- }
- let a = mod(number, modulo);
- let b = modulo;
- let x = _0n, u = _1n;
- while (a !== _0n) {
- const q = b / a;
- const r = b % a;
- const m = x - u * q;
- b = a, a = r, x = u, u = m;
- }
- const gcd = b;
- if (gcd !== _1n)
- throw new Error('invert: does not exist');
- return mod(x, modulo);
- }
- function invertBatch(nums, p = CURVE.P) {
- const scratch = new Array(nums.length);
- const lastMultiplied = nums.reduce((acc, num, i) => {
- if (num === _0n)
- return acc;
- scratch[i] = acc;
- return mod(acc * num, p);
- }, _1n);
- const inverted = invert(lastMultiplied, p);
- nums.reduceRight((acc, num, i) => {
- if (num === _0n)
- return acc;
- scratch[i] = mod(acc * scratch[i], p);
- return mod(acc * num, p);
- }, inverted);
- return scratch;
- }
- const divNearest = (a, b) => (a + b / _2n) / b;
- const POW_2_128 = _2n ** BigInt(128);
- function splitScalarEndo(k) {
- const { n } = CURVE;
- const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15');
- const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3');
- const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8');
- const b2 = a1;
- const c1 = divNearest(b2 * k, n);
- const c2 = divNearest(-b1 * k, n);
- let k1 = mod(k - c1 * a1 - c2 * a2, n);
- let k2 = mod(-c1 * b1 - c2 * b2, n);
- const k1neg = k1 > POW_2_128;
- const k2neg = k2 > POW_2_128;
- if (k1neg)
- k1 = n - k1;
- if (k2neg)
- k2 = n - k2;
- if (k1 > POW_2_128 || k2 > POW_2_128) {
- throw new Error('splitScalarEndo: Endomorphism failed, k=' + k);
- }
- return { k1neg, k1, k2neg, k2 };
- }
- function truncateHash(hash) {
- const { n } = CURVE;
- const byteLength = hash.length;
- const delta = byteLength * 8 - 256;
- let h = bytesToNumber(hash);
- if (delta > 0)
- h = h >> BigInt(delta);
- if (h >= n)
- h -= n;
- return h;
- }
- class HmacDrbg {
- constructor() {
- this.v = new Uint8Array(32).fill(1);
- this.k = new Uint8Array(32).fill(0);
- this.counter = 0;
- }
- hmac(...values) {
- return utils.hmacSha256(this.k, ...values);
- }
- hmacSync(...values) {
- if (typeof utils.hmacSha256Sync !== 'function')
- throw new Error('utils.hmacSha256Sync is undefined, you need to set it');
- const res = utils.hmacSha256Sync(this.k, ...values);
- if (res instanceof Promise)
- throw new Error('To use sync sign(), ensure utils.hmacSha256 is sync');
- return res;
- }
- incr() {
- if (this.counter >= 1000) {
- throw new Error('Tried 1,000 k values for sign(), all were invalid');
- }
- this.counter += 1;
- }
- async reseed(seed = new Uint8Array()) {
- this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed);
- this.v = await this.hmac(this.v);
- if (seed.length === 0)
- return;
- this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed);
- this.v = await this.hmac(this.v);
- }
- reseedSync(seed = new Uint8Array()) {
- this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed);
- this.v = this.hmacSync(this.v);
- if (seed.length === 0)
- return;
- this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed);
- this.v = this.hmacSync(this.v);
- }
- async generate() {
- this.incr();
- this.v = await this.hmac(this.v);
- return this.v;
- }
- generateSync() {
- this.incr();
- this.v = this.hmacSync(this.v);
- return this.v;
- }
- }
- function isWithinCurveOrder(num) {
- return _0n < num && num < CURVE.n;
- }
- function isValidFieldElement(num) {
- return _0n < num && num < CURVE.P;
- }
- function kmdToSig(kBytes, m, d) {
- const k = bytesToNumber(kBytes);
- if (!isWithinCurveOrder(k))
- return;
- const { n } = CURVE;
- const q = Point.BASE.multiply(k);
- const r = mod(q.x, n);
- if (r === _0n)
- return;
- const s = mod(invert(k, n) * mod(m + d * r, n), n);
- if (s === _0n)
- return;
- const sig = new Signature(r, s);
- const recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n);
- return { sig, recovery };
- }
- function normalizePrivateKey(key) {
- let num;
- if (typeof key === 'bigint') {
- num = key;
- }
- else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) {
- num = BigInt(key);
- }
- else if (typeof key === 'string') {
- if (key.length !== 64)
- throw new Error('Expected 32 bytes of private key');
- num = hexToNumber(key);
- }
- else if (isUint8a(key)) {
- if (key.length !== 32)
- throw new Error('Expected 32 bytes of private key');
- num = bytesToNumber(key);
- }
- else {
- throw new TypeError('Expected valid private key');
- }
- if (!isWithinCurveOrder(num))
- throw new Error('Expected private key: 0 < key < n');
- return num;
- }
- function normalizePublicKey(publicKey) {
- if (publicKey instanceof Point) {
- publicKey.assertValidity();
- return publicKey;
- }
- else {
- return Point.fromHex(publicKey);
- }
- }
- function normalizeSignature(signature) {
- if (signature instanceof Signature) {
- signature.assertValidity();
- return signature;
- }
- try {
- return Signature.fromDER(signature);
- }
- catch (error) {
- return Signature.fromCompact(signature);
- }
- }
- function getPublicKey(privateKey, isCompressed = false) {
- return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed);
- }
- function recoverPublicKey(msgHash, signature, recovery, isCompressed = false) {
- return Point.fromSignature(msgHash, signature, recovery).toRawBytes(isCompressed);
- }
- function isPub(item) {
- const arr = isUint8a(item);
- const str = typeof item === 'string';
- const len = (arr || str) && item.length;
- if (arr)
- return len === 33 || len === 65;
- if (str)
- return len === 66 || len === 130;
- if (item instanceof Point)
- return true;
- return false;
- }
- function getSharedSecret(privateA, publicB, isCompressed = false) {
- if (isPub(privateA))
- throw new TypeError('getSharedSecret: first arg must be private key');
- if (!isPub(publicB))
- throw new TypeError('getSharedSecret: second arg must be public key');
- const b = normalizePublicKey(publicB);
- b.assertValidity();
- return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed);
- }
- function bits2int(bytes) {
- const slice = bytes.length > 32 ? bytes.slice(0, 32) : bytes;
- return bytesToNumber(slice);
- }
- function bits2octets(bytes) {
- const z1 = bits2int(bytes);
- const z2 = mod(z1, CURVE.n);
- return int2octets(z2 < _0n ? z1 : z2);
- }
- function int2octets(num) {
- if (typeof num !== 'bigint')
- throw new Error('Expected bigint');
- const hex = numTo32bStr(num);
- return hexToBytes(hex);
- }
- function initSigArgs(msgHash, privateKey, extraEntropy) {
- if (msgHash == null)
- throw new Error(`sign: expected valid message hash, not "${msgHash}"`);
- const h1 = ensureBytes(msgHash);
- const d = normalizePrivateKey(privateKey);
- const seedArgs = [int2octets(d), bits2octets(h1)];
- if (extraEntropy != null) {
- if (extraEntropy === true)
- extraEntropy = utils.randomBytes(32);
- const e = ensureBytes(extraEntropy);
- if (e.length !== 32)
- throw new Error('sign: Expected 32 bytes of extra data');
- seedArgs.push(e);
- }
- const seed = concatBytes(...seedArgs);
- const m = bits2int(h1);
- return { seed, m, d };
- }
- function finalizeSig(recSig, opts) {
- let { sig, recovery } = recSig;
- const { canonical, der, recovered } = Object.assign({ canonical: true, der: true }, opts);
- if (canonical && sig.hasHighS()) {
- sig = sig.normalizeS();
- recovery ^= 1;
- }
- const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes();
- return recovered ? [hashed, recovery] : hashed;
- }
- async function sign(msgHash, privKey, opts = {}) {
- const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy);
- let sig;
- const drbg = new HmacDrbg();
- await drbg.reseed(seed);
- while (!(sig = kmdToSig(await drbg.generate(), m, d)))
- await drbg.reseed();
- return finalizeSig(sig, opts);
- }
- function signSync(msgHash, privKey, opts = {}) {
- const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy);
- let sig;
- const drbg = new HmacDrbg();
- drbg.reseedSync(seed);
- while (!(sig = kmdToSig(drbg.generateSync(), m, d)))
- drbg.reseedSync();
- return finalizeSig(sig, opts);
- }
- const vopts = { strict: true };
- function verify(signature, msgHash, publicKey, opts = vopts) {
- let sig;
- try {
- sig = normalizeSignature(signature);
- msgHash = ensureBytes(msgHash);
- }
- catch (error) {
- return false;
- }
- const { r, s } = sig;
- if (opts.strict && sig.hasHighS())
- return false;
- const h = truncateHash(msgHash);
- let P;
- try {
- P = normalizePublicKey(publicKey);
- }
- catch (error) {
- return false;
- }
- const { n } = CURVE;
- const sinv = invert(s, n);
- const u1 = mod(h * sinv, n);
- const u2 = mod(r * sinv, n);
- const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2);
- if (!R)
- return false;
- const v = mod(R.x, n);
- return v === r;
- }
- function finalizeSchnorrChallenge(ch) {
- return mod(bytesToNumber(ch), CURVE.n);
- }
- function hasEvenY(point) {
- return (point.y & _1n) === _0n;
- }
- class SchnorrSignature {
- constructor(r, s) {
- this.r = r;
- this.s = s;
- this.assertValidity();
- }
- static fromHex(hex) {
- const bytes = ensureBytes(hex);
- if (bytes.length !== 64)
- throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`);
- const r = bytesToNumber(bytes.subarray(0, 32));
- const s = bytesToNumber(bytes.subarray(32, 64));
- return new SchnorrSignature(r, s);
- }
- assertValidity() {
- const { r, s } = this;
- if (!isValidFieldElement(r) || !isWithinCurveOrder(s))
- throw new Error('Invalid signature');
- }
- toHex() {
- return numTo32bStr(this.r) + numTo32bStr(this.s);
- }
- toRawBytes() {
- return hexToBytes(this.toHex());
- }
- }
- function schnorrGetPublicKey(privateKey) {
- return Point.fromPrivateKey(privateKey).toRawX();
- }
- function initSchnorrSigArgs(message, privateKey, auxRand) {
- if (message == null)
- throw new TypeError(`sign: Expected valid message, not "${message}"`);
- const m = ensureBytes(message);
- const d0 = normalizePrivateKey(privateKey);
- const rand = ensureBytes(auxRand);
- if (rand.length !== 32)
- throw new TypeError('sign: Expected 32 bytes of aux randomness');
- const P = Point.fromPrivateKey(d0);
- const px = P.toRawX();
- const d = hasEvenY(P) ? d0 : CURVE.n - d0;
- return { m, P, px, d, rand };
- }
- function initSchnorrNonce(d, t0h) {
- return numTo32b(d ^ bytesToNumber(t0h));
- }
- function finalizeSchnorrNonce(k0h) {
- const k0 = mod(bytesToNumber(k0h), CURVE.n);
- if (k0 === _0n)
- throw new Error('sign: Creation of signature failed. k is zero');
- const R = Point.fromPrivateKey(k0);
- const rx = R.toRawX();
- const k = hasEvenY(R) ? k0 : CURVE.n - k0;
- return { R, rx, k };
- }
- function finalizeSchnorrSig(R, k, e, d) {
- return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes();
- }
- async function schnorrSign(message, privateKey, auxRand = utils.randomBytes()) {
- const { m, px, d, rand } = initSchnorrSigArgs(message, privateKey, auxRand);
- const t = initSchnorrNonce(d, await utils.taggedHash(TAGS.aux, rand));
- const { R, rx, k } = finalizeSchnorrNonce(await utils.taggedHash(TAGS.nonce, t, px, m));
- const e = finalizeSchnorrChallenge(await utils.taggedHash(TAGS.challenge, rx, px, m));
- const sig = finalizeSchnorrSig(R, k, e, d);
- const isValid = await schnorrVerify(sig, m, px);
- if (!isValid)
- throw new Error('sign: Invalid signature produced');
- return sig;
- }
- function schnorrSignSync(message, privateKey, auxRand = utils.randomBytes()) {
- const { m, px, d, rand } = initSchnorrSigArgs(message, privateKey, auxRand);
- const t = initSchnorrNonce(d, utils.taggedHashSync(TAGS.aux, rand));
- const { R, rx, k } = finalizeSchnorrNonce(utils.taggedHashSync(TAGS.nonce, t, px, m));
- const e = finalizeSchnorrChallenge(utils.taggedHashSync(TAGS.challenge, rx, px, m));
- const sig = finalizeSchnorrSig(R, k, e, d);
- const isValid = schnorrVerifySync(sig, m, px);
- if (!isValid)
- throw new Error('sign: Invalid signature produced');
- return sig;
- }
- function initSchnorrVerify(signature, message, publicKey) {
- const raw = signature instanceof SchnorrSignature;
- const sig = raw ? signature : SchnorrSignature.fromHex(signature);
- if (raw)
- sig.assertValidity();
- return {
- ...sig,
- m: ensureBytes(message),
- P: normalizePublicKey(publicKey),
- };
- }
- function finalizeSchnorrVerify(r, P, s, e) {
- const R = Point.BASE.multiplyAndAddUnsafe(P, normalizePrivateKey(s), mod(-e, CURVE.n));
- if (!R || !hasEvenY(R) || R.x !== r)
- return false;
- return true;
- }
- async function schnorrVerify(signature, message, publicKey) {
- try {
- const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey);
- const e = finalizeSchnorrChallenge(await utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m));
- return finalizeSchnorrVerify(r, P, s, e);
- }
- catch (error) {
- return false;
- }
- }
- function schnorrVerifySync(signature, message, publicKey) {
- try {
- const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey);
- const e = finalizeSchnorrChallenge(utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m));
- return finalizeSchnorrVerify(r, P, s, e);
- }
- catch (error) {
- return false;
- }
- }
- const schnorr = {
- Signature: SchnorrSignature,
- getPublicKey: schnorrGetPublicKey,
- sign: schnorrSign,
- verify: schnorrVerify,
- signSync: schnorrSignSync,
- verifySync: schnorrVerifySync,
- };
- Point.BASE._setWindowSize(8);
- const crypto = {
- node: nodeCrypto,
- web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined,
- };
- const TAGS = {
- challenge: 'BIP0340/challenge',
- aux: 'BIP0340/aux',
- nonce: 'BIP0340/nonce',
- };
- const TAGGED_HASH_PREFIXES = {};
- const utils = {
- isValidPrivateKey(privateKey) {
- try {
- normalizePrivateKey(privateKey);
- return true;
- }
- catch (error) {
- return false;
- }
- },
- privateAdd: (privateKey, tweak) => {
- const p = normalizePrivateKey(privateKey);
- const t = normalizePrivateKey(tweak);
- return numTo32b(mod(p + t, CURVE.n));
- },
- privateNegate: (privateKey) => {
- const p = normalizePrivateKey(privateKey);
- return numTo32b(CURVE.n - p);
- },
- pointAddScalar: (p, tweak, isCompressed) => {
- const P = Point.fromHex(p);
- const t = normalizePrivateKey(tweak);
- const Q = Point.BASE.multiplyAndAddUnsafe(P, t, _1n);
- if (!Q)
- throw new Error('Tweaked point at infinity');
- return Q.toRawBytes(isCompressed);
- },
- pointMultiply: (p, tweak, isCompressed) => {
- const P = Point.fromHex(p);
- const t = bytesToNumber(ensureBytes(tweak));
- return P.multiply(t).toRawBytes(isCompressed);
- },
- hashToPrivateKey: (hash) => {
- hash = ensureBytes(hash);
- if (hash.length < 40 || hash.length > 1024)
- throw new Error('Expected 40-1024 bytes of private key as per FIPS 186');
- const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n;
- return numTo32b(num);
- },
- randomBytes: (bytesLength = 32) => {
- if (crypto.web) {
- return crypto.web.getRandomValues(new Uint8Array(bytesLength));
- }
- else if (crypto.node) {
- const { randomBytes } = crypto.node;
- return Uint8Array.from(randomBytes(bytesLength));
- }
- else {
- throw new Error("The environment doesn't have randomBytes function");
- }
- },
- randomPrivateKey: () => {
- return utils.hashToPrivateKey(utils.randomBytes(40));
- },
- bytesToHex,
- hexToBytes,
- concatBytes,
- mod,
- invert,
- sha256: async (...messages) => {
- if (crypto.web) {
- const buffer = await crypto.web.subtle.digest('SHA-256', concatBytes(...messages));
- return new Uint8Array(buffer);
- }
- else if (crypto.node) {
- const { createHash } = crypto.node;
- const hash = createHash('sha256');
- messages.forEach((m) => hash.update(m));
- return Uint8Array.from(hash.digest());
- }
- else {
- throw new Error("The environment doesn't have sha256 function");
- }
- },
- hmacSha256: async (key, ...messages) => {
- if (crypto.web) {
- const ckey = await crypto.web.subtle.importKey('raw', key, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']);
- const message = concatBytes(...messages);
- const buffer = await crypto.web.subtle.sign('HMAC', ckey, message);
- return new Uint8Array(buffer);
- }
- else if (crypto.node) {
- const { createHmac } = crypto.node;
- const hash = createHmac('sha256', key);
- messages.forEach((m) => hash.update(m));
- return Uint8Array.from(hash.digest());
- }
- else {
- throw new Error("The environment doesn't have hmac-sha256 function");
- }
- },
- sha256Sync: undefined,
- hmacSha256Sync: undefined,
- taggedHash: async (tag, ...messages) => {
- let tagP = TAGGED_HASH_PREFIXES[tag];
- if (tagP === undefined) {
- const tagH = await utils.sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0)));
- tagP = concatBytes(tagH, tagH);
- TAGGED_HASH_PREFIXES[tag] = tagP;
- }
- return utils.sha256(tagP, ...messages);
- },
- taggedHashSync: (tag, ...messages) => {
- if (typeof utils.sha256Sync !== 'function')
- throw new Error('utils.sha256Sync is undefined, you need to set it');
- let tagP = TAGGED_HASH_PREFIXES[tag];
- if (tagP === undefined) {
- const tagH = utils.sha256Sync(Uint8Array.from(tag, (c) => c.charCodeAt(0)));
- tagP = concatBytes(tagH, tagH);
- TAGGED_HASH_PREFIXES[tag] = tagP;
- }
- return utils.sha256Sync(tagP, ...messages);
- },
- precompute(windowSize = 8, point = Point.BASE) {
- const cached = point === Point.BASE ? point : new Point(point.x, point.y);
- cached._setWindowSize(windowSize);
- cached.multiply(_3n);
- return cached;
- },
- };
-
- exports.CURVE = CURVE;
- exports.Point = Point;
- exports.Signature = Signature;
- exports.getPublicKey = getPublicKey;
- exports.getSharedSecret = getSharedSecret;
- exports.recoverPublicKey = recoverPublicKey;
- exports.schnorr = schnorr;
- exports.sign = sign;
- exports.signSync = signSync;
- exports.utils = utils;
- exports.verify = verify;
-
- Object.defineProperty(exports, '__esModule', { value: true });
-
-}));
diff --git a/lnbits/extensions/watchonly/static/js/index.js b/lnbits/extensions/watchonly/static/js/index.js
deleted file mode 100644
index 880d6b30..00000000
--- a/lnbits/extensions/watchonly/static/js/index.js
+++ /dev/null
@@ -1,435 +0,0 @@
-const watchOnly = async () => {
- Vue.component(VueQrcode.name, VueQrcode)
-
- await walletConfig('static/components/wallet-config/wallet-config.html')
- await walletList('static/components/wallet-list/wallet-list.html')
- await addressList('static/components/address-list/address-list.html')
- await history('static/components/history/history.html')
- await utxoList('static/components/utxo-list/utxo-list.html')
- await feeRate('static/components/fee-rate/fee-rate.html')
- await seedInput('static/components/seed-input/seed-input.html')
- await sendTo('static/components/send-to/send-to.html')
- await payment('static/components/payment/payment.html')
- await serialSigner('static/components/serial-signer/serial-signer.html')
- await serialPortConfig(
- 'static/components/serial-port-config/serial-port-config.html'
- )
-
- Vue.filter('reverse', function (value) {
- // slice to make a copy of array, then reverse the copy
- return value.slice().reverse()
- })
-
- new Vue({
- el: '#vue',
- mixins: [windowMixin],
- data: function () {
- return {
- scan: {
- scanning: false,
- scanCount: 0,
- scanIndex: 0
- },
-
- currentAddress: null,
-
- tab: 'addresses',
-
- config: {sats_denominated: true},
-
- qrCodeDialog: {
- show: false,
- data: null
- },
- ...tables,
- ...tableData,
-
- walletAccounts: [],
- addresses: [],
- history: [],
- historyFilter: '',
-
- showAddress: false,
- addressNote: '',
- showPayment: false,
- fetchedUtxos: false,
- utxosFilter: '',
- network: null,
-
- showEnterSignedPsbt: false,
- signedBase64Psbt: null
- }
- },
- computed: {
- mempoolHostname: function () {
- if (!this.config.isLoaded) return
- let hostname = new URL(this.config.mempool_endpoint).hostname
- if (this.config.network === 'Testnet') {
- hostname += '/testnet'
- }
- return hostname
- }
- },
-
- methods: {
- updateAmountForAddress: async function (addressData, amount = 0) {
- try {
- const wallet = this.g.user.wallets[0]
- addressData.amount = amount
- if (!addressData.isChange) {
- const addressWallet = this.walletAccounts.find(
- w => w.id === addressData.wallet
- )
- if (
- addressWallet &&
- addressWallet.address_no < addressData.addressIndex
- ) {
- addressWallet.address_no = addressData.addressIndex
- }
- }
-
- // todo: account deleted
- await LNbits.api.request(
- 'PUT',
- `/watchonly/api/v1/address/${addressData.id}`,
- wallet.adminkey,
- {amount}
- )
- } catch (err) {
- addressData.error = 'Failed to refresh amount for address'
- this.$q.notify({
- type: 'warning',
- message: `Failed to refresh amount for address ${addressData.address}`,
- timeout: 10000
- })
- LNbits.utils.notifyApiError(err)
- }
- },
- updateNoteForAddress: async function ({addressId, note}) {
- try {
- const wallet = this.g.user.wallets[0]
- await LNbits.api.request(
- 'PUT',
- `/watchonly/api/v1/address/${addressId}`,
- wallet.adminkey,
- {note}
- )
- const updatedAddress =
- this.addresses.find(a => a.id === addressId) || {}
- updatedAddress.note = note
- } catch (err) {
- LNbits.utils.notifyApiError(err)
- }
- },
-
- //################### ADDRESS HISTORY ###################
- addressHistoryFromTxs: function (addressData, txs) {
- const addressHistory = []
- txs.forEach(tx => {
- const sent = tx.vin
- .filter(
- vin => vin.prevout.scriptpubkey_address === addressData.address
- )
- .map(vin => mapInputToSentHistory(tx, addressData, vin))
-
- const received = tx.vout
- .filter(vout => vout.scriptpubkey_address === addressData.address)
- .map(vout => mapOutputToReceiveHistory(tx, addressData, vout))
- addressHistory.push(...sent, ...received)
- })
- return addressHistory
- },
-
- markSameTxAddressHistory: function () {
- this.history
- .filter(s => s.sent)
- .forEach((el, i, arr) => {
- if (el.isSubItem) return
-
- const sameTxItems = arr.slice(i + 1).filter(e => e.txId === el.txId)
- if (!sameTxItems.length) return
- sameTxItems.forEach(e => {
- e.isSubItem = true
- })
-
- el.totalAmount =
- el.amount + sameTxItems.reduce((t, e) => (t += e.amount || 0), 0)
- el.sameTxItems = sameTxItems
- })
- },
-
- //################### PAYMENT ###################
-
- initPaymentData: async function () {
- if (!this.payment.show) return
- await this.refreshAddresses()
- },
-
- goToPaymentView: async function () {
- this.showPayment = true
- await this.initPaymentData()
- },
-
- //################### PSBT ###################
-
- updateSignedPsbt: async function (psbtBase64) {
- this.$refs.paymentRef.updateSignedPsbt(psbtBase64)
- },
-
- showEnterSignedPsbtDialog: function () {
- this.signedBase64Psbt = ''
- this.showEnterSignedPsbt = true
- },
-
- checkPsbt: function () {
- this.$refs.paymentRef.updateSignedPsbt(this.signedBase64Psbt)
- },
-
- //################### UTXOs ###################
- scanAllAddresses: async function () {
- await this.refreshAddresses()
- this.history = []
- let addresses = this.addresses
- this.utxos.data = []
- this.utxos.total = 0
- // Loop while new funds are found on the gap adresses.
- // Use 1000 limit as a safety check (scan 20 000 addresses max)
- for (let i = 0; i < 1000 && addresses.length; i++) {
- await this.updateUtxosForAddresses(addresses)
- const oldAddresses = this.addresses.slice()
- await this.refreshAddresses()
- const newAddresses = this.addresses.slice()
- // check if gap addresses have been extended
- addresses = newAddresses.filter(
- newAddr => !oldAddresses.find(oldAddr => oldAddr.id === newAddr.id)
- )
- if (addresses.length) {
- this.$q.notify({
- type: 'positive',
- message: 'Funds found! Scanning for more...',
- timeout: 10000
- })
- }
- }
- },
- scanAddressWithAmount: async function () {
- this.utxos.data = []
- this.utxos.total = 0
- this.history = []
- const addresses = this.addresses.filter(a => a.hasActivity)
- await this.updateUtxosForAddresses(addresses)
- },
- scanAddress: async function (addressData) {
- this.updateUtxosForAddresses([addressData])
- this.$q.notify({
- type: 'positive',
- message: 'Address Rescanned',
- timeout: 10000
- })
- },
- refreshAddresses: async function () {
- if (!this.walletAccounts) return
- this.addresses = []
- for (const {id, type} of this.walletAccounts) {
- const newAddresses = await this.getAddressesForWallet(id)
- const uniqueAddresses = newAddresses.filter(
- newAddr => !this.addresses.find(a => a.address === newAddr.address)
- )
-
- const lastActiveAddress =
- uniqueAddresses.filter(a => !a.isChange && a.hasActivity).pop() ||
- {}
-
- uniqueAddresses.forEach(a => {
- a.expanded = false
- a.accountType = type
- a.gapLimitExceeded =
- !a.isChange &&
- a.addressIndex >
- lastActiveAddress.addressIndex + DEFAULT_RECEIVE_GAP_LIMIT
- })
- this.addresses.push(...uniqueAddresses)
- }
- this.$emit('update:addresses', this.addresses)
- },
- getAddressesForWallet: async function (walletId) {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/watchonly/api/v1/addresses/' + walletId,
- this.g.user.wallets[0].inkey
- )
- return data.map(mapAddressesData)
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: `Failed to fetch addresses for wallet with id ${walletId}.`,
- timeout: 10000
- })
- LNbits.utils.notifyApiError(error)
- }
- return []
- },
- updateUtxosForAddresses: async function (addresses = []) {
- this.scan = {scanning: true, scanCount: addresses.length, scanIndex: 0}
-
- try {
- for (addrData of addresses) {
- const addressHistory = await this.getAddressTxsDelayed(addrData)
- // remove old entries
- this.history = this.history.filter(
- h => h.address !== addrData.address
- )
-
- // add new entries
- this.history.push(...addressHistory)
- this.history.sort((a, b) => (!a.height ? -1 : b.height - a.height))
- this.markSameTxAddressHistory()
-
- if (addressHistory.length) {
- // search only if it ever had any activity
- const utxos = await this.getAddressTxsUtxoDelayed(
- addrData.address
- )
- this.updateUtxosForAddress(addrData, utxos)
- }
-
- this.scan.scanIndex++
- }
- } catch (error) {
- console.error(error)
- this.$q.notify({
- type: 'warning',
- message: 'Failed to scan addresses',
- timeout: 10000
- })
- } finally {
- this.scan.scanning = false
- }
- },
- updateUtxosForAddress: function (addressData, utxos = []) {
- const wallet =
- this.walletAccounts.find(w => w.id === addressData.wallet) || {}
-
- const newUtxos = utxos.map(utxo =>
- mapAddressDataToUtxo(wallet, addressData, utxo)
- )
- // remove old utxos
- this.utxos.data = this.utxos.data.filter(
- u => u.address !== addressData.address
- )
- // add new utxos
- this.utxos.data.push(...newUtxos)
- if (utxos.length) {
- this.utxos.data.sort((a, b) => b.sort - a.sort)
- this.utxos.total = this.utxos.data.reduce(
- (total, y) => (total += y?.amount || 0),
- 0
- )
- }
- const addressTotal = utxos.reduce(
- (total, y) => (total += y?.value || 0),
- 0
- )
- this.updateAmountForAddress(addressData, addressTotal)
- },
-
- //################### MEMPOOL API ###################
- getAddressTxsDelayed: async function (addrData) {
- const accounts = this.walletAccounts
- const {
- bitcoin: {addresses: addressesAPI}
- } = mempoolJS({
- hostname: this.mempoolHostname
- })
- const fn = async () => {
- if (!accounts.find(w => w.id === addrData.wallet)) return []
- return addressesAPI.getAddressTxs({
- address: addrData.address
- })
- }
- const addressTxs = await retryWithDelay(fn)
- return this.addressHistoryFromTxs(addrData, addressTxs)
- },
-
- getAddressTxsUtxoDelayed: async function (address) {
- const endpoint = this.mempoolHostname
- const {
- bitcoin: {addresses: addressesAPI}
- } = mempoolJS({
- hostname: endpoint
- })
-
- const fn = async () => {
- if (endpoint !== this.mempoolHostname) return []
- return addressesAPI.getAddressTxsUtxo({
- address
- })
- }
- return retryWithDelay(fn)
- },
-
- //################### OTHER ###################
-
- openQrCodeDialog: function (addressData) {
- this.currentAddress = addressData
- this.addressNote = addressData.note || ''
- this.showAddress = true
- },
- searchInTab: function ({tab, value}) {
- this.tab = tab
- this[`${tab}Filter`] = value
- },
-
- updateAccounts: async function (accounts) {
- this.walletAccounts = accounts
- await this.refreshAddresses()
- await this.scanAddressWithAmount()
- },
- showAddressDetails: function (addressData) {
- this.openQrCodeDialog(addressData)
- },
- showAddressDetailsWithConfirmation: function ({addressData, wallet}) {
- this.showAddressDetails(addressData)
- if (this.$refs.serialSigner.isConnected()) {
- if (this.$refs.serialSigner.isAuthenticated()) {
- if (wallet.meta?.accountPath) {
- const branchIndex = addressData.isChange ? 1 : 0
- const path =
- wallet.meta.accountPath +
- `/${branchIndex}/${addressData.addressIndex}`
- this.$refs.serialSigner.hwwShowAddress(path, addressData.address)
- }
- } else {
- this.$q.notify({
- type: 'warning',
- message: 'Please login in order to confirm address on device',
- timeout: 10000
- })
- }
- }
- },
- initUtxos: function (addresses) {
- if (!this.fetchedUtxos && addresses.length) {
- this.fetchedUtxos = true
- this.addresses = addresses
- this.scanAddressWithAmount()
- }
- },
- handleBroadcastSuccess: async function (txId) {
- this.tab = 'history'
- this.searchInTab({tab: 'history', value: txId})
- this.showPayment = false
- await this.refreshAddresses()
- await this.scanAddressWithAmount()
- }
- },
- created: async function () {
- if (this.g.user.wallets.length) {
- await this.refreshAddresses()
- await this.scanAddressWithAmount()
- }
- }
- })
-}
-watchOnly()
diff --git a/lnbits/extensions/watchonly/static/js/map.js b/lnbits/extensions/watchonly/static/js/map.js
deleted file mode 100644
index 81093936..00000000
--- a/lnbits/extensions/watchonly/static/js/map.js
+++ /dev/null
@@ -1,81 +0,0 @@
-const mapAddressesData = a => ({
- id: a.id,
- address: a.address,
- amount: a.amount,
- wallet: a.wallet,
- note: a.note,
-
- isChange: a.branch_index === 1,
- addressIndex: a.address_index,
- hasActivity: a.has_activity
-})
-
-const mapInputToSentHistory = (tx, addressData, vin) => ({
- sent: true,
- txId: tx.txid,
- address: addressData.address,
- isChange: addressData.isChange,
- amount: vin.prevout.value,
- date: blockTimeToDate(tx.status.block_time),
- height: tx.status.block_height,
- confirmed: tx.status.confirmed,
- fee: tx.fee,
- expanded: false
-})
-
-const mapOutputToReceiveHistory = (tx, addressData, vout) => ({
- received: true,
- txId: tx.txid,
- address: addressData.address,
- isChange: addressData.isChange,
- amount: vout.value,
- date: blockTimeToDate(tx.status.block_time),
- height: tx.status.block_height,
- confirmed: tx.status.confirmed,
- fee: tx.fee,
- expanded: false
-})
-
-const mapUtxoToPsbtInput = utxo => ({
- tx_id: utxo.txId,
- vout: utxo.vout,
- amount: utxo.amount,
- address: utxo.address,
- branch_index: utxo.isChange ? 1 : 0,
- address_index: utxo.addressIndex,
- wallet: utxo.wallet,
- accountType: utxo.accountType,
- txHex: ''
-})
-
-const mapAddressDataToUtxo = (wallet, addressData, utxo) => ({
- id: addressData.id,
- address: addressData.address,
- isChange: addressData.isChange,
- addressIndex: addressData.addressIndex,
- wallet: addressData.wallet,
- accountType: addressData.accountType,
- masterpubFingerprint: wallet.fingerprint,
- txId: utxo.txid,
- vout: utxo.vout,
- confirmed: utxo.status.confirmed,
- amount: utxo.value,
- date: blockTimeToDate(utxo.status?.block_time),
- sort: utxo.status?.block_time,
- expanded: false,
- selected: false
-})
-
-const mapWalletAccount = function (o) {
- return Object.assign({}, o, {
- date: o.time
- ? Quasar.utils.date.formatDate(
- new Date(o.time * 1000),
- 'YYYY-MM-DD HH:mm'
- )
- : '',
- meta: o.meta ? JSON.parse(o.meta) : null,
- label: o.title,
- expanded: false
- })
-}
diff --git a/lnbits/extensions/watchonly/static/js/tables.js b/lnbits/extensions/watchonly/static/js/tables.js
deleted file mode 100644
index f437bcd5..00000000
--- a/lnbits/extensions/watchonly/static/js/tables.js
+++ /dev/null
@@ -1,61 +0,0 @@
-const tables = {
- summaryTable: {
- columns: [
- {
- name: 'totalInputs',
- align: 'center',
- label: 'Selected Amount'
- },
- {
- name: 'totalOutputs',
- align: 'center',
- label: 'Payed Amount'
- },
- {
- name: 'fees',
- align: 'center',
- label: 'Fees'
- },
- {
- name: 'change',
- align: 'center',
- label: 'Change'
- }
- ]
- }
-}
-
-const tableData = {
- utxos: {
- data: [],
- total: 0
- },
- payment: {
- fee: 0,
- txSize: 0,
- tx: null,
- psbtBase64: '',
- psbtBase64Signed: '',
- signedTx: null,
- signedTxHex: null,
- sentTxId: null,
-
- signModes: [
- {
- label: 'Serial Port Device',
- value: 'serial-port'
- },
- {
- label: 'Animated QR',
- value: 'animated-qr',
- disable: true
- }
- ],
- signMode: '',
- show: false,
- showAdvanced: false
- },
- summary: {
- data: [{totalInputs: 0, totalOutputs: 0, fees: 0, change: 0}]
- }
-}
diff --git a/lnbits/extensions/watchonly/static/js/utils.js b/lnbits/extensions/watchonly/static/js/utils.js
deleted file mode 100644
index c73dd9c0..00000000
--- a/lnbits/extensions/watchonly/static/js/utils.js
+++ /dev/null
@@ -1,210 +0,0 @@
-const PSBT_BASE64_PREFIX = 'cHNidP8'
-
-const COMMAND_PING = '/ping'
-const COMMAND_PASSWORD = '/password'
-const COMMAND_PASSWORD_CLEAR = '/password-clear'
-const COMMAND_ADDRESS = '/address'
-const COMMAND_SEND_PSBT = '/psbt'
-const COMMAND_SIGN_PSBT = '/sign'
-const COMMAND_HELP = '/help'
-const COMMAND_WIPE = '/wipe'
-const COMMAND_SEED = '/seed'
-const COMMAND_RESTORE = '/restore'
-const COMMAND_CONFIRM_NEXT = '/confirm-next'
-const COMMAND_CANCEL = '/cancel'
-const COMMAND_XPUB = '/xpub'
-const COMMAND_PAIR = '/pair'
-const COMMAND_LOG = '/log'
-const COMMAND_CHECK_PAIRING = '/check-pairing'
-
-const DEFAULT_RECEIVE_GAP_LIMIT = 20
-const PAIRING_CONTROL_TEXT = 'lnbits'
-
-const HWW_DEFAULT_CONFIG = Object.freeze({
- name: '',
- buttonOnePin: '',
- buttonTwoPin: '',
- baudRate: 9600,
- bufferSize: 255,
- dataBits: 8,
- flowControl: 'none',
- parity: 'none',
- stopBits: 1
-})
-
-const blockTimeToDate = blockTime =>
- blockTime ? moment(blockTime * 1000).format('LLL') : ''
-
-const currentDateTime = () => moment().format('LLL')
-
-const sleep = ms => new Promise(r => setTimeout(r, ms))
-
-const retryWithDelay = async function (fn, retryCount = 0) {
- try {
- await sleep(25)
- // Do not return the call directly, use result.
- // Otherwise the error will not be cought in this try-catch block.
- const result = await fn()
- return result
- } catch (err) {
- if (retryCount > 100) throw err
- await sleep((retryCount + 1) * 1000)
- return retryWithDelay(fn, retryCount + 1)
- }
-}
-
-const txSize = tx => {
- // https://bitcoinops.org/en/tools/calc-size/
- // overhead size
- const nVersion = 4
- const inCount = 1
- const outCount = 1
- const nlockTime = 4
- const hasSegwit = !!tx.inputs.find(inp =>
- ['p2wsh', 'p2wpkh', 'p2tr'].includes(inp.accountType)
- )
- const segwitFlag = hasSegwit ? 0.5 : 0
- const overheadSize = nVersion + inCount + outCount + nlockTime + segwitFlag
-
- // inputs size
- const outpoint = 36 // txId plus vout index number
- const scriptSigLength = 1
- const nSequence = 4
- const inputsSize = tx.inputs.reduce((t, inp) => {
- const scriptSig =
- inp.accountType === 'p2pkh' ? 107 : inp.accountType === 'p2sh' ? 254 : 0
- const witnessItemCount = hasSegwit ? 0.25 : 0
- const witnessItems =
- inp.accountType === 'p2wpkh'
- ? 27
- : inp.accountType === 'p2wsh'
- ? 63.5
- : inp.accountType === 'p2tr'
- ? 16.5
- : 0
- t +=
- outpoint +
- scriptSigLength +
- nSequence +
- scriptSig +
- witnessItemCount +
- witnessItems
- return t
- }, 0)
-
- // outputs size
- const nValue = 8
- const scriptPubKeyLength = 1
-
- const outputsSize = tx.outputs.reduce((t, out) => {
- const type = guessAddressType(out.address)
-
- const scriptPubKey =
- type === 'p2pkh'
- ? 25
- : type === 'p2wpkh'
- ? 22
- : type === 'p2sh'
- ? 23
- : type === 'p2wsh'
- ? 34
- : 34 // default to the largest size (p2tr included)
- t += nValue + scriptPubKeyLength + scriptPubKey
- return t
- }, 0)
-
- return overheadSize + inputsSize + outputsSize
-}
-const guessAddressType = (a = '') => {
- if (a.startsWith('1') || a.startsWith('n')) return 'p2pkh'
- if (a.startsWith('3') || a.startsWith('2')) return 'p2sh'
- if (a.startsWith('bc1q') || a.startsWith('tb1q'))
- return a.length === 42 ? 'p2wpkh' : 'p2wsh'
- if (a.startsWith('bc1p') || a.startsWith('tb1p')) return 'p2tr'
-}
-
-const ACCOUNT_TYPES = {
- p2tr: 'Taproot, BIP86, P2TR, Bech32m',
- p2wpkh: 'SegWit, BIP84, P2WPKH, Bech32',
- p2sh: 'BIP49, P2SH-P2WPKH, Base58',
- p2pkh: 'Legacy, BIP44, P2PKH, Base58'
-}
-
-const getAccountDescription = type => ACCOUNT_TYPES[type] || 'nonstandard'
-
-const readFromSerialPort = reader => {
- let partialChunk
- let fulliness = []
-
- const readStringUntil = async (separator = '\n') => {
- if (fulliness.length) return fulliness.shift().trim()
- const chunks = []
- if (partialChunk) {
- // leftovers from previous read
- chunks.push(partialChunk)
- partialChunk = undefined
- }
- while (true) {
- const {value, done} = await reader.read()
- if (value) {
- const values = value.split(separator)
- // found one or more separators
- if (values.length > 1) {
- chunks.push(values.shift()) // first element
- partialChunk = values.pop() // last element
- fulliness = values // full lines
- return {value: chunks.join('').trim(), done: false}
- }
- chunks.push(value)
- }
- if (done) return {value: chunks.join('').trim(), done: true}
- }
- }
- return readStringUntil
-}
-
-function satOrBtc(val, showUnit = true, showSats = false) {
- const value = showSats
- ? LNbits.utils.formatSat(val)
- : val == 0
- ? 0.0
- : (val / 100000000).toFixed(8)
- if (!showUnit) return value
- return showSats ? value + ' sat' : value + ' BTC'
-}
-
-function loadTemplateAsync(path) {
- const result = new Promise(resolve => {
- const xhttp = new XMLHttpRequest()
-
- xhttp.onreadystatechange = function () {
- if (this.readyState == 4) {
- if (this.status == 200) resolve(this.responseText)
-
- if (this.status == 404) resolve(`Page not found: ${path}
`)
- }
- }
-
- xhttp.open('GET', path, true)
- xhttp.send()
- })
-
- return result
-}
-
-function findAccountPathIssues(path = '') {
- const p = path.split('/')
- if (p[0] !== 'm') return "Path must start with 'm/'"
- for (let i = 1; i < p.length; i++) {
- if (p[i].endsWith('')) p[i] = p[i].substring(0, p[i].length - 1)
- if (isNaN(p[i])) return `${p[i]} is not a valid value`
- }
-}
-
-function asciiToUint8Array(str) {
- var chars = []
- for (var i = 0; i < str.length; ++i) {
- chars.push(str.charCodeAt(i))
- }
- return new Uint8Array(chars)
-}
diff --git a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html
deleted file mode 100644
index 0365df44..00000000
--- a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
- Onchain Wallet (watch-only) extension uses mempool.space
- For use with "account Extended Public Key"
- https://iancoleman.io/bip39/
-
- Flash binaries
- directly from browser
-
- Created by
- Ben Arc ,
- Tiago Vasconcelos ,
- motorina0
- (using,
- Embit )
-
-
- Swagger REST API Documentation
-
-
-
diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html
deleted file mode 100644
index 84be5c81..00000000
--- a/lnbits/extensions/watchonly/templates/watchonly/index.html
+++ /dev/null
@@ -1,316 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
-
-
-
-
-
-
-
- {% raw %}
-
-
-
- Scan Blockchain
-
-
-
-
-
-
-
-
-
- New Payment
- Create a new payment by selecting Inputs and
- Outputs
-
-
-
-
- From Signed PSBT
- Paste a signed PSBT
-
-
-
-
-
- Back
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
- {{SITE_TITLE}} Onchain Wallet (watch-only) Extension
- (v0.3)
-
-
-
-
- {% include "watchonly/_api_docs.html" %}
-
-
-
- {% raw %}
-
-
- Address Details
-
-
-
-
-
-
-
-
- {{ currentAddress.address }}
-
-
-
-
-
-
-
- Gap limit of 20 addresses exceeded. Other wallets might not detect
- funds at this address.
-
-
- Save Note
- Close
-
-
-
-
-
-
-
- Enter the Signed PSBT
-
-
-
-
-
-
-
- Check PSBT
- Close
-
-
-
-
-
- {% endraw %}
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/watchonly/views.py b/lnbits/extensions/watchonly/views.py
deleted file mode 100644
index 8cebc6cc..00000000
--- a/lnbits/extensions/watchonly/views.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from fastapi import Depends, 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 watchonly_ext, watchonly_renderer
-
-templates = Jinja2Templates(directory="templates")
-
-
-@watchonly_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return watchonly_renderer().TemplateResponse(
- "watchonly/index.html", {"request": request, "user": user.dict()}
- )
diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py
deleted file mode 100644
index e0c427fe..00000000
--- a/lnbits/extensions/watchonly/views_api.py
+++ /dev/null
@@ -1,385 +0,0 @@
-import json
-from http import HTTPStatus
-from typing import List
-
-import httpx
-from embit import finalizer, script
-from embit.ec import PublicKey
-from embit.networks import NETWORKS
-from embit.psbt import PSBT, DerivationPath
-from embit.transaction import Transaction, TransactionInput, TransactionOutput
-from fastapi import Depends, HTTPException, Query, Request
-
-from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
-
-from . import watchonly_ext
-from .crud import (
- create_config,
- create_fresh_addresses,
- create_watch_wallet,
- delete_addresses_for_wallet,
- delete_watch_wallet,
- get_addresses,
- get_config,
- get_fresh_address,
- get_watch_wallet,
- get_watch_wallets,
- update_address,
- update_config,
- update_watch_wallet,
-)
-from .helpers import parse_key
-from .models import (
- Config,
- CreatePsbt,
- CreateWallet,
- ExtractPsbt,
- SerializedTransaction,
- SignedTransaction,
- WalletAccount,
-)
-
-###################WALLETS#############################
-
-
-@watchonly_ext.get("/api/v1/wallet")
-async def api_wallets_retrieve(
- network: str = Query("Mainnet"), wallet: WalletTypeInfo = Depends(get_key_type)
-):
-
- try:
- return [
- wallet.dict()
- for wallet in await get_watch_wallets(wallet.wallet.user, network)
- ]
- except:
- return []
-
-
-@watchonly_ext.get("/api/v1/wallet/{wallet_id}", dependencies=[Depends(get_key_type)])
-async def api_wallet_retrieve(wallet_id: str):
- w_wallet = await get_watch_wallet(wallet_id)
-
- if not w_wallet:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
- )
-
- return w_wallet.dict()
-
-
-@watchonly_ext.post("/api/v1/wallet")
-async def api_wallet_create_or_update(
- data: CreateWallet, w: WalletTypeInfo = Depends(require_admin_key)
-):
- try:
- descriptor, network = parse_key(data.masterpub)
- assert network
- if data.network != network["name"]:
- raise ValueError(
- "Account network error. This account is for '{}'".format(
- network["name"]
- )
- )
-
- new_wallet = WalletAccount(
- id="none",
- masterpub=data.masterpub,
- fingerprint=descriptor.keys[0].fingerprint.hex(),
- type=descriptor.scriptpubkey_type(),
- title=data.title,
- address_no=-1, # so fresh address on empty wallet can get address with index 0
- balance=0,
- network=network["name"],
- meta=data.meta,
- )
-
- wallets = await get_watch_wallets(w.wallet.user, network["name"])
- existing_wallet = next(
- (
- ew
- for ew in wallets
- if ew.fingerprint == new_wallet.fingerprint
- and ew.network == new_wallet.network
- and ew.masterpub == new_wallet.masterpub
- ),
- None,
- )
- if existing_wallet:
- raise ValueError(
- "Account '{}' has the same master pulic key".format(
- existing_wallet.title
- )
- )
-
- wallet = await create_watch_wallet(w.wallet.user, new_wallet)
-
- await api_get_addresses(wallet.id, w)
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
-
- config = await get_config(w.wallet.user)
- if not config:
- await create_config(user=w.wallet.user)
- return wallet.dict()
-
-
-@watchonly_ext.delete(
- "/api/v1/wallet/{wallet_id}", dependencies=[Depends(require_admin_key)]
-)
-async def api_wallet_delete(wallet_id: str):
- wallet = await get_watch_wallet(wallet_id)
-
- if not wallet:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
- )
-
- await delete_watch_wallet(wallet_id)
- await delete_addresses_for_wallet(wallet_id)
-
- return "", HTTPStatus.NO_CONTENT
-
-
-#############################ADDRESSES##########################
-
-
-@watchonly_ext.get("/api/v1/address/{wallet_id}", dependencies=[Depends(get_key_type)])
-async def api_fresh_address(wallet_id: str):
- address = await get_fresh_address(wallet_id)
- assert address
- return address.dict()
-
-
-@watchonly_ext.put("/api/v1/address/{id}", dependencies=[Depends(require_admin_key)])
-async def api_update_address(id: str, req: Request):
- body = await req.json()
- params = {}
- # amout is only updated if the address has history
- if "amount" in body:
- params["amount"] = int(body["amount"])
- params["has_activity"] = True
-
- if "note" in body:
- params["note"] = body["note"]
-
- address = await update_address(**params, id=id)
- assert address
-
- wallet = (
- await get_watch_wallet(address.wallet)
- if address.branch_index == 0 and address.amount != 0
- else None
- )
-
- if wallet and wallet.address_no < address.address_index:
- await update_watch_wallet(
- address.wallet, **{"address_no": address.address_index}
- )
- return address
-
-
-@watchonly_ext.get("/api/v1/addresses/{wallet_id}")
-async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
- wallet = await get_watch_wallet(wallet_id)
- if not wallet:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
- )
-
- addresses = await get_addresses(wallet_id)
- config = await get_config(w.wallet.user)
- assert config
-
- if not addresses:
- await create_fresh_addresses(wallet_id, 0, config.receive_gap_limit)
- await create_fresh_addresses(wallet_id, 0, config.change_gap_limit, True)
- addresses = await get_addresses(wallet_id)
-
- receive_addresses = list(filter(lambda addr: addr.branch_index == 0, addresses))
- change_addresses = list(filter(lambda addr: addr.branch_index == 1, addresses))
-
- last_receive_address = list(
- filter(lambda addr: addr.has_activity, receive_addresses)
- )[-1:]
- last_change_address = list(
- filter(lambda addr: addr.has_activity, change_addresses)
- )[-1:]
-
- if last_receive_address:
- current_index = receive_addresses[-1].address_index
- address_index = last_receive_address[0].address_index
- await create_fresh_addresses(
- wallet_id, current_index + 1, address_index + config.receive_gap_limit + 1
- )
-
- if last_change_address:
- current_index = change_addresses[-1].address_index
- address_index = last_change_address[0].address_index
- await create_fresh_addresses(
- wallet_id,
- current_index + 1,
- address_index + config.change_gap_limit + 1,
- True,
- )
-
- addresses = await get_addresses(wallet_id)
- return [address.dict() for address in addresses]
-
-
-#############################PSBT##########################
-
-
-@watchonly_ext.post("/api/v1/psbt", dependencies=[Depends(require_admin_key)])
-async def api_psbt_create(data: CreatePsbt):
- try:
- vin = [
- TransactionInput(bytes.fromhex(inp.tx_id), inp.vout) for inp in data.inputs
- ]
- vout = [
- TransactionOutput(out.amount, script.address_to_scriptpubkey(out.address))
- for out in data.outputs
- ]
-
- descriptors = {}
- for _, masterpub in enumerate(data.masterpubs):
- descriptors[masterpub.id] = parse_key(masterpub.public_key)
-
- inputs_extra: List[dict] = []
-
- for i, inp in enumerate(data.inputs):
- bip32_derivations = {}
- descriptor = descriptors[inp.wallet][0]
- d = descriptor.derive(inp.address_index, inp.branch_index)
- for k in d.keys:
- bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
- k.origin.fingerprint, k.origin.derivation
- )
- inputs_extra.append(
- {
- "bip32_derivations": bip32_derivations,
- "non_witness_utxo": Transaction.from_string(inp.tx_hex),
- }
- )
-
- tx = Transaction(vin=vin, vout=vout)
- psbt = PSBT(tx)
-
- for i, inp_extra in enumerate(inputs_extra):
- psbt.inputs[i].bip32_derivations = inp_extra["bip32_derivations"]
- psbt.inputs[i].non_witness_utxo = inp_extra.get("non_witness_utxo", None)
-
- outputs_extra = []
- bip32_derivations = {}
- for i, out in enumerate(data.outputs):
- if out.branch_index == 1:
- assert out.wallet
- descriptor = descriptors[out.wallet][0]
- d = descriptor.derive(out.address_index, out.branch_index)
- for k in d.keys:
- bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
- k.origin.fingerprint, k.origin.derivation
- )
- outputs_extra.append({"bip32_derivations": bip32_derivations})
-
- for i, out_extra in enumerate(outputs_extra):
- psbt.outputs[i].bip32_derivations = out_extra["bip32_derivations"]
-
- return psbt.to_string()
-
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
-
-
-@watchonly_ext.put("/api/v1/psbt/utxos")
-async def api_psbt_utxos_tx(
- req: Request, w: WalletTypeInfo = Depends(require_admin_key)
-):
- """Extract previous unspent transaction outputs (tx_id, vout) from PSBT"""
-
- body = await req.json()
- try:
- psbt = PSBT.from_base64(body["psbtBase64"])
- res = []
- for _, inp in enumerate(psbt.inputs):
- res.append({"tx_id": inp.txid.hex(), "vout": inp.vout})
-
- return res
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
-
-
-@watchonly_ext.put("/api/v1/psbt/extract", dependencies=[Depends(require_admin_key)])
-async def api_psbt_extract_tx(data: ExtractPsbt):
- network = NETWORKS["main"] if data.network == "Mainnet" else NETWORKS["test"]
- try:
- psbt = PSBT.from_base64(data.psbtBase64)
- for i, inp in enumerate(data.inputs):
- psbt.inputs[i].non_witness_utxo = Transaction.from_string(inp.tx_hex)
-
- final_psbt = finalizer.finalize_psbt(psbt)
- if not final_psbt:
- raise ValueError("PSBT cannot be finalized!")
-
- tx_hex = final_psbt.to_string()
- transaction = Transaction.from_string(tx_hex)
- tx = {
- "locktime": transaction.locktime,
- "version": transaction.version,
- "outputs": [],
- "fee": psbt.fee(),
- }
-
- for out in transaction.vout:
- tx["outputs"].append(
- {"amount": out.value, "address": out.script_pubkey.address(network)}
- )
- signed_tx = SignedTransaction(tx_hex=tx_hex, tx_json=json.dumps(tx))
- return signed_tx.dict()
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
-
-
-@watchonly_ext.post("/api/v1/tx")
-async def api_tx_broadcast(
- data: SerializedTransaction, w: WalletTypeInfo = Depends(require_admin_key)
-):
- try:
- config = await get_config(w.wallet.user)
- if not config:
- raise ValueError(
- "Cannot broadcast transaction. Mempool endpoint not defined!"
- )
-
- endpoint = (
- config.mempool_endpoint
- if config.network == "Mainnet"
- else config.mempool_endpoint + "/testnet"
- )
- async with httpx.AsyncClient() as client:
- r = await client.post(endpoint + "/api/tx", content=data.tx_hex)
- r.raise_for_status()
- tx_id = r.text
- return tx_id
- except Exception as e:
- raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
-
-
-#############################CONFIG##########################
-
-
-@watchonly_ext.put("/api/v1/config")
-async def api_update_config(
- data: Config, w: WalletTypeInfo = Depends(require_admin_key)
-):
- config = await update_config(data, user=w.wallet.user)
- assert config
- return config.dict()
-
-
-@watchonly_ext.get("/api/v1/config")
-async def api_get_config(w: WalletTypeInfo = Depends(get_key_type)):
- config = await get_config(w.wallet.user)
- if not config:
- config = await create_config(user=w.wallet.user)
- return config.dict()
diff --git a/lnbits/extensions/withdraw/README.md b/lnbits/extensions/withdraw/README.md
deleted file mode 100644
index fce2c6e5..00000000
--- a/lnbits/extensions/withdraw/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# LNURLw
-
-## Create a static QR code people can use to withdraw funds from a Lightning Network wallet
-
-LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL withdraw is the permission for someone to pull a certain amount of funds from a lightning wallet.
-
-The most common use case for an LNURL withdraw is a faucet, although it is a very powerful technology, with much further reaching implications. For example, an LNURL withdraw could be minted to pay for a subscription service. Or you can have a LNURLw as an offline Lightning wallet (a pre paid "card"), you use to pay for something without having to even reach your smartphone.
-
-LNURL withdraw is a **very powerful tool** and should not have his use limited to just faucet applications. With LNURL withdraw, you have the ability to give someone the right to spend a range, once or multiple times. **This functionality has not existed in money before**.
-
-[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
-
-## Usage
-
-#### Quick Vouchers
-
-LNbits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes that you can print and distribute as rewards, onboarding people into Lightning Network, gifts, etc...
-
-1. Create Quick Vouchers\
- 
- - select wallet
- - set the amount each voucher will allow someone to withdraw
- - set the amount of vouchers you want to create - _have in mind you need to have a balance on the wallet that supports the amount \* number of vouchers_
-2. You can now print, share, display your LNURLw links or QR codes\
- 
- - on details you can print the vouchers\
- 
- - every printed LNURLw QR code is unique, it can only be used once
-3. Bonus: you can use an LNbits themed voucher, or use a custom one. There's a _template.svg_ file in `static/images` folder if you want to create your own.\
- 
-
-#### Advanced
-
-1. Create the Advanced LNURLw\
- 
- - set the wallet
- - set a title for the LNURLw (it will show up in users wallet)
- - define the minimum and maximum a user can withdraw, if you want a fixed amount set them both to an equal value
- - set how many times can the LNURLw be scanned, if it's a one time use or it can be scanned 100 times
- - LNbits has the "_Time between withdraws_" setting, you can define how long the LNURLw will be unavailable between scans
- - you can set the time in _seconds, minutes or hours_
- - the "_Use unique withdraw QR..._" reduces the chance of your LNURL withdraw being exploited and depleted by one person, by generating a new QR code every time it's scanned
-2. Print, share or display your LNURLw link or it's QR code\
- 
-
-**LNbits bonus:** If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with their smartphone camera, or a QR scanner app, they can follow the link provided to claim their satoshis and get an instant LNbits wallet!
-
-
diff --git a/lnbits/extensions/withdraw/__init__.py b/lnbits/extensions/withdraw/__init__.py
deleted file mode 100644
index cb5eb9c4..00000000
--- a/lnbits/extensions/withdraw/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from fastapi import APIRouter
-from fastapi.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-
-db = Database("ext_withdraw")
-
-withdraw_static_files = [
- {
- "path": "/withdraw/static",
- "app": StaticFiles(packages=[("lnbits", "extensions/withdraw/static")]),
- "name": "withdraw_static",
- }
-]
-
-
-withdraw_ext: APIRouter = APIRouter(prefix="/withdraw", tags=["withdraw"])
-
-
-def withdraw_renderer():
- return template_renderer(["lnbits/extensions/withdraw/templates"])
-
-
-from .lnurl import * # noqa: F401,F403
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/withdraw/config.json b/lnbits/extensions/withdraw/config.json
deleted file mode 100644
index c22d69c8..00000000
--- a/lnbits/extensions/withdraw/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "LNURLw",
- "short_description": "Make LNURL withdraw links",
- "tile": "/withdraw/static/image/lnurl-withdraw.png",
- "contributors": ["arcbtc", "eillarra"]
-}
diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py
deleted file mode 100644
index 83dd0593..00000000
--- a/lnbits/extensions/withdraw/crud.py
+++ /dev/null
@@ -1,173 +0,0 @@
-from datetime import datetime
-from typing import List, Optional, Union
-
-import shortuuid
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import CreateWithdrawData, HashCheck, WithdrawLink
-
-
-async def create_withdraw_link(
- data: CreateWithdrawData, wallet_id: str
-) -> WithdrawLink:
- link_id = urlsafe_short_hash()[:6]
- available_links = ",".join([str(i) for i in range(data.uses)])
- await db.execute(
- """
- INSERT INTO withdraw.withdraw_link (
- id,
- wallet,
- title,
- min_withdrawable,
- max_withdrawable,
- uses,
- wait_time,
- is_unique,
- unique_hash,
- k1,
- open_time,
- usescsv,
- webhook_url,
- webhook_headers,
- webhook_body,
- custom_url
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- link_id,
- wallet_id,
- data.title,
- data.min_withdrawable,
- data.max_withdrawable,
- data.uses,
- data.wait_time,
- int(data.is_unique),
- urlsafe_short_hash(),
- urlsafe_short_hash(),
- int(datetime.now().timestamp()) + data.wait_time,
- available_links,
- data.webhook_url,
- data.webhook_headers,
- data.webhook_body,
- data.custom_url,
- ),
- )
- link = await get_withdraw_link(link_id, 0)
- assert link, "Newly created link couldn't be retrieved"
- return link
-
-
-async def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]:
- row = await db.fetchone(
- "SELECT * FROM withdraw.withdraw_link WHERE id = ?", (link_id,)
- )
- if not row:
- return None
-
- link = dict(**row)
- link["number"] = num
-
- return WithdrawLink.parse_obj(link)
-
-
-async def get_withdraw_link_by_hash(unique_hash: str, num=0) -> Optional[WithdrawLink]:
- row = await db.fetchone(
- "SELECT * FROM withdraw.withdraw_link WHERE unique_hash = ?", (unique_hash,)
- )
- if not row:
- return None
-
- link = dict(**row)
- link["number"] = num
-
- return WithdrawLink.parse_obj(link)
-
-
-async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[WithdrawLink]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM withdraw.withdraw_link WHERE wallet IN ({q})", (*wallet_ids,)
- )
- return [WithdrawLink(**row) for row in rows]
-
-
-async def remove_unique_withdraw_link(link: WithdrawLink, unique_hash: str) -> None:
- unique_links = [
- x.strip()
- for x in link.usescsv.split(",")
- if unique_hash != shortuuid.uuid(name=link.id + link.unique_hash + x.strip())
- ]
- await update_withdraw_link(
- link.id,
- usescsv=",".join(unique_links),
- )
-
-
-async def increment_withdraw_link(link: WithdrawLink) -> None:
- await update_withdraw_link(
- link.id,
- used=link.used + 1,
- open_time=link.wait_time + int(datetime.now().timestamp()),
- )
-
-
-async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]:
- if "is_unique" in kwargs:
- kwargs["is_unique"] = int(kwargs["is_unique"])
- q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
- await db.execute(
- f"UPDATE withdraw.withdraw_link SET {q} WHERE id = ?",
- (*kwargs.values(), link_id),
- )
- row = await db.fetchone(
- "SELECT * FROM withdraw.withdraw_link WHERE id = ?", (link_id,)
- )
- return WithdrawLink(**row) if row else None
-
-
-async def delete_withdraw_link(link_id: str) -> None:
- await db.execute("DELETE FROM withdraw.withdraw_link WHERE id = ?", (link_id,))
-
-
-def chunks(lst, n):
- for i in range(0, len(lst), n):
- yield lst[i : i + n]
-
-
-async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
- await db.execute(
- """
- INSERT INTO withdraw.hash_check (
- id,
- lnurl_id
- )
- VALUES (?, ?)
- """,
- (the_hash, lnurl_id),
- )
- hashCheck = await get_hash_check(the_hash, lnurl_id)
- return hashCheck
-
-
-async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
- rowid = await db.fetchone(
- "SELECT * FROM withdraw.hash_check WHERE id = ?", (the_hash,)
- )
- rowlnurl = await db.fetchone(
- "SELECT * FROM withdraw.hash_check WHERE lnurl_id = ?", (lnurl_id,)
- )
- if not rowlnurl:
- await create_hash_check(the_hash, lnurl_id)
- return HashCheck(lnurl=True, hash=False)
- else:
- if not rowid:
- await create_hash_check(the_hash, lnurl_id)
- return HashCheck(lnurl=True, hash=False)
- else:
- return HashCheck(lnurl=True, hash=True)
diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py
deleted file mode 100644
index 5ef521fa..00000000
--- a/lnbits/extensions/withdraw/lnurl.py
+++ /dev/null
@@ -1,200 +0,0 @@
-import json
-from datetime import datetime
-from http import HTTPStatus
-
-import httpx
-import shortuuid
-from fastapi import HTTPException, Query, Request, Response
-from loguru import logger
-
-from lnbits.core.crud import update_payment_extra
-from lnbits.core.services import pay_invoice
-
-from . import withdraw_ext
-from .crud import (
- get_withdraw_link_by_hash,
- increment_withdraw_link,
- remove_unique_withdraw_link,
-)
-from .models import WithdrawLink
-
-
-@withdraw_ext.get(
- "/api/v1/lnurl/{unique_hash}",
- response_class=Response,
- name="withdraw.api_lnurl_response",
-)
-async def api_lnurl_response(request: Request, unique_hash):
- link = await get_withdraw_link_by_hash(unique_hash)
-
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist."
- )
-
- if link.is_spent:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent."
- )
- url = request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash)
- withdrawResponse = {
- "tag": "withdrawRequest",
- "callback": url,
- "k1": link.k1,
- "minWithdrawable": link.min_withdrawable * 1000,
- "maxWithdrawable": link.max_withdrawable * 1000,
- "defaultDescription": link.title,
- "webhook_url": link.webhook_url,
- "webhook_headers": link.webhook_headers,
- "webhook_body": link.webhook_body,
- }
-
- return json.dumps(withdrawResponse)
-
-
-@withdraw_ext.get(
- "/api/v1/lnurl/cb/{unique_hash}",
- name="withdraw.api_lnurl_callback",
- summary="lnurl withdraw callback",
- description="""
- This endpoints allows you to put unique_hash, k1
- and a payment_request to get your payment_request paid.
- """,
- response_description="JSON with status",
- responses={
- 200: {"description": "status: OK"},
- 400: {"description": "k1 is wrong or link open time or withdraw not working."},
- 404: {"description": "withdraw link not found."},
- 405: {"description": "withdraw link is spent."},
- },
-)
-async def api_lnurl_callback(
- unique_hash,
- k1: str = Query(...),
- pr: str = Query(...),
- id_unique_hash=None,
-):
- link = await get_withdraw_link_by_hash(unique_hash)
- now = int(datetime.now().timestamp())
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
- )
-
- if link.is_spent:
- raise HTTPException(
- status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="withdraw is spent."
- )
-
- if link.k1 != k1:
- raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="k1 is wrong.")
-
- if now < link.open_time:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST,
- detail=f"wait link open_time {link.open_time - now} seconds.",
- )
-
- if id_unique_hash:
- if check_unique_link(link, id_unique_hash):
- await remove_unique_withdraw_link(link, id_unique_hash)
- else:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
- )
-
- try:
- payment_hash = await pay_invoice(
- wallet_id=link.wallet,
- payment_request=pr,
- max_sat=link.max_withdrawable,
- extra={"tag": "withdraw"},
- )
- await increment_withdraw_link(link)
- if link.webhook_url:
- await dispatch_webhook(link, payment_hash, pr)
- return {"status": "OK"}
- except Exception as e:
- raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}"
- )
-
-
-def check_unique_link(link: WithdrawLink, unique_hash: str) -> bool:
- return any(
- unique_hash == shortuuid.uuid(name=link.id + link.unique_hash + x.strip())
- for x in link.usescsv.split(",")
- )
-
-
-async def dispatch_webhook(
- link: WithdrawLink, payment_hash: str, payment_request: str
-) -> None:
- async with httpx.AsyncClient() as client:
- try:
- r: httpx.Response = await client.post(
- link.webhook_url,
- json={
- "payment_hash": payment_hash,
- "payment_request": payment_request,
- "lnurlw": link.id,
- "body": json.loads(link.webhook_body) if link.webhook_body else "",
- },
- headers=json.loads(link.webhook_headers)
- if link.webhook_headers
- else None,
- timeout=40,
- )
- await update_payment_extra(
- payment_hash=payment_hash,
- extra={
- "wh_success": r.is_success,
- "wh_message": r.reason_phrase,
- "wh_response": r.text,
- },
- outgoing=True,
- )
- except Exception as exc:
- # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid
- logger.error("Caught exception when dispatching webhook url: " + str(exc))
- await update_payment_extra(
- payment_hash=payment_hash,
- extra={"wh_success": False, "wh_message": str(exc)},
- outgoing=True,
- )
-
-
-# FOR LNURLs WHICH ARE UNIQUE
-@withdraw_ext.get(
- "/api/v1/lnurl/{unique_hash}/{id_unique_hash}",
- response_class=Response,
- name="withdraw.api_lnurl_multi_response",
-)
-async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash):
- link = await get_withdraw_link_by_hash(unique_hash)
-
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
- )
-
- if link.is_spent:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent."
- )
-
- if not check_unique_link(link, id_unique_hash):
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
- )
-
- url = request.url_for("withdraw.api_lnurl_callback", unique_hash=link.unique_hash)
- withdrawResponse = {
- "tag": "withdrawRequest",
- "callback": url + "?id_unique_hash=" + id_unique_hash,
- "k1": link.k1,
- "minWithdrawable": link.min_withdrawable * 1000,
- "maxWithdrawable": link.max_withdrawable * 1000,
- "defaultDescription": link.title,
- }
- return json.dumps(withdrawResponse)
diff --git a/lnbits/extensions/withdraw/migrations.py b/lnbits/extensions/withdraw/migrations.py
deleted file mode 100644
index 95805ae7..00000000
--- a/lnbits/extensions/withdraw/migrations.py
+++ /dev/null
@@ -1,134 +0,0 @@
-async def m001_initial(db):
- """
- Creates an improved withdraw table and migrates the existing data.
- """
- await db.execute(
- f"""
- CREATE TABLE withdraw.withdraw_links (
- id TEXT PRIMARY KEY,
- wallet TEXT,
- title TEXT,
- min_withdrawable {db.big_int} DEFAULT 1,
- max_withdrawable {db.big_int} DEFAULT 1,
- uses INTEGER DEFAULT 1,
- wait_time INTEGER,
- is_unique INTEGER DEFAULT 0,
- unique_hash TEXT UNIQUE,
- k1 TEXT,
- open_time INTEGER,
- used INTEGER DEFAULT 0,
- usescsv TEXT
- );
- """
- )
-
-
-async def m002_change_withdraw_table(db):
- """
- Creates an improved withdraw table and migrates the existing data.
- """
- await db.execute(
- f"""
- CREATE TABLE withdraw.withdraw_link (
- id TEXT PRIMARY KEY,
- wallet TEXT,
- title TEXT,
- min_withdrawable {db.big_int} DEFAULT 1,
- max_withdrawable {db.big_int} DEFAULT 1,
- uses INTEGER DEFAULT 1,
- wait_time INTEGER,
- is_unique INTEGER DEFAULT 0,
- unique_hash TEXT UNIQUE,
- k1 TEXT,
- open_time INTEGER,
- used INTEGER DEFAULT 0,
- usescsv TEXT
- );
- """
- )
-
- for row in [
- list(row) for row in await db.fetchall("SELECT * FROM withdraw.withdraw_links")
- ]:
- usescsv = ""
-
- for i in range(row[5]):
- if row[7]:
- usescsv += "," + str(i + 1)
- else:
- usescsv += "," + str(1)
- usescsv = usescsv[1:]
- await db.execute(
- """
- INSERT INTO withdraw.withdraw_link (
- id,
- wallet,
- title,
- min_withdrawable,
- max_withdrawable,
- uses,
- wait_time,
- is_unique,
- unique_hash,
- k1,
- open_time,
- used,
- usescsv
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- row[0],
- row[1],
- row[2],
- row[3],
- row[4],
- row[5],
- row[6],
- row[7],
- row[8],
- row[9],
- row[10],
- row[11],
- usescsv,
- ),
- )
- await db.execute("DROP TABLE withdraw.withdraw_links")
-
-
-async def m003_make_hash_check(db):
- """
- Creates a hash check table.
- """
- await db.execute(
- """
- CREATE TABLE withdraw.hash_check (
- id TEXT PRIMARY KEY,
- lnurl_id TEXT
- );
- """
- )
-
-
-async def m004_webhook_url(db):
- """
- Adds webhook_url
- """
- await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_url TEXT;")
-
-
-async def m005_add_custom_print_design(db):
- """
- Adds custom print design
- """
- await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;")
-
-
-async def m006_webhook_headers_and_body(db):
- """
- Add headers and body to webhooks
- """
- await db.execute(
- "ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_headers TEXT;"
- )
- await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_body TEXT;")
diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py
deleted file mode 100644
index 49421a79..00000000
--- a/lnbits/extensions/withdraw/models.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import shortuuid
-from fastapi import Query
-from lnurl import Lnurl, LnurlWithdrawResponse
-from lnurl import encode as lnurl_encode
-from lnurl.models import ClearnetUrl, MilliSatoshi
-from pydantic import BaseModel
-from starlette.requests import Request
-
-
-class CreateWithdrawData(BaseModel):
- title: str = Query(...)
- min_withdrawable: int = Query(..., ge=1)
- max_withdrawable: int = Query(..., ge=1)
- uses: int = Query(..., ge=1)
- wait_time: int = Query(..., ge=1)
- is_unique: bool
- webhook_url: str = Query(None)
- webhook_headers: str = Query(None)
- webhook_body: str = Query(None)
- custom_url: str = Query(None)
-
-
-class WithdrawLink(BaseModel):
- id: str
- wallet: str = Query(None)
- title: str = Query(None)
- min_withdrawable: int = Query(0)
- max_withdrawable: int = Query(0)
- uses: int = Query(0)
- wait_time: int = Query(0)
- is_unique: bool = Query(False)
- unique_hash: str = Query(0)
- k1: str = Query(None)
- open_time: int = Query(0)
- used: int = Query(0)
- usescsv: str = Query(None)
- number: int = Query(0)
- webhook_url: str = Query(None)
- webhook_headers: str = Query(None)
- webhook_body: str = Query(None)
- custom_url: str = Query(None)
-
- @property
- def is_spent(self) -> bool:
- return self.used >= self.uses
-
- def lnurl(self, req: Request) -> Lnurl:
- if self.is_unique:
- usescssv = self.usescsv.split(",")
- tohash = self.id + self.unique_hash + usescssv[self.number]
- multihash = shortuuid.uuid(name=tohash)
- url = req.url_for(
- "withdraw.api_lnurl_multi_response",
- unique_hash=self.unique_hash,
- id_unique_hash=multihash,
- )
- else:
- url = req.url_for(
- "withdraw.api_lnurl_response", unique_hash=self.unique_hash
- )
-
- return lnurl_encode(url)
-
- def lnurl_response(self, req: Request) -> LnurlWithdrawResponse:
- url = req.url_for(
- name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash
- )
- return LnurlWithdrawResponse(
- callback=ClearnetUrl(url, scheme="https"),
- k1=self.k1,
- minWithdrawable=MilliSatoshi(self.min_withdrawable * 1000),
- maxWithdrawable=MilliSatoshi(self.max_withdrawable * 1000),
- defaultDescription=self.title,
- )
-
-
-class HashCheck(BaseModel):
- hash: bool
- lnurl: bool
diff --git a/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png b/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png
deleted file mode 100644
index 4f036423..00000000
Binary files a/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png and /dev/null differ
diff --git a/lnbits/extensions/withdraw/static/js/index.js b/lnbits/extensions/withdraw/static/js/index.js
deleted file mode 100644
index ced78439..00000000
--- a/lnbits/extensions/withdraw/static/js/index.js
+++ /dev/null
@@ -1,323 +0,0 @@
-/* global Vue, VueQrcode, _, Quasar, LOCALE, windowMixin, LNbits */
-
-Vue.component(VueQrcode.name, VueQrcode)
-
-var locationPath = [
- window.location.protocol,
- '//',
- window.location.host,
- window.location.pathname
-].join('')
-
-var mapWithdrawLink = function (obj) {
- obj._data = _.clone(obj)
- obj.date = Quasar.utils.date.formatDate(
- new Date(obj.time * 1000),
- 'YYYY-MM-DD HH:mm'
- )
- obj.min_fsat = new Intl.NumberFormat(LOCALE).format(obj.min_withdrawable)
- obj.max_fsat = new Intl.NumberFormat(LOCALE).format(obj.max_withdrawable)
- obj.uses_left = obj.uses - obj.used
- obj.print_url = [locationPath, 'print/', obj.id].join('')
- obj.withdraw_url = [locationPath, obj.id].join('')
- obj._data.use_custom = Boolean(obj.custom_url)
- return obj
-}
-
-const CUSTOM_URL = '/static/images/default_voucher.png'
-
-new Vue({
- el: '#vue',
- mixins: [windowMixin],
- data: function () {
- return {
- checker: null,
- withdrawLinks: [],
- withdrawLinksTable: {
- columns: [
- {name: 'id', align: 'left', label: 'ID', field: 'id'},
- {name: 'title', align: 'left', label: 'Title', field: 'title'},
- {
- name: 'wait_time',
- align: 'right',
- label: 'Wait',
- field: 'wait_time'
- },
- {
- name: 'uses_left',
- align: 'right',
- label: 'Uses left',
- field: 'uses_left'
- },
- {name: 'min', align: 'right', label: 'Min (sat)', field: 'min_fsat'},
- {name: 'max', align: 'right', label: 'Max (sat)', field: 'max_fsat'}
- ],
- pagination: {
- rowsPerPage: 10
- }
- },
- nfcTagWriting: false,
- formDialog: {
- show: false,
- secondMultiplier: 'seconds',
- secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
- data: {
- is_unique: false,
- use_custom: false,
- has_webhook: false
- }
- },
- simpleformDialog: {
- show: false,
- data: {
- is_unique: true,
- use_custom: false,
- title: 'Vouchers',
- min_withdrawable: 0,
- wait_time: 1
- }
- },
- qrCodeDialog: {
- show: false,
- data: null
- }
- }
- },
- computed: {
- sortedWithdrawLinks: function () {
- return this.withdrawLinks.sort(function (a, b) {
- return b.uses_left - a.uses_left
- })
- }
- },
- methods: {
- getWithdrawLinks: function () {
- var self = this
-
- LNbits.api
- .request(
- 'GET',
- '/withdraw/api/v1/links?all_wallets=true',
- this.g.user.wallets[0].inkey
- )
- .then(function (response) {
- self.withdrawLinks = response.data.map(function (obj) {
- return mapWithdrawLink(obj)
- })
- })
- .catch(function (error) {
- clearInterval(self.checker)
- LNbits.utils.notifyApiError(error)
- })
- },
- closeFormDialog: function () {
- this.formDialog.data = {
- is_unique: false,
- use_custom: false
- }
- },
- simplecloseFormDialog: function () {
- this.simpleformDialog.data = {
- is_unique: false,
- use_custom: false
- }
- },
- openQrCodeDialog: function (linkId) {
- var link = _.findWhere(this.withdrawLinks, {id: linkId})
-
- this.qrCodeDialog.data = _.clone(link)
- this.qrCodeDialog.data.url =
- window.location.protocol + '//' + window.location.host
- this.qrCodeDialog.show = true
- },
- openUpdateDialog: function (linkId) {
- var link = _.findWhere(this.withdrawLinks, {id: linkId})
- this.formDialog.data = _.clone(link._data)
- this.formDialog.show = true
- },
- sendFormData: function () {
- var wallet = _.findWhere(this.g.user.wallets, {
- id: this.formDialog.data.wallet
- })
- var data = _.omit(this.formDialog.data, 'wallet')
-
- if (!data.use_custom) {
- data.custom_url = null
- }
-
- if (data.use_custom && !data?.custom_url) {
- data.custom_url = CUSTOM_URL
- }
-
- data.wait_time =
- data.wait_time *
- {
- seconds: 1,
- minutes: 60,
- hours: 3600
- }[this.formDialog.secondMultiplier]
- if (data.id) {
- this.updateWithdrawLink(wallet, data)
- } else {
- this.createWithdrawLink(wallet, data)
- }
- },
- simplesendFormData: function () {
- var wallet = _.findWhere(this.g.user.wallets, {
- id: this.simpleformDialog.data.wallet
- })
- var data = _.omit(this.simpleformDialog.data, 'wallet')
-
- data.wait_time = 1
- data.min_withdrawable = data.max_withdrawable
- data.title = 'vouchers'
- data.is_unique = true
-
- if (!data.use_custom) {
- data.custom_url = null
- }
-
- if (data.use_custom && !data?.custom_url) {
- data.custom_url = '/static/images/default_voucher.png'
- }
-
- if (data.id) {
- this.updateWithdrawLink(wallet, data)
- } else {
- this.createWithdrawLink(wallet, data)
- }
- },
- updateWithdrawLink: function (wallet, data) {
- var self = this
- const body = _.pick(
- data,
- 'title',
- 'min_withdrawable',
- 'max_withdrawable',
- 'uses',
- 'wait_time',
- 'is_unique',
- 'webhook_url',
- 'webhook_headers',
- 'webhook_body',
- 'custom_url'
- )
-
- if (data.has_webhook) {
- body = {
- ...body,
- webhook_url: data.webhook_url,
- webhook_headers: data.webhook_headers,
- webhook_body: data.webhook_body
- }
- }
-
- LNbits.api
- .request(
- 'PUT',
- '/withdraw/api/v1/links/' + data.id,
- wallet.adminkey,
- body
- )
- .then(function (response) {
- self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) {
- return obj.id === data.id
- })
- self.withdrawLinks.push(mapWithdrawLink(response.data))
- self.formDialog.show = false
- })
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
- })
- },
- createWithdrawLink: function (wallet, data) {
- var self = this
-
- LNbits.api
- .request('POST', '/withdraw/api/v1/links', wallet.adminkey, data)
- .then(function (response) {
- self.withdrawLinks.push(mapWithdrawLink(response.data))
- self.formDialog.show = false
- self.simpleformDialog.show = false
- })
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
- })
- },
- deleteWithdrawLink: function (linkId) {
- var self = this
- var link = _.findWhere(this.withdrawLinks, {id: linkId})
-
- LNbits.utils
- .confirmDialog('Are you sure you want to delete this withdraw link?')
- .onOk(function () {
- LNbits.api
- .request(
- 'DELETE',
- '/withdraw/api/v1/links/' + linkId,
- _.findWhere(self.g.user.wallets, {id: link.wallet}).adminkey
- )
- .then(function (response) {
- self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) {
- return obj.id === linkId
- })
- })
- .catch(function (error) {
- LNbits.utils.notifyApiError(error)
- })
- })
- },
- writeNfcTag: async function (lnurl) {
- try {
- if (typeof NDEFReader == 'undefined') {
- throw {
- toString: function () {
- return 'NFC not supported on this device or browser.'
- }
- }
- }
-
- const ndef = new NDEFReader()
-
- this.nfcTagWriting = true
- this.$q.notify({
- message: 'Tap your NFC tag to write the LNURL-withdraw link to it.'
- })
-
- await ndef.write({
- records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}]
- })
-
- this.nfcTagWriting = false
- this.$q.notify({
- type: 'positive',
- message: 'NFC tag written successfully.'
- })
- } catch (error) {
- this.nfcTagWriting = false
- this.$q.notify({
- type: 'negative',
- message: error
- ? error.toString()
- : 'An unexpected error has occurred.'
- })
- }
- },
- exportCSV() {
- LNbits.utils.exportCSV(
- this.withdrawLinksTable.columns,
- this.withdrawLinks,
- 'withdraw-links'
- )
- }
- },
- created: function () {
- if (this.g.user.wallets.length) {
- var getWithdrawLinks = this.getWithdrawLinks
- getWithdrawLinks()
- this.checker = setInterval(function () {
- getWithdrawLinks()
- }, 300000)
- }
- }
-})
diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html
deleted file mode 100644
index ff88189d..00000000
--- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html
+++ /dev/null
@@ -1,204 +0,0 @@
-
-
-
-
-
- GET /withdraw/api/v1/links
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<withdraw_link_object>, ...]
- Curl example
- curl -X GET {{ request.base_url }}withdraw/api/v1/links -H
- "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- GET
- /withdraw/api/v1/links/<withdraw_id>
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 201 CREATED (application/json)
-
- {"lnurl": <string>}
- Curl example
- curl -X GET {{ request.base_url
- }}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{
- user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- POST /withdraw/api/v1/links
- Headers
- {"X-Api-Key": <admin_key>}
- Body (application/json)
- {"title": <string>, "min_withdrawable": <integer>,
- "max_withdrawable": <integer>, "uses": <integer>,
- "wait_time": <integer>, "is_unique": <boolean>,
- "webhook_url": <string>}
-
- Returns 201 CREATED (application/json)
-
- {"lnurl": <string>}
- Curl example
- curl -X POST {{ request.base_url }}withdraw/api/v1/links -d
- '{"title": <string>, "min_withdrawable": <integer>,
- "max_withdrawable": <integer>, "uses": <integer>,
- "wait_time": <integer>, "is_unique": <boolean>,
- "webhook_url": <string>}' -H "Content-type: application/json" -H
- "X-Api-Key: {{ user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- PUT
- /withdraw/api/v1/links/<withdraw_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Body (application/json)
- {"title": <string>, "min_withdrawable": <integer>,
- "max_withdrawable": <integer>, "uses": <integer>,
- "wait_time": <integer>, "is_unique": <boolean>}
-
- Returns 200 OK (application/json)
-
- {"lnurl": <string>}
- Curl example
- curl -X PUT {{ request.base_url
- }}withdraw/api/v1/links/<withdraw_id> -d '{"title":
- <string>, "min_withdrawable": <integer>,
- "max_withdrawable": <integer>, "uses": <integer>,
- "wait_time": <integer>, "is_unique": <boolean>}' -H
- "Content-type: application/json" -H "X-Api-Key: {{
- user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- DELETE
- /withdraw/api/v1/links/<withdraw_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Returns 204 NO CONTENT
-
- Curl example
- curl -X DELETE {{ request.base_url
- }}withdraw/api/v1/links/<withdraw_id> -H "X-Api-Key: {{
- user.wallets[0].adminkey }}"
-
-
-
-
-
-
-
- GET
- /withdraw/api/v1/links/<the_hash>/<lnurl_id>
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 201 CREATED (application/json)
-
- {"status": <bool>}
- Curl example
- curl -X GET {{ request.base_url
- }}withdraw/api/v1/links/<the_hash>/<lnurl_id> -H
- "X-Api-Key: {{ user.wallets[0].inkey }}"
-
-
-
-
-
-
-
- GET
- /withdraw/img/<lnurl_id>
- Curl example
- curl -X GET {{ request.base_url }}withdraw/img/<lnurl_id>"
-
-
-
-
-
diff --git a/lnbits/extensions/withdraw/templates/withdraw/_lnurl.html b/lnbits/extensions/withdraw/templates/withdraw/_lnurl.html
deleted file mode 100644
index f6b52050..00000000
--- a/lnbits/extensions/withdraw/templates/withdraw/_lnurl.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
- WARNING: LNURL must be used over https or TOR
- LNURL is a range of lightning-network standards that allow us to use
- lightning-network differently. An LNURL withdraw is the permission for
- someone to pull a certain amount of funds from a lightning wallet. In
- this extension time is also added - an amount can be withdraw over a
- period of time. A typical use case for an LNURL withdraw is a faucet,
- although it is a very powerful technology, with much further reaching
- implications. For example, an LNURL withdraw could be minted to pay for
- a subscription service.
-
-
- Exploring LNURL and finding use cases, is really helping inform
- lightning protocol development, rather than the protocol dictating how
- lightning-network should be engaged with.
-
- Check
- Awesome LNURL
- for further information.
-
-
-
diff --git a/lnbits/extensions/withdraw/templates/withdraw/csv.html b/lnbits/extensions/withdraw/templates/withdraw/csv.html
deleted file mode 100644
index 62902905..00000000
--- a/lnbits/extensions/withdraw/templates/withdraw/csv.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% extends "print.html" %} {% block page %} {% for page in link %} {% for threes
-in page %} {% for one in threes %} {{one}}, {% endfor %} {% endfor %} {% endfor
-%} {% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/withdraw/templates/withdraw/display.html b/lnbits/extensions/withdraw/templates/withdraw/display.html
deleted file mode 100644
index 3ef545c3..00000000
--- a/lnbits/extensions/withdraw/templates/withdraw/display.html
+++ /dev/null
@@ -1,68 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
- {% if link.is_spent %}
-
Withdraw is spent.
- {% endif %}
-
-
-
-
-
-
-
-
- Copy LNURL
-
-
-
-
-
-
-
-
-
- LNbits LNURL-withdraw link
-
-
- Use a LNURL compatible bitcoin wallet to claim the sats.
-
-
-
-
- {% include "withdraw/_lnurl.html" %}
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html
deleted file mode 100644
index 3ae244e6..00000000
--- a/lnbits/extensions/withdraw/templates/withdraw/index.html
+++ /dev/null
@@ -1,471 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %} {% block page %}
-
-
-
-
- Quick vouchers
- Advanced withdraw link(s)
-
-
-
-
-
-
-
-
Withdraw links
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
-
- shareable link
- embeddable image
- csv list
- view LNURL
-
-
- {{ col.value }}
-
-
-
- Webhook to {{ props.row.webhook_url}}
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} LNURL-withdraw extension
-
-
-
-
-
- {% include "withdraw/_api_docs.html" %}
-
- {% include "withdraw/_lnurl.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Use a custom voucher design
- You can use an LNbits voucher design or a custom
- one
-
-
-
-
-
-
-
-
-
-
- Use unique withdraw QR codes to reduce `assmilking`
-
- This is recommended if you are sharing the links on social
- media or print QR codes.
-
-
-
-
- Update withdraw link
- Create withdraw link
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Use a custom voucher design
- You can use an LNbits voucher design or a custom
- one
-
-
-
-
-
-
- Create vouchers
- Cancel
-
-
-
-
-
-
-
-
-
- {% raw %}
-
-
- ID: {{ qrCodeDialog.data.id }}
- Unique: {{ qrCodeDialog.data.is_unique }}
- (QR code will change after each withdrawal)
- Max. withdrawable: {{
- qrCodeDialog.data.max_withdrawable }} sat
- Wait time: {{ qrCodeDialog.data.wait_time }} seconds
- Withdraws: {{ qrCodeDialog.data.used }} / {{
- qrCodeDialog.data.uses }}
-
-
- {% endraw %}
-
- Copy LNURL
- Copy sharable link
-
- Write to NFC
- Print
- Close
-
-
-
-
-{% endblock %}
diff --git a/lnbits/extensions/withdraw/templates/withdraw/print_qr.html b/lnbits/extensions/withdraw/templates/withdraw/print_qr.html
deleted file mode 100644
index df4ca7d7..00000000
--- a/lnbits/extensions/withdraw/templates/withdraw/print_qr.html
+++ /dev/null
@@ -1,71 +0,0 @@
-{% extends "print.html" %} {% block page %}
-
-
-
- {% for page in link %}
-
-
- {% for threes in page %}
-
- {% for one in threes %}
-
-
-
-
-
- {% endfor %}
-
- {% endfor %}
-
-
- {% endfor %}
-
-
-{% endblock %} {% block styles %}
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/withdraw/templates/withdraw/print_qr_custom.html b/lnbits/extensions/withdraw/templates/withdraw/print_qr_custom.html
deleted file mode 100644
index ca47cec4..00000000
--- a/lnbits/extensions/withdraw/templates/withdraw/print_qr_custom.html
+++ /dev/null
@@ -1,113 +0,0 @@
-{% extends "print.html" %} {% block page %}
-
-
-
- {% for page in link %}
-
- {% for one in page %}
-
-
-
{{ amt }} sats
-
-
-
-
- {% endfor %}
-
- {% endfor %}
-
-
-{% endblock %} {% block styles %}
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py
deleted file mode 100644
index e8e5719a..00000000
--- a/lnbits/extensions/withdraw/views.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from http import HTTPStatus
-from io import BytesIO
-
-import pyqrcode
-from fastapi import Depends, HTTPException, Request
-from fastapi.templating import Jinja2Templates
-from starlette.responses import HTMLResponse, StreamingResponse
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import withdraw_ext, withdraw_renderer
-from .crud import chunks, get_withdraw_link
-
-templates = Jinja2Templates(directory="templates")
-
-
-@withdraw_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return withdraw_renderer().TemplateResponse(
- "withdraw/index.html", {"request": request, "user": user.dict()}
- )
-
-
-@withdraw_ext.get("/{link_id}", response_class=HTMLResponse)
-async def display(request: Request, link_id):
- link = await get_withdraw_link(link_id, 0)
-
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist."
- )
- return withdraw_renderer().TemplateResponse(
- "withdraw/display.html",
- {
- "request": request,
- "link": link.dict(),
- "lnurl": link.lnurl(req=request),
- "unique": True,
- },
- )
-
-
-@withdraw_ext.get("/img/{link_id}", response_class=StreamingResponse)
-async def img(request: Request, link_id):
- link = await get_withdraw_link(link_id, 0)
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist."
- )
- qr = pyqrcode.create(link.lnurl(request))
- stream = BytesIO()
- qr.svg(stream, scale=3)
- stream.seek(0)
-
- async def _generator(stream: BytesIO):
- yield stream.getvalue()
-
- return StreamingResponse(
- _generator(stream),
- headers={
- "Content-Type": "image/svg+xml",
- "Cache-Control": "no-cache, no-store, must-revalidate",
- "Pragma": "no-cache",
- "Expires": "0",
- },
- )
-
-
-@withdraw_ext.get("/print/{link_id}", response_class=HTMLResponse)
-async def print_qr(request: Request, link_id):
- link = await get_withdraw_link(link_id)
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist."
- )
- # response.status_code = HTTPStatus.NOT_FOUND
- # return "Withdraw link does not exist."
-
- if link.uses == 0:
-
- return withdraw_renderer().TemplateResponse(
- "withdraw/print_qr.html",
- {"request": request, "link": link.dict(), "unique": False},
- )
- links = []
- count = 0
-
- for x in link.usescsv.split(","):
- linkk = await get_withdraw_link(link_id, count)
- if not linkk:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist."
- )
- links.append(str(linkk.lnurl(request)))
- count = count + 1
- page_link = list(chunks(links, 2))
- linked = list(chunks(page_link, 5))
-
- if link.custom_url:
- return withdraw_renderer().TemplateResponse(
- "withdraw/print_qr_custom.html",
- {
- "request": request,
- "link": page_link,
- "unique": True,
- "custom_url": link.custom_url,
- "amt": link.max_withdrawable,
- },
- )
-
- return withdraw_renderer().TemplateResponse(
- "withdraw/print_qr.html", {"request": request, "link": linked, "unique": True}
- )
-
-
-@withdraw_ext.get("/csv/{link_id}", response_class=HTMLResponse)
-async def csv(request: Request, link_id):
- link = await get_withdraw_link(link_id)
- if not link:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist."
- )
- # response.status_code = HTTPStatus.NOT_FOUND
- # return "Withdraw link does not exist."
-
- if link.uses == 0:
-
- return withdraw_renderer().TemplateResponse(
- "withdraw/csv.html",
- {"request": request, "link": link.dict(), "unique": False},
- )
- links = []
- count = 0
-
- for x in link.usescsv.split(","):
- linkk = await get_withdraw_link(link_id, count)
- if not linkk:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist."
- )
- links.append(str(linkk.lnurl(request)))
- count = count + 1
- page_link = list(chunks(links, 2))
- linked = list(chunks(page_link, 5))
-
- return withdraw_renderer().TemplateResponse(
- "withdraw/csv.html", {"request": request, "link": linked, "unique": True}
- )
diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py
deleted file mode 100644
index 525796c9..00000000
--- a/lnbits/extensions/withdraw/views_api.py
+++ /dev/null
@@ -1,128 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Depends, HTTPException, Query, Request
-from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
-
-from lnbits.core.crud import get_user
-from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
-
-from . import withdraw_ext
-from .crud import (
- create_withdraw_link,
- delete_withdraw_link,
- get_hash_check,
- get_withdraw_link,
- get_withdraw_links,
- update_withdraw_link,
-)
-from .models import CreateWithdrawData
-
-
-@withdraw_ext.get("/api/v1/links", status_code=HTTPStatus.OK)
-async def api_links(
- req: Request,
- wallet: WalletTypeInfo = Depends(get_key_type),
- all_wallets: bool = Query(False),
-):
- wallet_ids = [wallet.wallet.id]
-
- if all_wallets:
- user = await get_user(wallet.wallet.user)
- wallet_ids = user.wallet_ids if user else []
-
- try:
- return [
- {**link.dict(), **{"lnurl": link.lnurl(req)}}
- for link in await get_withdraw_links(wallet_ids)
- ]
-
- except LnurlInvalidUrl:
- raise HTTPException(
- status_code=HTTPStatus.UPGRADE_REQUIRED,
- detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.",
- )
-
-
-@withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_retrieve(
- link_id: str, request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- link = await get_withdraw_link(link_id, 0)
-
- if not link:
- raise HTTPException(
- detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
- )
- return {**link.dict(), **{"lnurl": link.lnurl(request)}}
-
-
-@withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
-@withdraw_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_create_or_update(
- req: Request,
- data: CreateWithdrawData,
- link_id: str = Query(None),
- wallet: WalletTypeInfo = Depends(require_admin_key),
-):
- if data.uses > 250:
- raise HTTPException(detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST)
-
- if data.min_withdrawable < 1:
- raise HTTPException(
- detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
- )
-
- if data.max_withdrawable < data.min_withdrawable:
- raise HTTPException(
- detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
- status_code=HTTPStatus.BAD_REQUEST,
- )
-
- if link_id:
- link = await get_withdraw_link(link_id, 0)
- if not link:
- raise HTTPException(
- detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
- )
- link = await update_withdraw_link(link_id, **data.dict())
- else:
- link = await create_withdraw_link(wallet_id=wallet.wallet.id, data=data)
- assert link
- return {**link.dict(), **{"lnurl": link.lnurl(req)}}
-
-
-@withdraw_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
-async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
- link = await get_withdraw_link(link_id)
-
- if not link:
- raise HTTPException(
- detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND
- )
-
- if link.wallet != wallet.wallet.id:
- raise HTTPException(
- detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
- )
-
- await delete_withdraw_link(link_id)
- return {"success": True}
-
-
-@withdraw_ext.get(
- "/api/v1/links/{the_hash}/{lnurl_id}",
- status_code=HTTPStatus.OK,
- dependencies=[Depends(get_key_type)],
-)
-async def api_hash_retrieve(the_hash, lnurl_id):
- hashCheck = await get_hash_check(the_hash, lnurl_id)
- return hashCheck
diff --git a/lnbits/static/scss/base.scss b/lnbits/static/scss/base.scss
index f6b0762e..0730d03e 100644
--- a/lnbits/static/scss/base.scss
+++ b/lnbits/static/scss/base.scss
@@ -1,153 +1,207 @@
-$themes: ( 'classic': ( primary: #673ab7, secondary: #9c27b0, dark: #1f2234, info: #333646, marginal-bg: #1f2234, marginal-text: #fff), 'bitcoin': ( primary: #ff9853, secondary: #ff7353, dark: #2d293b, info: #333646, marginal-bg: #2d293b, marginal-text: #fff), 'freedom': ( primary: #e22156, secondary: #b91a45, dark: #0a0a0a, info: #1b1b1b, marginal-bg: #2d293b, marginal-text: #fff), 'mint': ( primary: #3ab77d, secondary: #27b065, dark: #1f342b, info: #334642, marginal-bg: #1f342b, marginal-text: #fff), 'autumn': ( primary: #b7763a, secondary: #b07927, dark: #34291f, info: #463f33, marginal-bg: #342a1f, marginal-text: rgb(255, 255, 255)), 'flamingo': ( primary: #d11d53, secondary: #db3e6d, dark: #803a45, info: #ec7599, marginal-bg: #803a45, marginal-text: rgb(255, 255, 255)), 'monochrome': ( primary: #494949, secondary: #6b6b6b, dark: #000, info: rgb(39, 39, 39), marginal-bg: #000, marginal-text: rgb(255, 255, 255)));
-@each $theme,
-$colors in $themes {
- @each $name,
- $color in $colors {
- @if $name=='dark' {
- [data-theme='#{$theme}'] .q-drawer--dark,
- body[data-theme='#{$theme}'].body--dark,
- [data-theme='#{$theme}'] .q-menu--dark {
- background: $color !important;
- }
- /* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
+$themes: (
+ 'classic': (
+ primary: #673ab7,
+ secondary: #9c27b0,
+ dark: #1f2234,
+ info: #333646,
+ marginal-bg: #1f2234,
+ marginal-text: #fff
+ ),
+ 'bitcoin': (
+ primary: #ff9853,
+ secondary: #ff7353,
+ dark: #2d293b,
+ info: #333646,
+ marginal-bg: #2d293b,
+ marginal-text: #fff
+ ),
+ 'freedom': (
+ primary: #e22156,
+ secondary: #b91a45,
+ dark: #0a0a0a,
+ info: #1b1b1b,
+ marginal-bg: #2d293b,
+ marginal-text: #fff
+ ),
+ 'mint': (
+ primary: #3ab77d,
+ secondary: #27b065,
+ dark: #1f342b,
+ info: #334642,
+ marginal-bg: #1f342b,
+ marginal-text: #fff
+ ),
+ 'autumn': (
+ primary: #b7763a,
+ secondary: #b07927,
+ dark: #34291f,
+ info: #463f33,
+ marginal-bg: #342a1f,
+ marginal-text: rgb(255, 255, 255)
+ ),
+ 'flamingo': (
+ primary: #d11d53,
+ secondary: #db3e6d,
+ dark: #803a45,
+ info: #ec7599,
+ marginal-bg: #803a45,
+ marginal-text: rgb(255, 255, 255)
+ ),
+ 'monochrome': (
+ primary: #494949,
+ secondary: #6b6b6b,
+ dark: #000,
+ info: rgb(39, 39, 39),
+ marginal-bg: #000,
+ marginal-text: rgb(255, 255, 255)
+ )
+);
+@each $theme, $colors in $themes {
+ @each $name, $color in $colors {
+ @if $name== 'dark' {
+ [data-theme='#{$theme}'] .q-drawer--dark,
+ body[data-theme='#{$theme}'].body--dark,
+ [data-theme='#{$theme}'] .q-menu--dark {
+ background: $color !important;
+ }
+ /* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='#{$theme}'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
- }
- @if $name=='info' {
- [data-theme='#{$theme}'] .q-card--dark,
- [data-theme='#{$theme}'] .q-stepper--dark {
- background: $color !important;
- }
- }
}
- [data-theme='#{$theme}'] {
- @each $name,
- $color in $colors {
- .bg-#{$name} {
- background: $color !important;
- }
- .text-#{$name} {
- color: $color !important;
- }
- }
+ @if $name== 'info' {
+ [data-theme='#{$theme}'] .q-card--dark,
+ [data-theme='#{$theme}'] .q-stepper--dark {
+ background: $color !important;
+ }
}
+ }
+ [data-theme='#{$theme}'] {
+ @each $name, $color in $colors {
+ .bg-#{$name} {
+ background: $color !important;
+ }
+ .text-#{$name} {
+ color: $color !important;
+ }
+ }
+ }
}
[data-theme='freedom'] .q-drawer--dark {
- background: #0a0a0a !important;
+ background: #0a0a0a !important;
}
[data-theme='freedom'] .q-header {
- background: #0a0a0a !important;
+ background: #0a0a0a !important;
}
[data-theme='salvador'] .q-drawer--dark {
- background: #242424 !important;
+ background: #242424 !important;
}
[data-theme='salvador'] .q-header {
- background: #0f47af !important;
+ background: #0f47af !important;
}
[data-theme='flamingo'] .q-drawer--dark {
- background: #e75480 !important;
+ background: #e75480 !important;
}
[data-theme='flamingo'] .q-header {
- background: #e75480 !important;
+ background: #e75480 !important;
}
[v-cloak] {
- display: none;
+ display: none;
}
body.body--dark .q-table--dark {
- background: transparent;
+ background: transparent;
}
body.body--dark .q-field--error {
- .text-negative,
- .q-field__messages {
- color: yellow !important;
- }
+ .text-negative,
+ .q-field__messages {
+ color: yellow !important;
+ }
}
.lnbits-drawer__q-list .q-item {
- padding-top: 5px !important;
- padding-bottom: 5px !important;
- border-top-right-radius: 3px;
- border-bottom-right-radius: 3px;
- &.q-item--active {
- color: inherit;
- font-weight: bold;
- }
+ padding-top: 5px !important;
+ padding-bottom: 5px !important;
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+ &.q-item--active {
+ color: inherit;
+ font-weight: bold;
+ }
}
.lnbits__dialog-card {
- width: 500px;
+ width: 500px;
}
.q-table--dense {
- th:first-child,
- td:first-child,
- .q-table__bottom {
- padding-left: 6px !important;
- }
- th:last-child,
- td:last-child,
- .q-table__bottom {
- padding-right: 6px !important;
- }
+ th:first-child,
+ td:first-child,
+ .q-table__bottom {
+ padding-left: 6px !important;
+ }
+ th:last-child,
+ td:last-child,
+ .q-table__bottom {
+ padding-right: 6px !important;
+ }
}
a.inherit {
- color: inherit;
- text-decoration: none;
+ color: inherit;
+ text-decoration: none;
}
// QR video
video {
- border-radius: 3px;
+ border-radius: 3px;
}
// Material icons font
@font-face {
- font-family: 'Material Icons';
- font-style: normal;
- font-weight: 400;
- src: url(../fonts/material-icons-v50.woff2) format('woff2');
+ font-family: 'Material Icons';
+ font-style: normal;
+ font-weight: 400;
+ src: url(../fonts/material-icons-v50.woff2) format('woff2');
}
.material-icons {
- font-family: 'Material Icons';
- font-weight: normal;
- font-style: normal;
- font-size: 24px;
- line-height: 1;
- letter-spacing: normal;
- text-transform: none;
- display: inline-block;
- white-space: nowrap;
- word-wrap: normal;
- direction: ltr;
- -moz-font-feature-settings: 'liga';
- -moz-osx-font-smoothing: grayscale;
+ font-family: 'Material Icons';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 24px;
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -moz-font-feature-settings: 'liga';
+ -moz-osx-font-smoothing: grayscale;
}
.q-rating__icon {
- font-size: 1em;
+ font-size: 1em;
}
// text-wrap
.text-wrap {
- word-break: break-word;
+ word-break: break-word;
}
.q-card {
code {
overflow-wrap: break-word;
}
-}
\ No newline at end of file
+}
diff --git a/tests/core/views/__init__.py b/tests/core/views/__init__.py
index e69de29b..4bc64062 100644
--- a/tests/core/views/__init__.py
+++ b/tests/core/views/__init__.py
@@ -0,0 +1 @@
+from tests.mocks import WALLET # noqa: F401
diff --git a/tests/core/views/test_generic.py b/tests/core/views/test_generic.py
index 33325157..0a8e71a5 100644
--- a/tests/core/views/test_generic.py
+++ b/tests/core/views/test_generic.py
@@ -125,14 +125,27 @@ async def test_get_extensions_no_user(client):
# check GET /extensions: enable extension
-@pytest.mark.asyncio
-async def test_get_extensions_enable(client, to_user):
- response = await client.get(
- "extensions", params={"usr": to_user.id, "enable": "lnurlp"}
- )
- assert response.status_code == 200, (
- str(response.url) + " " + str(response.status_code)
- )
+# TODO: test fails because of removing lnurlp extension
+# @pytest.mark.asyncio
+# async def test_get_extensions_enable(client, to_user):
+# response = await client.get(
+# "extensions", params={"usr": to_user.id, "enable": "lnurlp"}
+# )
+# assert response.status_code == 200, (
+# str(response.url) + " " + str(response.status_code)
+# )
+
+
+# check GET /extensions: enable and disable extensions, expect code 400 bad request
+# @pytest.mark.asyncio
+# async def test_get_extensions_enable_and_disable(client, to_user):
+# response = await client.get(
+# "extensions",
+# params={"usr": to_user.id, "enable": "lnurlp", "disable": "lnurlp"},
+# )
+# assert response.status_code == 400, (
+# str(response.url) + " " + str(response.status_code)
+# )
# check GET /extensions: enable nonexistent extension, expect code 400 bad request
@@ -144,15 +157,3 @@ async def test_get_extensions_enable_nonexistent_extension(client, to_user):
assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code)
)
-
-
-# check GET /extensions: enable and disable extensions, expect code 400 bad request
-@pytest.mark.asyncio
-async def test_get_extensions_enable_and_disable(client, to_user):
- response = await client.get(
- "extensions",
- params={"usr": to_user.id, "enable": "lnurlp", "disable": "lnurlp"},
- )
- assert response.status_code == 400, (
- str(response.url) + " " + str(response.status_code)
- )
diff --git a/tests/core/views/test_public_api.py b/tests/core/views/test_public_api.py
index 144cd161..9c351cf1 100644
--- a/tests/core/views/test_public_api.py
+++ b/tests/core/views/test_public_api.py
@@ -24,13 +24,3 @@ async def test_api_public_payment_longpolling_wrong_hash(client, invoice):
)
assert response.status_code == 404
assert response.json()["detail"] == "Payment does not exist."
-
-
-# check GET /.well-known/lnurlp/{username}: wrong username [should fail]
-@pytest.mark.asyncio
-async def test_lnaddress_wrong_hash(client):
- username = "wrong_name"
- response = await client.get(f"/.well-known/lnurlp/{username}")
- assert response.status_code == 200
- assert response.json()["status"] == "ERROR"
- assert response.json()["reason"] == "Address not found."
diff --git a/tests/extensions/bleskomat/__init__.py b/tests/extensions/bleskomat/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/extensions/bleskomat/conftest.py b/tests/extensions/bleskomat/conftest.py
deleted file mode 100644
index 595ba6b8..00000000
--- a/tests/extensions/bleskomat/conftest.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import json
-import secrets
-
-import pytest_asyncio
-
-from lnbits.core.crud import create_account, create_wallet
-from lnbits.extensions.bleskomat.crud import create_bleskomat, create_bleskomat_lnurl
-from lnbits.extensions.bleskomat.exchange_rates import exchange_rate_providers
-from lnbits.extensions.bleskomat.helpers import (
- generate_bleskomat_lnurl_secret,
- generate_bleskomat_lnurl_signature,
- prepare_lnurl_params,
- query_to_signing_payload,
-)
-from lnbits.extensions.bleskomat.models import CreateBleskomat
-
-exchange_rate_providers["dummy"] = {
- "name": "dummy",
- "domain": None,
- "api_url": None,
- "getter": lambda data, replacements: str(1e8), # 1 BTC = 100000000 sats
-}
-
-
-@pytest_asyncio.fixture
-async def bleskomat():
- user = await create_account()
- wallet = await create_wallet(user_id=user.id, wallet_name="bleskomat_test")
- data = CreateBleskomat(
- name="Test Bleskomat",
- fiat_currency="EUR",
- exchange_rate_provider="dummy",
- fee="0",
- )
- bleskomat = await create_bleskomat(data=data, wallet_id=wallet.id)
- return bleskomat
-
-
-@pytest_asyncio.fixture
-async def lnurl(bleskomat):
- query = {
- "tag": "withdrawRequest",
- "nonce": secrets.token_hex(10),
- "tag": "withdrawRequest",
- "minWithdrawable": "50000",
- "maxWithdrawable": "50000",
- "defaultDescription": "test valid sig",
- }
- tag = query["tag"]
- params = prepare_lnurl_params(tag, query)
- payload = query_to_signing_payload(query)
- signature = generate_bleskomat_lnurl_signature(
- payload=payload,
- api_key_secret=bleskomat.api_key_secret,
- api_key_encoding=bleskomat.api_key_encoding,
- )
- secret = generate_bleskomat_lnurl_secret(bleskomat.api_key_id, signature)
- params = json.JSONEncoder().encode(params)
- lnurl = await create_bleskomat_lnurl(
- bleskomat=bleskomat, secret=secret, tag=tag, params=params, uses=1
- )
- return {
- "bleskomat": bleskomat,
- "lnurl": lnurl,
- "secret": secret,
- }
diff --git a/tests/extensions/bleskomat/test_lnurl_api.py b/tests/extensions/bleskomat/test_lnurl_api.py
deleted file mode 100644
index a66c9220..00000000
--- a/tests/extensions/bleskomat/test_lnurl_api.py
+++ /dev/null
@@ -1,141 +0,0 @@
-import secrets
-
-import pytest
-
-from lnbits.core.crud import get_wallet
-from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl
-from lnbits.extensions.bleskomat.helpers import (
- generate_bleskomat_lnurl_signature,
- query_to_signing_payload,
-)
-from lnbits.settings import get_wallet_class, settings
-from tests.helpers import credit_wallet, is_regtest
-
-WALLET = get_wallet_class()
-
-
-@pytest.mark.asyncio
-async def test_bleskomat_lnurl_api_missing_secret(client):
- response = await client.get("/bleskomat/u")
- assert response.status_code == 200
- assert response.json() == {"status": "ERROR", "reason": "Missing secret"}
-
-
-@pytest.mark.asyncio
-async def test_bleskomat_lnurl_api_invalid_secret(client):
- response = await client.get("/bleskomat/u?k1=invalid-secret")
- assert response.status_code == 200
- assert response.json() == {"status": "ERROR", "reason": "Invalid secret"}
-
-
-@pytest.mark.asyncio
-async def test_bleskomat_lnurl_api_unknown_api_key(client):
- query = {
- "id": "does-not-exist",
- "nonce": secrets.token_hex(10),
- "tag": "withdrawRequest",
- "minWithdrawable": "1",
- "maxWithdrawable": "1",
- "defaultDescription": "",
- "f": "EUR",
- }
- payload = query_to_signing_payload(query)
- signature = "xxx" # not checked, so doesn't matter
- response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
- assert response.status_code == 200
- assert response.json() == {"status": "ERROR", "reason": "Unknown API key"}
-
-
-@pytest.mark.asyncio
-async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
- query = {
- "id": bleskomat.api_key_id,
- "nonce": secrets.token_hex(10),
- "tag": "withdrawRequest",
- "minWithdrawable": "1",
- "maxWithdrawable": "1",
- "defaultDescription": "",
- "f": "EUR",
- }
- payload = query_to_signing_payload(query)
- signature = "invalid"
- response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
- assert response.status_code == 200
- assert response.json() == {"status": "ERROR", "reason": "Invalid API key signature"}
-
-
-@pytest.mark.asyncio
-async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
- query = {
- "id": bleskomat.api_key_id,
- "nonce": secrets.token_hex(10),
- "tag": "withdrawRequest",
- "minWithdrawable": "1",
- "maxWithdrawable": "1",
- "defaultDescription": "test valid sig",
- "f": "EUR", # tests use the dummy exchange rate provider
- }
- payload = query_to_signing_payload(query)
- signature = generate_bleskomat_lnurl_signature(
- payload=payload,
- api_key_secret=bleskomat.api_key_secret,
- api_key_encoding=bleskomat.api_key_encoding,
- )
- response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
- assert response.status_code == 200
- data = response.json()
- assert data["tag"] == "withdrawRequest"
- assert data["minWithdrawable"] == 1000
- assert data["maxWithdrawable"] == 1000
- assert data["defaultDescription"] == "test valid sig"
- assert data["callback"] == f"http://{settings.host}:{settings.port}/bleskomat/u"
- k1 = data["k1"]
- lnurl = await get_bleskomat_lnurl(secret=k1)
- assert lnurl
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_regtest, reason="this test is only passes in fakewallet")
-async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl):
- bleskomat = lnurl["bleskomat"]
- secret = lnurl["secret"]
- pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu"
- WALLET.pay_invoice.reset_mock()
- response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
- assert response.status_code == 200
- assert response.json()["status"] == "ERROR"
- assert ("Insufficient balance" in response.json()["reason"]) or (
- "fee" in response.json()["reason"]
- )
- wallet = await get_wallet(bleskomat.wallet)
- assert wallet, not None
- assert wallet.balance_msat == 0
- bleskomat_lnurl = await get_bleskomat_lnurl(secret)
- assert bleskomat_lnurl, not None
- assert bleskomat_lnurl.has_uses_remaining() is True
- WALLET.pay_invoice.assert_not_called()
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_regtest, reason="this test is only passes in fakewallet")
-async def test_bleskomat_lnurl_api_action_success(client, lnurl):
- bleskomat = lnurl["bleskomat"]
- secret = lnurl["secret"]
- pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu"
- await credit_wallet(
- wallet_id=bleskomat.wallet,
- amount=100000,
- )
- wallet = await get_wallet(bleskomat.wallet)
- assert wallet, not None
- assert wallet.balance_msat == 100000
- WALLET.pay_invoice.reset_mock()
- response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
- assert response.json() == {"status": "OK"}
- wallet = await get_wallet(bleskomat.wallet)
- assert wallet, not None
- assert wallet.balance_msat == 50000
- bleskomat_lnurl = await get_bleskomat_lnurl(secret)
- assert bleskomat_lnurl, not None
- assert bleskomat_lnurl.has_uses_remaining() is False
- WALLET.pay_invoice.assert_called_once_with(pr, 2000)
diff --git a/tests/extensions/boltz/__init__.py b/tests/extensions/boltz/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/extensions/boltz/conftest.py b/tests/extensions/boltz/conftest.py
deleted file mode 100644
index 1eba452a..00000000
--- a/tests/extensions/boltz/conftest.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import pytest_asyncio
-
-from lnbits.extensions.boltz.models import CreateReverseSubmarineSwap
-
-
-@pytest_asyncio.fixture(scope="session")
-async def reverse_swap(from_wallet):
- data = CreateReverseSubmarineSwap(
- wallet=from_wallet.id,
- instant_settlement=True,
- onchain_address="bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43",
- amount=20_000,
- )
- return data
diff --git a/tests/extensions/boltz/test_api.py b/tests/extensions/boltz/test_api.py
deleted file mode 100644
index 057bdab5..00000000
--- a/tests/extensions/boltz/test_api.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import pytest
-
-from tests.helpers import is_fake
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
-async def test_mempool_url(client):
- response = await client.get("/boltz/api/v1/swap/mempool")
- assert response.status_code == 200
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
-async def test_boltz_config(client):
- response = await client.get("/boltz/api/v1/swap/boltz")
- assert response.status_code == 200
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
-async def test_endpoints_unauthenticated(client):
- response = await client.get("/boltz/api/v1/swap?all_wallets=true")
- assert response.status_code == 401
- response = await client.get("/boltz/api/v1/swap/reverse?all_wallets=true")
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap")
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap/reverse")
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap/status")
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap/check")
- assert response.status_code == 401
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
-async def test_endpoints_inkey(client, inkey_headers_to):
- response = await client.get(
- "/boltz/api/v1/swap?all_wallets=true", headers=inkey_headers_to
- )
- assert response.status_code == 200
- response = await client.get(
- "/boltz/api/v1/swap/reverse?all_wallets=true", headers=inkey_headers_to
- )
- assert response.status_code == 200
-
- response = await client.post("/boltz/api/v1/swap", headers=inkey_headers_to)
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap/reverse", headers=inkey_headers_to)
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap/refund", headers=inkey_headers_to)
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap/status", headers=inkey_headers_to)
- assert response.status_code == 401
- response = await client.post("/boltz/api/v1/swap/check", headers=inkey_headers_to)
- assert response.status_code == 401
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
-async def test_endpoints_adminkey_badrequest(client, adminkey_headers_to):
- response = await client.post("/boltz/api/v1/swap", headers=adminkey_headers_to)
- assert response.status_code == 400
- response = await client.post(
- "/boltz/api/v1/swap/reverse", headers=adminkey_headers_to
- )
- assert response.status_code == 400
- response = await client.post(
- "/boltz/api/v1/swap/refund", headers=adminkey_headers_to
- )
- assert response.status_code == 400
- response = await client.post(
- "/boltz/api/v1/swap/status", headers=adminkey_headers_to
- )
- assert response.status_code == 400
-
-
-@pytest.mark.asyncio
-@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
-async def test_endpoints_adminkey_regtest(client, from_wallet, adminkey_headers_to):
- swap = {
- "wallet": from_wallet.id,
- "refund_address": "bcrt1q3cwq33y435h52gq3qqsdtczh38ltlnf69zvypm",
- "amount": 50_000,
- }
- response = await client.post(
- "/boltz/api/v1/swap", json=swap, headers=adminkey_headers_to
- )
- assert response.status_code == 201
-
- reverse_swap = {
- "wallet": from_wallet.id,
- "instant_settlement": True,
- "onchain_address": "bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43",
- "amount": 50_000,
- }
- response = await client.post(
- "/boltz/api/v1/swap/reverse", json=reverse_swap, headers=adminkey_headers_to
- )
- assert response.status_code == 201
diff --git a/tests/extensions/invoices/__init__.py b/tests/extensions/invoices/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/extensions/invoices/conftest.py b/tests/extensions/invoices/conftest.py
deleted file mode 100644
index 522ba81f..00000000
--- a/tests/extensions/invoices/conftest.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import pytest_asyncio
-
-from lnbits.core.crud import create_account, create_wallet
-from lnbits.extensions.invoices.crud import (
- create_invoice_internal,
- create_invoice_items,
-)
-from lnbits.extensions.invoices.models import CreateInvoiceData
-
-
-@pytest_asyncio.fixture
-async def invoices_wallet():
- user = await create_account()
- wallet = await create_wallet(user_id=user.id, wallet_name="invoices_test")
-
- return wallet
-
-
-@pytest_asyncio.fixture
-async def accounting_invoice(invoices_wallet):
- invoice_data = CreateInvoiceData(
- status="open",
- currency="USD",
- company_name="LNbits, Inc",
- first_name="Ben",
- last_name="Arc",
- items=[{"amount": 10.20, "description": "Item costs 10.20"}],
- )
- invoice = await create_invoice_internal(
- wallet_id=invoices_wallet.id, data=invoice_data
- )
- items = await create_invoice_items(invoice_id=invoice.id, data=invoice_data.items)
-
- invoice_dict = invoice.dict()
- invoice_dict["items"] = items
- return invoice_dict
diff --git a/tests/extensions/invoices/test_invoices_api.py b/tests/extensions/invoices/test_invoices_api.py
deleted file mode 100644
index 3c337d7e..00000000
--- a/tests/extensions/invoices/test_invoices_api.py
+++ /dev/null
@@ -1,133 +0,0 @@
-import pytest
-import pytest_asyncio # noqa: F401
-from loguru import logger # noqa: F401
-
-from lnbits.core.crud import get_wallet # noqa: F401
-from tests.helpers import credit_wallet # noqa: F401
-from tests.mocks import WALLET # noqa: F401
-
-
-@pytest.mark.asyncio
-async def test_invoices_unknown_invoice(client):
- response = await client.get("/invoices/pay/u")
- assert response.json() == {"detail": "Invoice does not exist."}
-
-
-@pytest.mark.asyncio
-async def test_invoices_api_create_invoice_valid(client, invoices_wallet):
- query = {
- "status": "open",
- "currency": "EUR",
- "company_name": "LNbits, Inc.",
- "first_name": "Ben",
- "last_name": "Arc",
- "email": "ben@legend.arc",
- "items": [
- {"amount": 2.34, "description": "Item 1"},
- {"amount": 0.98, "description": "Item 2"},
- ],
- }
-
- status = query["status"]
- currency = query["currency"]
- fname = query["first_name"]
- total = sum(d["amount"] for d in query["items"])
-
- response = await client.post(
- "/invoices/api/v1/invoice",
- json=query,
- headers={"X-Api-Key": invoices_wallet.inkey},
- )
-
- assert response.status_code == 201
- data = response.json()
-
- assert data["status"] == status
- assert data["wallet"] == invoices_wallet.id
- assert data["currency"] == currency
- assert data["first_name"] == fname
- assert sum(d["amount"] / 100 for d in data["items"]) == total
-
-
-@pytest.mark.asyncio
-async def test_invoices_api_partial_pay_invoice(
- client, accounting_invoice, adminkey_headers_from
-):
- invoice_id = accounting_invoice["id"]
- amount_to_pay = int(5.05 * 100) # mock invoice total amount is 10 USD
-
- # ask for an invoice
- response = await client.post(
- f"/invoices/api/v1/invoice/{invoice_id}/payments?famount={amount_to_pay}"
- )
- assert response.status_code < 300
- data = response.json()
- payment_hash = data["payment_hash"]
-
- # pay the invoice
- data = {"out": True, "bolt11": data["payment_request"]}
- response = await client.post(
- "/api/v1/payments", json=data, headers=adminkey_headers_from
- )
- assert response.status_code < 300
- assert len(response.json()["payment_hash"]) == 64
- assert len(response.json()["checking_id"]) > 0
-
- # check invoice is paid
- response = await client.get(
- f"/invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}"
- )
- assert response.status_code == 200
- assert response.json()["paid"] is True
-
- # check invoice status
- response = await client.get(f"/invoices/api/v1/invoice/{invoice_id}")
- assert response.status_code == 200
- data = response.json()
-
- assert data["status"] == "open"
-
-
-####
-#
-# TEST FAILS FOR NOW, AS LISTENERS ARE NOT WORKING ON TESTING
-#
-###
-
-# @pytest.mark.asyncio
-# async def test_invoices_api_full_pay_invoice(client, accounting_invoice, adminkey_headers_to):
-# invoice_id = accounting_invoice["id"]
-# print(accounting_invoice["id"])
-# amount_to_pay = int(10.20 * 100)
-
-# # ask for an invoice
-# response = await client.post(
-# f"/invoices/api/v1/invoice/{invoice_id}/payments?famount={amount_to_pay}"
-# )
-# assert response.status_code == 201
-# data = response.json()
-# payment_hash = data["payment_hash"]
-
-# # pay the invoice
-# data = {"out": True, "bolt11": data["payment_request"]}
-# response = await client.post(
-# "/api/v1/payments", json=data, headers=adminkey_headers_to
-# )
-# assert response.status_code < 300
-# assert len(response.json()["payment_hash"]) == 64
-# assert len(response.json()["checking_id"]) > 0
-
-# # check invoice is paid
-# response = await client.get(
-# f"/invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}"
-# )
-# assert response.status_code == 200
-# assert response.json()["paid"] is True
-
-# # check invoice status
-# response = await client.get(f"/invoices/api/v1/invoice/{invoice_id}")
-# assert response.status_code == 200
-# data = response.json()
-
-# print(data)
-# assert data["status"] == "paid"