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 @@
-
- Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange
- API
- Link :
- https://boltz.exchange
-
-
- More details
-
- Created by,
- dni
-
+ Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange API
+ Link :
+ https://boltz.exchange
+
+
+ More details
+
+ Created by,
+ dni
+
-
-
- Boltz.exchange: Do onchain to offchain and vice-versa swaps
-
-
- 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
+
+
+ Auto Lightning -> Onchain
+ pending swaps
+ pending reverse swaps
+
+ Expected amount (sats): {{ qrCodeDialog.data.expected_amount }}
+
+ Expected amount (btc): {{ qrCodeDialog.data.expected_amount_btc }}
+
+ Onchain Address: {{ qrCodeDialog.data.address }}
+ {% endraw %}
+ Lightning -> Onchain
+
+
+ {% endraw %}
+ Onchain -> Lightning
+