From 7baa248204182a364dc0b53319932e3fabe87cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Wed, 15 Feb 2023 10:06:21 +0100 Subject: [PATCH] remove boltz --- lnbits/extensions/boltz/README.md | 42 -- lnbits/extensions/boltz/__init__.py | 35 - lnbits/extensions/boltz/config.json | 6 - lnbits/extensions/boltz/crud.py | 284 -------- lnbits/extensions/boltz/migrations.py | 64 -- lnbits/extensions/boltz/models.py | 68 -- .../extensions/boltz/static/image/boltz.png | Bin 38310 -> 0 bytes lnbits/extensions/boltz/tasks.py | 180 ----- .../boltz/templates/boltz/_api_docs.html | 109 --- .../boltz/_autoReverseSwapDialog.html | 83 --- .../templates/boltz/_autoReverseSwapList.html | 54 -- .../boltz/templates/boltz/_buttons.html | 35 - .../templates/boltz/_checkSwapDialog.html | 113 ---- .../boltz/templates/boltz/_qrDialog.html | 31 - .../boltz/_reverseSubmarineSwapDialog.html | 72 -- .../boltz/_reverseSubmarineSwapList.html | 66 -- .../boltz/templates/boltz/_statusDialog.html | 29 - .../templates/boltz/_submarineSwapDialog.html | 58 -- .../templates/boltz/_submarineSwapList.html | 78 --- .../boltz/templates/boltz/index.html | 621 ------------------ lnbits/extensions/boltz/utils.py | 87 --- lnbits/extensions/boltz/views.py | 21 - lnbits/extensions/boltz/views_api.py | 332 ---------- tests/extensions/boltz/__init__.py | 0 tests/extensions/boltz/conftest.py | 14 - tests/extensions/boltz/test_api.py | 102 --- 26 files changed, 2584 deletions(-) delete mode 100644 lnbits/extensions/boltz/README.md delete mode 100644 lnbits/extensions/boltz/__init__.py delete mode 100644 lnbits/extensions/boltz/config.json delete mode 100644 lnbits/extensions/boltz/crud.py delete mode 100644 lnbits/extensions/boltz/migrations.py delete mode 100644 lnbits/extensions/boltz/models.py delete mode 100644 lnbits/extensions/boltz/static/image/boltz.png delete mode 100644 lnbits/extensions/boltz/tasks.py delete mode 100644 lnbits/extensions/boltz/templates/boltz/_api_docs.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_autoReverseSwapList.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_buttons.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_qrDialog.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapDialog.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapList.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_statusDialog.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_submarineSwapDialog.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html delete mode 100644 lnbits/extensions/boltz/templates/boltz/index.html delete mode 100644 lnbits/extensions/boltz/utils.py delete mode 100644 lnbits/extensions/boltz/views.py delete mode 100644 lnbits/extensions/boltz/views_api.py delete mode 100644 tests/extensions/boltz/__init__.py delete mode 100644 tests/extensions/boltz/conftest.py delete mode 100644 tests/extensions/boltz/test_api.py diff --git a/lnbits/extensions/boltz/README.md b/lnbits/extensions/boltz/README.md deleted file mode 100644 index 9ca38d491..000000000 --- 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. ---- -![create swap](https://imgur.com/OyOh3Nm.png) ---- -2. after you confirm your inputs, following dialog with the QR code for the onchain transaction, onchain- address and amount, will pop up. ---- -![pay onchain tx](https://imgur.com/r2UhwCY.png) ---- -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. ---- -![reverse swap](https://imgur.com/UEAPpbs.png) ---- -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. ---- -![refund](https://imgur.com/pN81ltf.png) ----- -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 98255e5e5..000000000 --- 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 db678207e..000000000 --- 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 5ad923f6d..000000000 --- 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 66648fccc..000000000 --- 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 9500b678a..000000000 --- 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 2dcefc94591e4526c820154dd2199b1869cf1982..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38310 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 diff --git a/lnbits/extensions/boltz/tasks.py b/lnbits/extensions/boltz/tasks.py deleted file mode 100644 index 63b981b1c..000000000 --- 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 f1be62a7d..000000000 --- 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. -
-
-

- 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 c9c682a8b..000000000 --- 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 b297524f1..000000000 --- a/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapList.html +++ /dev/null @@ -1,54 +0,0 @@ - - -
-
-
Auto Lightning -> Onchain
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_buttons.html b/lnbits/extensions/boltz/templates/boltz/_buttons.html deleted file mode 100644 index 3817b0766..000000000 --- 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 e59702d24..000000000 --- a/lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html +++ /dev/null @@ -1,113 +0,0 @@ - - -
pending swaps
- - {% raw %} - - - {% endraw %} - -
pending reverse swaps
- - {% raw %} - - - {% 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 053ef65e7..000000000 --- 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 5b3cf861a..000000000 --- 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 fc9668d0d..000000000 --- a/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapList.html +++ /dev/null @@ -1,66 +0,0 @@ - - -
-
-
Lightning -> Onchain
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
diff --git a/lnbits/extensions/boltz/templates/boltz/_statusDialog.html b/lnbits/extensions/boltz/templates/boltz/_statusDialog.html deleted file mode 100644 index f6c14abc9..000000000 --- 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 bf6aaa187..000000000 --- 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 b42e1dee9..000000000 --- a/lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html +++ /dev/null @@ -1,78 +0,0 @@ - - -
-
-
Onchain -> Lightning
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
diff --git a/lnbits/extensions/boltz/templates/boltz/index.html b/lnbits/extensions/boltz/templates/boltz/index.html deleted file mode 100644 index d985a01f5..000000000 --- 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 7623fb6f6..000000000 --- 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 4b0e6d535..000000000 --- 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 ffec612c6..000000000 --- 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/tests/extensions/boltz/__init__.py b/tests/extensions/boltz/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/extensions/boltz/conftest.py b/tests/extensions/boltz/conftest.py deleted file mode 100644 index 1eba452a9..000000000 --- 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 057bdab5c..000000000 --- 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

- Link: - https://boltz.exchange - -
- README: - read more -