From d89a6a337a8e5f2956a347e4527f45698cdd2a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 19 Jan 2023 10:30:47 +0100 Subject: [PATCH] boltz extension v2, recurring swaps (#981) * add status to statusdialog * first commits for boltz update * formatting * add latest boltz-clien package * big refactor, depending on boltz_client package, clean up, mypy issues, not tested yet * blacking, sorting and stuff * remove unused req_wrap helper * remove api docs from frontend * bug: frontend boltz limits error * clean up buttons * update to boltz-client 0.0.8 * fix tests to poetry version 1.3.1 * update requirements * formatting * recurring swap works now, need more finetuning * add exceptions for multiple auto swaps and swapping in with active auto swap * black * auto reverse swap actually works :) * remove swap status dialogs * update to boltz_client 0.0.9 * update to boltz-client 0.1.1, and fix startup * update requirement.txt for boltz-client * fixup columns in table, remove unused payment.extra, change deezy label * remove balance check for auto swap out * update boltzc-lient to 0.1.2, fix mypy issue inside boltz package * nitpicks calle tasks.py * calle nitpicks crud * calle nitpicks crud * refactor * fix formatting * circular import * black :) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- lnbits/extensions/boltz/boltz.py | 421 ---------- lnbits/extensions/boltz/crud.py | 224 ++++-- lnbits/extensions/boltz/mempool.py | 93 --- lnbits/extensions/boltz/migrations.py | 18 + lnbits/extensions/boltz/models.py | 43 +- lnbits/extensions/boltz/tasks.py | 268 ++++--- .../boltz/templates/boltz/_api_docs.html | 277 +------ .../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 | 744 ++++-------------- lnbits/extensions/boltz/utils.py | 85 +- lnbits/extensions/boltz/views.py | 6 +- lnbits/extensions/boltz/views_api.py | 280 ++++--- .../deezy/templates/deezy/index.html | 2 +- poetry.lock | 26 +- pyproject.toml | 6 +- requirements.txt | 1 + tests/extensions/boltz/conftest.py | 15 +- tests/extensions/boltz/test_api.py | 3 +- tests/extensions/boltz/test_swap.py | 31 - 28 files changed, 1380 insertions(+), 1782 deletions(-) delete mode 100644 lnbits/extensions/boltz/boltz.py delete mode 100644 lnbits/extensions/boltz/mempool.py create mode 100644 lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_autoReverseSwapList.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_buttons.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_qrDialog.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapDialog.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapList.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_statusDialog.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_submarineSwapDialog.html create mode 100644 lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html delete mode 100644 tests/extensions/boltz/test_swap.py diff --git a/lnbits/extensions/boltz/boltz.py b/lnbits/extensions/boltz/boltz.py deleted file mode 100644 index 31d927eac..000000000 --- a/lnbits/extensions/boltz/boltz.py +++ /dev/null @@ -1,421 +0,0 @@ -import asyncio -import os -from hashlib import sha256 -from typing import Awaitable, Union - -import httpx -from embit import ec, script -from embit.networks import NETWORKS -from embit.transaction import SIGHASH, Transaction, TransactionInput, TransactionOutput -from loguru import logger - -from lnbits.core.services import create_invoice, pay_invoice -from lnbits.helpers import urlsafe_short_hash -from lnbits.settings import settings - -from .crud import update_swap_status -from .mempool import ( - get_fee_estimation, - get_mempool_blockheight, - get_mempool_fees, - get_mempool_tx, - get_mempool_tx_from_txs, - send_onchain_tx, - wait_for_websocket_message, -) -from .models import ( - CreateReverseSubmarineSwap, - CreateSubmarineSwap, - ReverseSubmarineSwap, - SubmarineSwap, - SwapStatus, -) -from .utils import check_balance, get_timestamp, req_wrap - -net = NETWORKS[settings.boltz_network] - - -async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap: - if not check_boltz_limits(data.amount): - msg = f"Boltz - swap not in boltz limits" - logger.warning(msg) - raise Exception(msg) - - swap_id = urlsafe_short_hash() - try: - 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}, - ) - except Exception as exc: - msg = f"Boltz - create_invoice failed {str(exc)}" - logger.error(msg) - raise - - refund_privkey = ec.PrivateKey(os.urandom(32), True, net) - refund_pubkey_hex = bytes.hex(refund_privkey.sec()).decode() - - res = req_wrap( - "post", - f"{settings.boltz_url}/createswap", - json={ - "type": "submarine", - "pairId": "BTC/BTC", - "orderSide": "sell", - "refundPublicKey": refund_pubkey_hex, - "invoice": payment_request, - "referralId": "lnbits", - }, - headers={"Content-Type": "application/json"}, - ) - res = res.json() - logger.info( - f"Boltz - created normal swap, boltz_id: {res['id']}. wallet: {data.wallet}" - ) - return SubmarineSwap( - id=swap_id, - time=get_timestamp(), - wallet=data.wallet, - amount=data.amount, - payment_hash=payment_hash, - refund_privkey=refund_privkey.wif(net), - refund_address=data.refund_address, - boltz_id=res["id"], - status="pending", - address=res["address"], - expected_amount=res["expectedAmount"], - timeout_block_height=res["timeoutBlockHeight"], - bip21=res["bip21"], - redeem_script=res["redeemScript"], - ) - - -""" -explanation taken from electrum -send on Lightning, receive on-chain -- User generates preimage, RHASH. Sends RHASH to server. -- Server creates an LN invoice for RHASH. -- User pays LN invoice - except server needs to hold the HTLC as preimage is unknown. -- Server creates on-chain output locked to RHASH. -- User spends on-chain output, revealing preimage. -- Server fulfills HTLC using preimage. -Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee. -""" - - -async def create_reverse_swap( - data: CreateReverseSubmarineSwap, -) -> [ReverseSubmarineSwap, asyncio.Task]: - if not check_boltz_limits(data.amount): - msg = f"Boltz - reverse swap not in boltz limits" - logger.warning(msg) - raise Exception(msg) - - swap_id = urlsafe_short_hash() - - if not await check_balance(data): - logger.error(f"Boltz - reverse swap, insufficient balance.") - return False - - claim_privkey = ec.PrivateKey(os.urandom(32), True, net) - claim_pubkey_hex = bytes.hex(claim_privkey.sec()).decode() - preimage = os.urandom(32) - preimage_hash = sha256(preimage).hexdigest() - - res = req_wrap( - "post", - f"{settings.boltz_url}/createswap", - json={ - "type": "reversesubmarine", - "pairId": "BTC/BTC", - "orderSide": "buy", - "invoiceAmount": data.amount, - "preimageHash": preimage_hash, - "claimPublicKey": claim_pubkey_hex, - "referralId": "lnbits", - }, - headers={"Content-Type": "application/json"}, - ) - res = res.json() - - logger.info( - f"Boltz - created reverse swap, boltz_id: {res['id']}. wallet: {data.wallet}" - ) - - swap = ReverseSubmarineSwap( - id=swap_id, - amount=data.amount, - wallet=data.wallet, - onchain_address=data.onchain_address, - instant_settlement=data.instant_settlement, - claim_privkey=claim_privkey.wif(net), - preimage=preimage.hex(), - status="pending", - boltz_id=res["id"], - timeout_block_height=res["timeoutBlockHeight"], - lockup_address=res["lockupAddress"], - onchain_amount=res["onchainAmount"], - redeem_script=res["redeemScript"], - invoice=res["invoice"], - time=get_timestamp(), - ) - logger.debug(f"Boltz - waiting for onchain tx, reverse swap_id: {swap.id}") - task = create_task_log_exception( - swap.id, wait_for_onchain_tx(swap, swap_websocket_callback_initial) - ) - return swap, task - - -def start_onchain_listener(swap: ReverseSubmarineSwap) -> asyncio.Task: - return create_task_log_exception( - swap.id, wait_for_onchain_tx(swap, swap_websocket_callback_restart) - ) - - -async def start_confirmation_listener( - swap: ReverseSubmarineSwap, mempool_lockup_tx -) -> asyncio.Task: - logger.debug(f"Boltz - reverse swap, waiting for confirmation...") - - tx, txid, *_ = mempool_lockup_tx - - confirmed = await wait_for_websocket_message({"track-tx": txid}, "txConfirmed") - if confirmed: - logger.debug(f"Boltz - reverse swap lockup transaction confirmed! claiming...") - await create_claim_tx(swap, mempool_lockup_tx) - else: - logger.debug(f"Boltz - reverse swap lockup transaction still not confirmed.") - - -def create_task_log_exception(swap_id: str, awaitable: Awaitable) -> asyncio.Task: - async def _log_exception(awaitable): - try: - return await awaitable - except Exception as e: - logger.error(f"Boltz - reverse swap failed!: {swap_id} - {e}") - await update_swap_status(swap_id, "failed") - - return asyncio.create_task(_log_exception(awaitable)) - - -async def swap_websocket_callback_initial(swap): - wstask = asyncio.create_task( - wait_for_websocket_message( - {"track-address": swap.lockup_address}, "address-transactions" - ) - ) - logger.debug( - f"Boltz - created task, waiting on mempool websocket for address: {swap.lockup_address}" - ) - - # create_task is used because pay_invoice is stuck as long as boltz does not - # see the onchain claim tx and it ends up in deadlock - task: asyncio.Task = create_task_log_exception( - swap.id, - pay_invoice( - wallet_id=swap.wallet, - payment_request=swap.invoice, - description=f"reverse swap for {swap.amount} sats on boltz.exchange", - extra={"tag": "boltz", "swap_id": swap.id, "reverse": True}, - ), - ) - logger.debug(f"Boltz - task pay_invoice created, reverse swap_id: {swap.id}") - - done, pending = await asyncio.wait( - [task, wstask], return_when=asyncio.FIRST_COMPLETED - ) - message = done.pop().result() - - # pay_invoice already failed, do not wait for onchain tx anymore - if message is None: - logger.debug(f"Boltz - pay_invoice already failed cancel websocket task.") - wstask.cancel() - raise - - return task, message - - -async def swap_websocket_callback_restart(swap): - logger.debug(f"Boltz - swap_websocket_callback_restart called...") - message = await wait_for_websocket_message( - {"track-address": swap.lockup_address}, "address-transactions" - ) - return None, message - - -async def wait_for_onchain_tx(swap: ReverseSubmarineSwap, callback): - task, txs = await callback(swap) - mempool_lockup_tx = get_mempool_tx_from_txs(txs, swap.lockup_address) - if mempool_lockup_tx: - tx, txid, *_ = mempool_lockup_tx - if swap.instant_settlement or tx["status"]["confirmed"]: - logger.debug( - f"Boltz - reverse swap instant settlement, claiming immediatly..." - ) - await create_claim_tx(swap, mempool_lockup_tx) - else: - await start_confirmation_listener(swap, mempool_lockup_tx) - try: - if task: - await task - except: - logger.error( - f"Boltz - could not await pay_invoice task, but sent onchain. should never happen!" - ) - else: - logger.error(f"Boltz - mempool lockup tx not found.") - - -async def create_claim_tx(swap: ReverseSubmarineSwap, mempool_lockup_tx): - tx = await create_onchain_tx(swap, mempool_lockup_tx) - await send_onchain_tx(tx) - logger.debug(f"Boltz - onchain tx sent, reverse swap completed") - await update_swap_status(swap.id, "complete") - - -async def create_refund_tx(swap: SubmarineSwap): - mempool_lockup_tx = get_mempool_tx(swap.address) - tx = await create_onchain_tx(swap, mempool_lockup_tx) - await send_onchain_tx(tx) - - -def check_block_height(block_height: int): - current_block_height = get_mempool_blockheight() - if current_block_height <= block_height: - msg = f"refund not possible, timeout_block_height ({block_height}) is not yet exceeded ({current_block_height})" - logger.debug(msg) - raise Exception(msg) - - -""" -a submarine swap consists of 2 onchain tx's a lockup and a redeem tx. -we create a tx to redeem the funds locked by the onchain lockup tx. -claim tx for reverse swaps, refund tx for normal swaps they are the same -onchain redeem tx, the difference between them is the private key, onchain_address, -input sequence and input script_sig -""" - - -async def create_onchain_tx( - swap: Union[ReverseSubmarineSwap, SubmarineSwap], mempool_lockup_tx -) -> Transaction: - is_refund_tx = type(swap) == SubmarineSwap - if is_refund_tx: - check_block_height(swap.timeout_block_height) - privkey = ec.PrivateKey.from_wif(swap.refund_privkey) - onchain_address = swap.refund_address - preimage = b"" - sequence = 0xFFFFFFFE - else: - privkey = ec.PrivateKey.from_wif(swap.claim_privkey) - preimage = bytes.fromhex(swap.preimage) - onchain_address = swap.onchain_address - sequence = 0xFFFFFFFF - - locktime = swap.timeout_block_height - redeem_script = bytes.fromhex(swap.redeem_script) - - fees = get_fee_estimation() - - tx, txid, vout_cnt, vout_amount = mempool_lockup_tx - - script_pubkey = script.address_to_scriptpubkey(onchain_address) - - vin = [TransactionInput(bytes.fromhex(txid), vout_cnt, sequence=sequence)] - vout = [TransactionOutput(vout_amount - fees, script_pubkey)] - tx = Transaction(vin=vin, vout=vout) - - if is_refund_tx: - tx.locktime = locktime - - # TODO: 2 rounds for fee calculation, look at vbytes after signing and do another TX - s = script.Script(data=redeem_script) - for i, inp in enumerate(vin): - if is_refund_tx: - rs = bytes([34]) + bytes([0]) + bytes([32]) + sha256(redeem_script).digest() - tx.vin[i].script_sig = script.Script(data=rs) - h = tx.sighash_segwit(i, s, vout_amount) - sig = privkey.sign(h).serialize() + bytes([SIGHASH.ALL]) - witness_items = [sig, preimage, redeem_script] - tx.vin[i].witness = script.Witness(items=witness_items) - - return tx - - -def get_swap_status(swap: Union[SubmarineSwap, ReverseSubmarineSwap]) -> SwapStatus: - swap_status = SwapStatus( - wallet=swap.wallet, - swap_id=swap.id, - ) - - try: - boltz_request = get_boltz_status(swap.boltz_id) - swap_status.boltz = boltz_request["status"] - except httpx.HTTPStatusError as exc: - json = exc.response.json() - swap_status.boltz = json["error"] - if "could not find" in swap_status.boltz: - swap_status.exists = False - - if type(swap) == SubmarineSwap: - swap_status.reverse = False - swap_status.address = swap.address - else: - swap_status.reverse = True - swap_status.address = swap.lockup_address - - swap_status.block_height = get_mempool_blockheight() - swap_status.timeout_block_height = ( - f"{str(swap.timeout_block_height)} -> current: {str(swap_status.block_height)}" - ) - - if swap_status.block_height >= swap.timeout_block_height: - swap_status.hit_timeout = True - - mempool_tx = get_mempool_tx(swap_status.address) - swap_status.lockup = mempool_tx - if mempool_tx == None: - swap_status.has_lockup = False - swap_status.confirmed = False - swap_status.mempool = "transaction.unknown" - swap_status.message = "lockup tx not in mempool" - else: - swap_status.has_lockup = True - tx, *_ = mempool_tx - if tx["status"]["confirmed"] == True: - swap_status.mempool = "transaction.confirmed" - swap_status.confirmed = True - else: - swap_status.confirmed = False - swap_status.mempool = "transaction.unconfirmed" - - return swap_status - - -def check_boltz_limits(amount): - try: - pairs = get_boltz_pairs() - limits = pairs["pairs"]["BTC/BTC"]["limits"] - return amount >= limits["minimal"] and amount <= limits["maximal"] - except: - return False - - -def get_boltz_pairs(): - res = req_wrap( - "get", - f"{settings.boltz_url}/getpairs", - headers={"Content-Type": "application/json"}, - ) - return res.json() - - -def get_boltz_status(boltzid): - res = req_wrap( - "post", - f"{settings.boltz_url}/swapstatus", - json={"id": boltzid}, - ) - return res.json() diff --git a/lnbits/extensions/boltz/crud.py b/lnbits/extensions/boltz/crud.py index 1bb4286dc..621fd3656 100644 --- a/lnbits/extensions/boltz/crud.py +++ b/lnbits/extensions/boltz/crud.py @@ -1,20 +1,21 @@ -from http import HTTPStatus +import time from typing import List, Optional, Union +from boltz_client.boltz import BoltzReverseSwapResponse, BoltzSwapResponse from loguru import logger -from starlette.exceptions import HTTPException + +from lnbits.helpers import urlsafe_short_hash from . import db from .models import ( + AutoReverseSubmarineSwap, + CreateAutoReverseSubmarineSwap, CreateReverseSubmarineSwap, CreateSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap, ) - -""" -Submarine Swaps -""" +from .utils import create_boltz_client, execute_reverse_swap async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[SubmarineSwap]: @@ -30,20 +31,6 @@ async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[Submari return [SubmarineSwap(**row) for row in rows] -async def get_pending_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}) and status='pending' 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( f"SELECT * FROM boltz.submarineswap WHERE status='pending' order by time DESC", @@ -51,14 +38,20 @@ async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]: return [SubmarineSwap(**row) for row in rows] -async def get_submarine_swap(swap_id) -> SubmarineSwap: +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(swap: SubmarineSwap) -> Optional[SubmarineSwap]: +async def create_submarine_swap( + data: CreateSubmarineSwap, + swap: BoltzSwapResponse, + swap_id: str, + refund_privkey_wif: str, + payment_hash: str, +) -> Optional[SubmarineSwap]: await db.execute( """ @@ -80,26 +73,22 @@ async def create_submarine_swap(swap: SubmarineSwap) -> Optional[SubmarineSwap]: VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( + swap_id, + data.wallet, + payment_hash, + "pending", swap.id, - swap.wallet, - swap.payment_hash, - swap.status, - swap.boltz_id, - swap.refund_privkey, - swap.refund_address, - swap.expected_amount, - swap.timeout_block_height, + refund_privkey_wif, + data.refund_address, + swap.expectedAmount, + swap.timeoutBlockHeight, swap.address, swap.bip21, - swap.redeem_script, - swap.amount, + swap.redeemScript, + data.amount, ), ) - return await get_submarine_swap(swap.id) - - -async def delete_submarine_swap(swap_id): - await db.execute("DELETE FROM boltz.submarineswap WHERE id = ?", (swap_id,)) + return await get_submarine_swap(swap_id) async def get_reverse_submarine_swaps( @@ -117,21 +106,6 @@ async def get_reverse_submarine_swaps( return [ReverseSubmarineSwap(**row) for row in rows] -async def get_pending_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}) and status='pending' 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( f"SELECT * FROM boltz.reverse_submarineswap WHERE status='pending' order by time DESC" @@ -140,7 +114,7 @@ async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap return [ReverseSubmarineSwap(**row) for row in rows] -async def get_reverse_submarine_swap(swap_id) -> SubmarineSwap: +async def get_reverse_submarine_swap(swap_id) -> Optional[ReverseSubmarineSwap]: row = await db.fetchone( "SELECT * FROM boltz.reverse_submarineswap WHERE id = ?", (swap_id,) ) @@ -148,8 +122,31 @@ async def get_reverse_submarine_swap(swap_id) -> SubmarineSwap: async def create_reverse_submarine_swap( - swap: ReverseSubmarineSwap, -) -> Optional[ReverseSubmarineSwap]: + 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( """ @@ -172,36 +169,93 @@ async def create_reverse_submarine_swap( VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( - swap.id, + 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.status, - swap.boltz_id, - swap.instant_settlement, - swap.preimage, - swap.claim_privkey, - swap.lockup_address, - swap.invoice, - swap.onchain_amount, swap.onchain_address, - swap.timeout_block_height, - swap.redeem_script, + swap.instant_settlement, + swap.balance, swap.amount, ), ) - return await get_reverse_submarine_swap(swap.id) + 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): - reverse = "" swap = await get_submarine_swap(swap_id) - if swap is None: - swap = await get_reverse_submarine_swap(swap_id) - - if swap is None: - return None - - if type(swap) == SubmarineSwap: + if swap: await db.execute( "UPDATE boltz.submarineswap SET status='" + status @@ -209,17 +263,23 @@ async def update_swap_status(swap_id: str, status: str): + swap.id + "'" ) - if type(swap) == ReverseSubmarineSwap: - reverse = "reverse" + 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='" - + swap.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 - message = f"Boltz - {reverse} swap status change: {status}. boltz_id: {swap.boltz_id}, wallet: {swap.wallet}" - logger.info(message) - - return swap + return None diff --git a/lnbits/extensions/boltz/mempool.py b/lnbits/extensions/boltz/mempool.py deleted file mode 100644 index c7d572a91..000000000 --- a/lnbits/extensions/boltz/mempool.py +++ /dev/null @@ -1,93 +0,0 @@ -import asyncio -import json - -import httpx -import websockets -from embit.transaction import Transaction -from loguru import logger - -from lnbits.settings import settings - -from .utils import req_wrap - -websocket_url = f"{settings.boltz_mempool_space_url_ws}/api/v1/ws" - - -async def wait_for_websocket_message(send, message_string): - async for websocket in websockets.connect(websocket_url): - try: - await websocket.send(json.dumps({"action": "want", "data": ["blocks"]})) - await websocket.send(json.dumps(send)) - async for raw in websocket: - message = json.loads(raw) - if message_string in message: - return message.get(message_string) - except websockets.ConnectionClosed: - continue - - -def get_mempool_tx(address): - res = req_wrap( - "get", - f"{settings.boltz_mempool_space_url}/api/address/{address}/txs", - headers={"Content-Type": "text/plain"}, - ) - txs = res.json() - return get_mempool_tx_from_txs(txs, address) - - -def get_mempool_tx_from_txs(txs, address): - if len(txs) == 0: - return None - tx = txid = vout_cnt = vout_amount = None - for a_tx in txs: - for i, vout in enumerate(a_tx["vout"]): - if vout["scriptpubkey_address"] == address: - tx = a_tx - txid = a_tx["txid"] - vout_cnt = i - vout_amount = vout["value"] - # should never happen - if tx == None: - raise Exception("mempool tx not found") - if txid == None: - raise Exception("mempool txid not found") - return tx, txid, vout_cnt, vout_amount - - -def get_fee_estimation() -> int: - # TODO: hardcoded maximum tx size, in the future we try to get the size of the tx via embit - # we need a function like Transaction.vsize() - tx_size_vbyte = 200 - mempool_fees = get_mempool_fees() - return mempool_fees * tx_size_vbyte - - -def get_mempool_fees() -> int: - res = req_wrap( - "get", - f"{settings.boltz_mempool_space_url}/api/v1/fees/recommended", - headers={"Content-Type": "text/plain"}, - ) - fees = res.json() - return int(fees["economyFee"]) - - -def get_mempool_blockheight() -> int: - res = req_wrap( - "get", - f"{settings.boltz_mempool_space_url}/api/blocks/tip/height", - headers={"Content-Type": "text/plain"}, - ) - return int(res.text) - - -async def send_onchain_tx(tx: Transaction): - raw = bytes.hex(tx.serialize()) - logger.debug(f"Boltz - mempool sending onchain tx...") - req_wrap( - "post", - f"{settings.boltz_mempool_space_url}/api/tx", - headers={"Content-Type": "text/plain"}, - content=raw, - ) diff --git a/lnbits/extensions/boltz/migrations.py b/lnbits/extensions/boltz/migrations.py index 925322ecd..66648fccc 100644 --- a/lnbits/extensions/boltz/migrations.py +++ b/lnbits/extensions/boltz/migrations.py @@ -44,3 +44,21 @@ async def m001_initial(db): ); """ ) + + +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 index 4f4ec9e29..9500b678a 100644 --- a/lnbits/extensions/boltz/models.py +++ b/lnbits/extensions/boltz/models.py @@ -1,9 +1,5 @@ -import json -from typing import Dict, List, Optional - -from fastapi.params import Query -from pydantic.main import BaseModel -from sqlalchemy.engine import base +from fastapi import Query +from pydantic import BaseModel class SubmarineSwap(BaseModel): @@ -51,25 +47,22 @@ class CreateReverseSubmarineSwap(BaseModel): wallet: str = Query(...) amount: int = Query(...) instant_settlement: bool = Query(...) - # validate on-address, bcrt1 for regtest addresses - onchain_address: str = Query( - ..., regex="^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$" - ) + onchain_address: str = Query(...) -class SwapStatus(BaseModel): - swap_id: str +class AutoReverseSubmarineSwap(BaseModel): + id: str wallet: str - status: str = "" - message: str = "" - boltz: str = "" - mempool: str = "" - address: str = "" - block_height: int = 0 - timeout_block_height: str = "" - lockup: Optional[dict] = {} - has_lockup: bool = False - hit_timeout: bool = False - confirmed: bool = True - exists: bool = True - reverse: bool = False + 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/tasks.py b/lnbits/extensions/boltz/tasks.py index d1ace04b8..ba394164b 100644 --- a/lnbits/extensions/boltz/tasks.py +++ b/lnbits/extensions/boltz/tasks.py @@ -1,129 +1,25 @@ import asyncio -import httpx +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 +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 .boltz import ( - create_claim_tx, - create_refund_tx, - get_swap_status, - start_confirmation_listener, - start_onchain_listener, -) from .crud import ( + create_reverse_submarine_swap, get_all_pending_reverse_submarine_swaps, get_all_pending_submarine_swaps, - get_reverse_submarine_swap, + get_auto_reverse_submarine_swap_by_wallet, get_submarine_swap, update_swap_status, ) - -""" -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 -> 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 instant -> kill -> no lockup -> start lnbits -> should start onchain listener -> boltz does lockup -> should claim/complete (difficult to test) - 3. test: create -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should start tx listener -> after confirmation -> should claim/complete - 4. test: create -> kill -> boltz does lockup -> confirmed -> start lnbits -> should claim/complete - 5. test: create -> kill -> boltz does lockup -> hit timeout -> boltz refunds -> start -> should timeout -""" - - -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(f"Boltz - startup swap check") - except: - # database is not created yet, do nothing - return - - if len(swaps) > 0: - logger.debug(f"Boltz - {len(swaps)} pending swaps") - for swap in swaps: - try: - swap_status = get_swap_status(swap) - # should only happen while development when regtest is reset - if swap_status.exists is False: - logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.") - await update_swap_status(swap.id, "failed") - continue - - 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: - if swap_status.hit_timeout: - if not swap_status.has_lockup: - logger.debug( - f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..." - ) - await update_swap_status(swap.id, "timeout") - else: - logger.debug(f"Boltz - refunding swap: {swap.id}...") - await create_refund_tx(swap) - await update_swap_status(swap.id, "refunded") - - except Exception as exc: - logger.error(f"Boltz - swap: {swap.id} - {str(exc)}") - - if len(reverse_swaps) > 0: - logger.debug(f"Boltz - {len(reverse_swaps)} pending reverse swaps") - for reverse_swap in reverse_swaps: - try: - swap_status = get_swap_status(reverse_swap) - - if swap_status.exists is False: - logger.debug( - f"Boltz - reverse_swap: {reverse_swap.boltz_id} does not exist." - ) - await update_swap_status(reverse_swap.id, "failed") - continue - - # if timeout hit, boltz would have already refunded - if swap_status.hit_timeout: - logger.debug( - f"Boltz - reverse_swap: {reverse_swap.boltz_id} timeout." - ) - await update_swap_status(reverse_swap.id, "timeout") - continue - - if not swap_status.has_lockup: - # start listener for onchain address - logger.debug( - f"Boltz - reverse_swap: {reverse_swap.boltz_id} restarted onchain address listener." - ) - await start_onchain_listener(reverse_swap) - continue - - if reverse_swap.instant_settlement or swap_status.confirmed: - await create_claim_tx(reverse_swap, swap_status.lockup) - else: - logger.debug( - f"Boltz - reverse_swap: {reverse_swap.boltz_id} restarted confirmation listener." - ) - await start_confirmation_listener(reverse_swap, swap_status.lockup) - - except Exception as exc: - logger.error(f"Boltz - reverse swap: {reverse_swap.id} - {str(exc)}") +from .models import CreateReverseSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap +from .utils import create_boltz_client, execute_reverse_swap async def wait_for_paid_invoices(): @@ -136,19 +32,149 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if "boltz" != payment.extra.get("tag"): + + await check_for_auto_swap(payment) + + if payment.extra.get("tag") != "boltz": # not a boltz invoice return await payment.set_pending(False) - swap_id = payment.extra.get("swap_id") - swap = await get_submarine_swap(swap_id) - if not swap: - logger.error(f"swap_id: {swap_id} not found.") + 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(f"Boltz - startup swap check") + except: + logger.error( + f"Boltz - startup swap check, database is not created yet, do nothing" + ) return - logger.info( - f"Boltz - lightning invoice is paid, normal swap completed. swap_id: {swap_id}" - ) - await update_swap_status(swap_id, "complete") + 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 as exc: + logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.") + await update_swap_status(swap.id, "failed") + except MempoolBlockHeightException as exc: + 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 as exc: + 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 index 704a8db56..bdcbc8ca2 100644 --- a/lnbits/extensions/boltz/templates/boltz/_api_docs.html +++ b/lnbits/extensions/boltz/templates/boltz/_api_docs.html @@ -1,242 +1,35 @@ - - - - - -
- Boltz.exchange: Do onchain to offchain and vice-versa swaps -
-

- Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange - API
-

-

- Link : - https://boltz.exchange - -

-

- More details -

-

- Created by, - dni -

-
-
-
- - - - - GET - /boltz/api/v1/swap/reverse -
- Returns 200 OK (application/json) -
- JSON list of reverse submarine swaps -
Curl example
- curl -X GET {{ root_url }}/boltz/api/v1/swap/reverse -H "X-Api-Key: - {{ user.wallets[0].adminkey }}" - -
-
-
- - - - POST - /boltz/api/v1/swap/reverse -
Body (application/json)
- {"wallet": <string>, "onchain_address": <string>, - "amount": <integer>, "instant_settlement": - <boolean>} -
- Returns 200 OK (application/json) -
- JSON create a reverse-submarine swaps -
Curl example
- curl -X POST {{ root_url }}/boltz/api/v1/swap/reverse -H "X-Api-Key: - {{ user.wallets[0].adminkey }}" - -
-
-
- - - - GET /boltz/api/v1/swap -
- Returns 200 OK (application/json) -
- JSON list of submarine swaps -
Curl example
- curl -X GET {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - POST /boltz/api/v1/swap -
Body (application/json)
- {"wallet": <string>, "refund_address": <string>, - "amount": <integer>} -
- Returns 200 OK (application/json) -
- JSON create a submarine swaps -
Curl example
- curl -X POST {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - POST - /boltz/api/v1/swap/refund/{swap_id} -
- Returns 200 OK (application/json) -
- JSON submarine swap -
Curl example
- curl -X GET {{ root_url }}/boltz/api/v1/swap/refund/{swap_id} -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" - -
-
-
- - - - POST - /boltz/api/v1/swap/status/{swap_id} -
- Returns 200 OK (text/plain) -
- swap status -
Curl example
- curl -X GET {{ root_url }}/boltz/api/v1/swap/status/{swap_id} -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /boltz/api/v1/swap/check -
- Returns 200 OK (application/json) -
- JSON pending swaps -
Curl example
- curl -X GET {{ root_url }}/boltz/api/v1/swap/check -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /boltz/api/v1/swap/boltz -
- Returns 200 OK (text/plain) -
- JSON boltz config -
Curl example
- curl -X GET {{ root_url }}/boltz/api/v1/swap/boltz -H "X-Api-Key: {{ - user.wallets[0].inkey }}" - -
-
-
- - - - GET - /boltz/api/v1/swap/mempool -
- Returns 200 OK (text/plain) -
- mempool url -
Curl example
- curl -X GET {{ root_url }}/boltz/api/v1/swap/mempool -H "X-Api-Key: - {{ user.wallets[0].inkey }}" - -
-
-
-
+ + + + +
+ Boltz.exchange: Do onchain to offchain and vice-versa swaps +
+

+ Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange API
+

+

+ Link : + https://boltz.exchange + +

+

+ More details +

+

+ Created by, + dni +

+
+
diff --git a/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html b/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html new file mode 100644 index 000000000..c9c682a8b --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapDialog.html @@ -0,0 +1,83 @@ + + + + + + + + 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 new file mode 100644 index 000000000..b297524f1 --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_autoReverseSwapList.html @@ -0,0 +1,54 @@ + + +
+
+
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 new file mode 100644 index 000000000..3817b0766 --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_buttons.html @@ -0,0 +1,35 @@ + + + + + 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 new file mode 100644 index 000000000..e59702d24 --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html @@ -0,0 +1,113 @@ + + +
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 new file mode 100644 index 000000000..053ef65e7 --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_qrDialog.html @@ -0,0 +1,31 @@ + + + + + +
+ {% 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 new file mode 100644 index 000000000..5b3cf861a --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapDialog.html @@ -0,0 +1,72 @@ + + + + + + + +
+
+ + + 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 new file mode 100644 index 000000000..fc9668d0d --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_reverseSubmarineSwapList.html @@ -0,0 +1,66 @@ + + +
+
+
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 new file mode 100644 index 000000000..f6c14abc9 --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_statusDialog.html @@ -0,0 +1,29 @@ + + +
+ {% 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 new file mode 100644 index 000000000..bf6aaa187 --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_submarineSwapDialog.html @@ -0,0 +1,58 @@ + + + + + + + + +
+ + + Cancel +
+
+
+
diff --git a/lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html b/lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html new file mode 100644 index 000000000..b42e1dee9 --- /dev/null +++ b/lnbits/extensions/boltz/templates/boltz/_submarineSwapList.html @@ -0,0 +1,78 @@ + + +
+
+
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 index b7312de7a..308c3a46e 100644 --- a/lnbits/extensions/boltz/templates/boltz/index.html +++ b/lnbits/extensions/boltz/templates/boltz/index.html @@ -1,531 +1,19 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block page %}
-
- - - - - Send onchain funds offchain (BTC -> LN) - - - - - Send offchain funds to onchain address (LN -> BTC) - - - - - Check all pending swaps if they can be refunded. - - - - - - -
-
-
Swaps (In)
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - -
-
-
Reverse Swaps (Out)
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
+
+ {% include "boltz/_buttons.html" %} {% include + "boltz/_submarineSwapList.html" %} {% include + "boltz/_reverseSubmarineSwapList.html" %} {% include + "boltz/_autoReverseSwapList.html" %}
-
- - -
{{SITE_TITLE}} Boltz extension
-
- - - {% include "boltz/_api_docs.html" %} - -
+
+ {% include "boltz/_api_docs.html" %}
- - - - - - - - -
- - - Cancel -
-
-
-
- - - - - - - -
-
- - - Create Onchain TX when transaction is in mempool, but not - confirmed yet. - - -
-
- -
- - - Cancel -
-
-
-
- - - - - -
- {% 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 -
-
-
- - -
- {% raw %} - Wallet: {{ statusDialog.data.wallet }}
- Boltz Status: {{ statusDialog.data.boltz }}
- Mempool Status: {{ statusDialog.data.mempool }}
- Blockheight timeout: {{ statusDialog.data.timeout_block_height - }}
- {% endraw %} -
-
- Refund - - Download refundfile - Close -
-
-
- - -
pending swaps
- - {% raw %} - - - {% endraw %} - -
pending reverse swaps
- - {% raw %} - - - {% endraw %} - -
- Close -
-
-
+ {% 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) }} diff --git a/lnbits/extensions/boltz/utils.py b/lnbits/extensions/boltz/utils.py index 4fb2edda2..7623fb6f6 100644 --- a/lnbits/extensions/boltz/utils.py +++ b/lnbits/extensions/boltz/utils.py @@ -1,10 +1,25 @@ +import asyncio import calendar import datetime +from typing import Awaitable -import httpx -from loguru import logger +from boltz_client.boltz import BoltzClient, BoltzConfig -from lnbits.core.services import fee_reserve, get_wallet +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: @@ -23,22 +38,50 @@ def get_timestamp(): return calendar.timegm(date.utctimetuple()) -def req_wrap(funcname, *args, **kwargs): - try: +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: - func = getattr(httpx, funcname) - except AttributeError: - logger.error('httpx function not found "%s"' % funcname) - else: - res = func(*args, timeout=30, **kwargs) - res.raise_for_status() - return res - except httpx.RequestError as exc: - msg = f"Unreachable: {exc.request.url!r}." - logger.error(msg) - raise - except httpx.HTTPStatusError as exc: - msg = f"HTTP Status Error: {exc.response.status_code} while requesting {exc.request.url!r}." - logger.error(msg) - logger.error(exc.response.json()["error"]) - raise + 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 index b6864113c..4b0e6d535 100644 --- a/lnbits/extensions/boltz/views.py +++ b/lnbits/extensions/boltz/views.py @@ -1,11 +1,10 @@ from urllib.parse import urlparse -from fastapi import Request -from fastapi.params import Depends +from fastapi import Depends, Request from fastapi.templating import Jinja2Templates from starlette.responses import HTMLResponse -from lnbits.core.models import Payment, User +from lnbits.core.models import User from lnbits.decorators import check_user_exists from . import boltz_ext, boltz_renderer @@ -16,7 +15,6 @@ 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 - wallet_ids = [wallet.id for wallet in user.wallets] 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 index 34f4033e8..ab32fac9a 100644 --- a/lnbits/extensions/boltz/views_api.py +++ b/lnbits/extensions/boltz/views_api.py @@ -1,34 +1,23 @@ -from datetime import datetime from http import HTTPStatus from typing import List -import httpx -from fastapi import status -from fastapi.encoders import jsonable_encoder -from fastapi.param_functions import Body -from fastapi.params import Depends, Query -from loguru import logger -from pydantic import BaseModel +from fastapi import Depends, Query, status from starlette.exceptions import HTTPException -from starlette.requests import Request 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 .boltz import ( - create_refund_tx, - create_reverse_swap, - create_swap, - get_boltz_pairs, - get_swap_status, -) from .crud import ( + create_auto_reverse_submarine_swap, create_reverse_submarine_swap, create_submarine_swap, - get_pending_reverse_submarine_swaps, - get_pending_submarine_swaps, + 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, @@ -36,12 +25,14 @@ from .crud import ( update_swap_status, ) from .models import ( + AutoReverseSubmarineSwap, + CreateAutoReverseSubmarineSwap, CreateReverseSubmarineSwap, CreateSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap, ) -from .utils import check_balance +from .utils import check_balance, create_boltz_client, execute_reverse_swap @boltz_ext.get( @@ -76,17 +67,8 @@ async def api_submarineswap( ): wallet_ids = [g.wallet.id] if all_wallets: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - for swap in await get_pending_submarine_swaps(wallet_ids): - swap_status = get_swap_status(swap) - if swap_status.hit_timeout: - if not swap_status.has_lockup: - logger.warning( - f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..." - ) - await update_swap_status(swap.id, "timeout") - + 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)] @@ -109,35 +91,29 @@ async def api_submarineswap( }, }, ) -async def api_submarineswap_refund( - swap_id: str, - g: WalletTypeInfo = Depends(require_admin_key), -): - if swap_id == None: +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 swap == None: + 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." ) - try: - await create_refund_tx(swap) - except httpx.RequestError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Unreachable: {exc.request.url!r}.", - ) - except Exception as exc: - raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc)) + 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 @@ -153,37 +129,43 @@ async def api_submarineswap_refund( """, response_description="create swap", response_model=SubmarineSwap, + dependencies=[Depends(require_admin_key)], responses={ - 405: {"description": "not allowed method, insufficient balance"}, + 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, - wallet: WalletTypeInfo = Depends(require_admin_key), -): - try: - swap_data = await create_swap(data) - except httpx.RequestError as exc: +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.INTERNAL_SERVER_ERROR, - detail=f"Unreachable: {exc.request.url!r}.", + status_code=HTTPStatus.METHOD_NOT_ALLOWED, + detail="auto reverse swap is active, a swap would immediatly be swapped out again.", ) - except Exception as exc: - raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc)) - except httpx.HTTPStatusError as exc: - raise HTTPException( - status_code=exc.response.status_code, detail=exc.response.json()["error"] - ) - swap = await create_submarine_swap(swap_data) - return swap.dict() + + 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=f"boltz.get /swap/reverse", - summary="get a list of reverse swaps a swap", + summary="get a list of reverse swaps", description=""" This endpoint gets a list of reverse swaps. """, @@ -192,13 +174,14 @@ async def api_submarineswap_create( response_model=List[ReverseSubmarineSwap], ) async def api_reverse_submarineswap( - g: WalletTypeInfo = Depends(get_key_type), # type:ignore + g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False), ): wallet_ids = [g.wallet.id] if all_wallets: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - return [swap.dict() for swap in await get_reverse_submarine_swaps(wallet_ids)] + 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( @@ -211,6 +194,7 @@ async def api_reverse_submarineswap( """, 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"}, @@ -218,30 +202,88 @@ async def api_reverse_submarineswap( ) async def api_reverse_submarineswap_create( data: CreateReverseSubmarineSwap, - wallet: WalletTypeInfo = Depends(require_admin_key), -): +) -> 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 - try: - swap_data, task = await create_reverse_swap(data) - except httpx.RequestError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Unreachable: {exc.request.url!r}.", - ) - except httpx.HTTPStatusError as exc: - raise HTTPException( - status_code=exc.response.status_code, detail=exc.response.json()["error"] - ) - except Exception as exc: - raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc)) - swap = await create_reverse_submarine_swap(swap_data) - return swap.dict() +@boltz_ext.get( + "/api/v1/swap/reverse/auto", + name=f"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=f"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=f"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( @@ -252,65 +294,22 @@ async def api_reverse_submarineswap_create( 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, wallet: WalletTypeInfo = Depends(require_admin_key) -): +async def api_swap_status(swap_id: str): swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap( swap_id ) - if swap == None: + if not swap: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist." ) - try: - status = get_swap_status(swap) - except httpx.RequestError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Unreachable: {exc.request.url!r}.", - ) - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc) - ) - return status - -@boltz_ext.post( - "/api/v1/swap/check", - name=f"boltz.swap_check", - summary="list all pending swaps", - description=""" - This endpoint gives you 2 lists of pending swaps and reverse swaps. - """, - response_description="list of pending swaps", -) -async def api_check_swaps( - g: WalletTypeInfo = Depends(require_admin_key), - all_wallets: bool = Query(False), -): - wallet_ids = [g.wallet.id] - if all_wallets: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - status = [] - try: - for swap in await get_pending_submarine_swaps(wallet_ids): - status.append(get_swap_status(swap)) - for reverseswap in await get_pending_reverse_submarine_swaps(wallet_ids): - status.append(get_swap_status(reverseswap)) - except httpx.RequestError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Unreachable: {exc.request.url!r}.", - ) - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc) - ) + client = create_boltz_client() + status = client.swap_status(swap.boltz_id) return status @@ -325,14 +324,5 @@ async def api_check_swaps( response_model=dict, ) async def api_boltz_config(): - try: - res = get_boltz_pairs() - except httpx.RequestError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Unreachable: {exc.request.url!r}.", - ) - except Exception as e: - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) - - return res["pairs"]["BTC/BTC"] + client = create_boltz_client() + return {"minimal": client.limit_minimal, "maximal": client.limit_maximal} diff --git a/lnbits/extensions/deezy/templates/deezy/index.html b/lnbits/extensions/deezy/templates/deezy/index.html index 858d32550..9d112ef13 100644 --- a/lnbits/extensions/deezy/templates/deezy/index.html +++ b/lnbits/extensions/deezy/templates/deezy/index.html @@ -224,7 +224,7 @@
-
{{SITE_TITLE}} Boltz extension
+
{{SITE_TITLE}} Deezy extension
diff --git a/poetry.lock b/poetry.lock index 5a99e198f..46dc503bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -175,6 +175,24 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "boltz-client" +version = "0.1.2" +description = "python boltz client" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "boltz_client-0.1.2-py3-none-any.whl", hash = "sha256:2fb0814c7c3ea88d039e71088648df27db0c036b777b0618bd30638dd76ebe90"}, + {file = "boltz_client-0.1.2.tar.gz", hash = "sha256:b360c0ff26f2dea62af6457de4d8c46e434cd24b607ed3aa71494409b57e082b"}, +] + +[package.dependencies] +click = ">=8" +embit = ">=0.4" +httpx = ">=0.23" +websockets = ">=10" + [[package]] name = "cashu" version = "0.8.2" @@ -532,10 +550,10 @@ files = [ cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools-rust (>=0.11.4)"] +sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] @@ -1784,7 +1802,7 @@ mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] mysql = ["mysqlclient"] -oracle = ["cx-oracle"] +oracle = ["cx_oracle"] postgresql = ["psycopg2"] postgresql-pg8000 = ["pg8000 (<1.16.6)"] postgresql-psycopg2binary = ["psycopg2-binary"] @@ -2094,4 +2112,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7" -content-hash = "9daf94dd600a7e23dcefcc8752fae1694e0084e56553dc578a63272776a8fe53" +content-hash = "b2d22a2a33b4c0a4491b5519b28772435c15747b407a150ffa591bcf6ccb56a6" diff --git a/pyproject.toml b/pyproject.toml index 68e06db5e..23fe0dae1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,8 @@ protobuf = "^4.21.6" Cerberus = "^1.3.4" async-timeout = "^4.0.2" pyln-client = "0.11.1" -cashu = "0.8.2" +cashu = "^0.8.2" +boltz-client = "^0.1.2" [tool.poetry.dev-dependencies] @@ -88,8 +89,7 @@ profile = "black" [tool.mypy] files = "lnbits" exclude = """(?x)( - ^lnbits/extensions/boltz. - | ^lnbits/wallets/lnd_grpc_files. + ^lnbits/wallets/lnd_grpc_files. )""" [[tool.mypy.overrides]] diff --git a/requirements.txt b/requirements.txt index c5332811a..1a0872db8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ attrs==22.2.0 ; python_version >= "3.7" and python_version < "4.0" base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0" bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0" bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0" +boltz-client==0.1.2 ; python_version >= "3.7" and python_version < "4.0" cashu==0.8.2 ; python_version >= "3.7" and python_version < "4.0" cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0" certifi==2022.12.7 ; python_version >= "3.7" and python_version < "4.0" diff --git a/tests/extensions/boltz/conftest.py b/tests/extensions/boltz/conftest.py index b9ef78875..1eba452a9 100644 --- a/tests/extensions/boltz/conftest.py +++ b/tests/extensions/boltz/conftest.py @@ -1,17 +1,6 @@ -import asyncio -import json -import secrets - -import pytest import pytest_asyncio -from lnbits.core.crud import create_account, create_wallet, get_wallet -from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap -from lnbits.extensions.boltz.models import ( - CreateReverseSubmarineSwap, - CreateSubmarineSwap, -) -from tests.mocks import WALLET +from lnbits.extensions.boltz.models import CreateReverseSubmarineSwap @pytest_asyncio.fixture(scope="session") @@ -22,4 +11,4 @@ async def reverse_swap(from_wallet): onchain_address="bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43", amount=20_000, ) - return await create_reverse_swap(data) + return data diff --git a/tests/extensions/boltz/test_api.py b/tests/extensions/boltz/test_api.py index 0266f9400..057bdab5c 100644 --- a/tests/extensions/boltz/test_api.py +++ b/tests/extensions/boltz/test_api.py @@ -1,7 +1,6 @@ import pytest -import pytest_asyncio -from tests.helpers import is_fake, is_regtest +from tests.helpers import is_fake @pytest.mark.asyncio diff --git a/tests/extensions/boltz/test_swap.py b/tests/extensions/boltz/test_swap.py deleted file mode 100644 index ab5954acb..000000000 --- a/tests/extensions/boltz/test_swap.py +++ /dev/null @@ -1,31 +0,0 @@ -import asyncio - -import pytest -import pytest_asyncio - -from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap -from lnbits.extensions.boltz.crud import ( - create_reverse_submarine_swap, - create_submarine_swap, - get_reverse_submarine_swap, - get_submarine_swap, -) -from tests.extensions.boltz.conftest import reverse_swap -from tests.helpers import is_fake, is_regtest - - -@pytest.mark.asyncio -@pytest.mark.skipif(is_fake, reason="this test is only passes in regtest") -async def test_create_reverse_swap(client, reverse_swap): - swap, wait_for_onchain = reverse_swap - assert swap.status == "pending" - assert swap.id is not None - assert swap.boltz_id is not None - assert swap.claim_privkey is not None - assert swap.onchain_address is not None - assert swap.lockup_address is not None - newswap = await create_reverse_submarine_swap(swap) - await wait_for_onchain - newswap = await get_reverse_submarine_swap(swap.id) - assert newswap is not None - assert newswap.status == "complete"