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>
This commit is contained in:
dni ⚡ 2023-01-19 10:30:47 +01:00 committed by GitHub
parent d8b5e3872b
commit d89a6a337a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1380 additions and 1782 deletions

View File

@ -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()

View File

@ -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

View File

@ -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,
)

View File

@ -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
+ """
);
"""
)

View File

@ -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(...)

View File

@ -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)}"
)

View File

@ -1,242 +1,35 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="About Boltz"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<img
src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg"
alt=""
/>
<img
src="https://boltz.exchange/static/media/Boltz.02fb7acb.svg"
style="padding: 5px 9px"
alt=""
/>
<h5 class="text-subtitle1 q-my-none">
Boltz.exchange: Do onchain to offchain and vice-versa swaps
</h5>
<p>
Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange
API<br />
</p>
<p>
Link :
<a class="text-secondary" target="_blank" href="https://boltz.exchange"
>https://boltz.exchange
</a>
</p>
<p>
<a
class="text-secondary"
target="_blank"
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltz"
>More details</a
>
</p>
<p>
<small
>Created by,
<a
class="text-secondary"
target="_blank"
href="https://github.com/dni"
>dni</a
></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="GET swap/reverse">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/boltz/api/v1/swap/reverse</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON list of reverse submarine swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/reverse -H "X-Api-Key:
{{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="POST swap/reverse"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span>
/boltz/api/v1/swap/reverse</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"wallet": &lt;string&gt;, "onchain_address": &lt;string&gt;,
"amount": &lt;integer&gt;, "instant_settlement":
&lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON create a reverse-submarine swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ root_url }}/boltz/api/v1/swap/reverse -H "X-Api-Key:
{{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap">
<q-card>
<q-card-section>
<code><span class="text-light-blue">GET</span> /boltz/api/v1/swap</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON list of submarine swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST swap">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span> /boltz/api/v1/swap</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"wallet": &lt;string&gt;, "refund_address": &lt;string&gt;,
"amount": &lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON create a submarine swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap/refund">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span>
/boltz/api/v1/swap/refund/{swap_id}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON submarine swap</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/refund/{swap_id} -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap/status">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span>
/boltz/api/v1/swap/status/{swap_id}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (text/plain)
</h5>
<code>swap status</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/status/{swap_id} -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap/check">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/boltz/api/v1/swap/check</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON pending swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/check -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET boltz-config">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/boltz/api/v1/swap/boltz</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (text/plain)
</h5>
<code>JSON boltz config</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/boltz -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET mempool-url">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/boltz/api/v1/swap/mempool</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (text/plain)
</h5>
<code>mempool url</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/mempool -H "X-Api-Key:
{{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
<q-card>
<q-card-section>
<img src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg" alt="" />
<img
src="https://boltz.exchange/static/media/Boltz.02fb7acb.svg"
style="padding: 5px 9px"
alt=""
/>
<h5 class="text-subtitle1 q-my-none">
Boltz.exchange: Do onchain to offchain and vice-versa swaps
</h5>
<p>
Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange API<br />
</p>
<p>
Link :
<a target="_blank" href="https://boltz.exchange"
>https://boltz.exchange
</a>
</p>
<p>
<a
target="_blank"
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
>More details</a
>
</p>
<p>
<small
>Created by,
<a target="_blank" href="https://github.com/dni">dni</a></small
>
</p>
</q-card-section>
</q-card>

View File

@ -0,0 +1,83 @@
<q-dialog v-model="autoReverseSubmarineSwapDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendAutoReverseSubmarineSwapFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="autoReverseSubmarineSwapDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
:disable="autoReverseSubmarineSwapDialog.data.id ? true : false"
>
</q-select>
<q-input
filled
dense
emit-value
label="Balance to kept + fee_reserve"
v-model="autoReverseSubmarineSwapDialog.data.balance"
type="number"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
mininum balance kept in wallet after a swap + the fee_reserve
</q-tooltip>
</q-input>
<q-input
filled
dense
emit-value
:label="amountLabel()"
v-model.trim="autoReverseSubmarineSwapDialog.data.amount"
type="number"
></q-input>
<div class="row">
<div class="col">
<q-checkbox
v-model="autoReverseSubmarineSwapDialog.data.instant_settlement"
value="false"
label="Instant settlement"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Create Onchain TX when transaction is in mempool, but not
confirmed yet.
</q-tooltip>
</q-checkbox>
</div>
</div>
<q-input
filled
dense
emit-value
v-model.trim="autoReverseSubmarineSwapDialog.data.onchain_address"
type="string"
label="Onchain address to receive funds"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="autoReverseSubmarineSwapDialog.data.id"
unelevated
color="primary"
type="submit"
label="Update Swap"
></q-btn>
<q-btn
v-else
unelevated
color="primary"
:disable="disableAutoReverseSubmarineSwapDialog()"
type="submit"
label="Create Auto Reverse Swap (Out)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
@click="resetAutoReverseSubmarineSwapDialog"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>

View File

@ -0,0 +1,54 @@
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Auto Lightning -> Onchain</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportAutoReverseSubmarineSwapCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="autoReverseSubmarineSwaps"
row-key="id"
:columns="autoReverseSubmarineSwapTable.columns"
:pagination.sync="autoReverseSubmarineSwapTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td>
<q-btn
unelevated
dense
size="xs"
icon="delete"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="deleteAutoReverseSwap(props.row.id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>delete the automatic reverse swap</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>

View File

@ -0,0 +1,35 @@
<q-card>
<q-card-section>
<q-btn
label="Onchain -> Lightning"
unelevated
color="primary"
@click="submarineSwapDialog.show = true"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send onchain funds offchain (BTC -> LN)
</q-tooltip>
</q-btn>
<q-btn
label="Lightning -> Onchain"
unelevated
color="primary"
@click="reverseSubmarineSwapDialog.show = true"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send offchain funds to onchain address (LN -> BTC)
</q-tooltip>
</q-btn>
<q-btn
label="Auto (Lightning -> Onchain)"
unelevated
color="primary"
@click="autoReverseSubmarineSwapDialog.show = true"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Automatically send offchain funds to onchain address (LN -> BTC) with a
predefined threshold
</q-tooltip>
</q-btn>
</q-card-section>
</q-card>

View File

@ -0,0 +1,113 @@
<q-dialog v-model="checkSwapDialog.show" maximized position="top">
<q-card v-if="checkSwapDialog.data" class="q-pa-lg lnbits__dialog-card">
<h5>pending swaps</h5>
<q-table
dense
flat
:data="checkSwapDialog.data.swaps"
row-key="id"
:columns="allStatusTable.columns"
:rows-per-page-options="[0]"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="cached"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="refundSwap(props.row.swap_id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>refund swap</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="download"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="downloadRefundFile(props.row.swap_id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>dowload refund file</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.swap_id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
<h5>pending reverse swaps</h5>
<q-table
dense
flat
:data="checkSwapDialog.data.reverse_swaps"
row-key="id"
:columns="allStatusTable.columns"
:rows-per-page-options="[0]"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.swap_id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
<div class="row q-mt-lg q-gutter-sm">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>

View File

@ -0,0 +1,31 @@
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="qrCodeDialog.data.bip21"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div>
{% raw %}
<b>Bitcoin On-Chain TX</b><br />
<b>Expected amount (sats): </b> {{ qrCodeDialog.data.expected_amount }}
<br />
<b>Expected amount (btc): </b> {{ qrCodeDialog.data.expected_amount_btc }}
<br />
<b>Onchain Address: </b> {{ qrCodeDialog.data.address }} <br />
{% endraw %}
</div>
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.address, 'Onchain address copied to clipboard!')"
class="q-ml-sm"
>Copy On-Chain Address</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>

View File

@ -0,0 +1,72 @@
<q-dialog v-model="reverseSubmarineSwapDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendReverseSubmarineSwapFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="reverseSubmarineSwapDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
:disable="reverseSubmarineSwapDialog.data.id ? true : false"
>
</q-select>
<q-input
filled
dense
emit-value
:label="amountLabel()"
v-model.trim="reverseSubmarineSwapDialog.data.amount"
type="number"
></q-input>
<div class="row">
<div class="col">
<q-checkbox
v-model="reverseSubmarineSwapDialog.data.instant_settlement"
value="false"
label="Instant settlement"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Create Onchain TX when transaction is in mempool, but not
confirmed yet.
</q-tooltip>
</q-checkbox>
</div>
</div>
<q-input
filled
dense
emit-value
v-model.trim="reverseSubmarineSwapDialog.data.onchain_address"
type="string"
label="Onchain address to receive funds"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="reverseSubmarineSwapDialog.data.id"
unelevated
color="primary"
type="submit"
label="Update Swap"
></q-btn>
<q-btn
v-else
unelevated
color="primary"
:disable="disableReverseSubmarineSwapDialog()"
type="submit"
label="Create Reverse Swap (OUT)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
@click="resetReverseSubmarineSwapDialog"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>

View File

@ -0,0 +1,66 @@
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Lightning -> Onchain</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportReverseSubmarineSwapCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="reverseSubmarineSwaps"
row-key="id"
:columns="reverseSubmarineSwapTable.columns"
:pagination.sync="reverseSubmarineSwapTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="info"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openStatusDialog(props.row.id, true)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>open swap status info</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>

View File

@ -0,0 +1,29 @@
<q-dialog v-model="statusDialog.show" position="top">
<q-card v-if="statusDialog.data" class="q-pa-lg lnbits__dialog-card">
<div>
{% raw %}
<b>Status: </b> {{ statusDialog.data.status }} <br />
<br />
{% endraw %}
</div>
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="refundSwap(statusDialog.data.swap_id)"
v-if="!statusDialog.data.reverse"
class="q-ml-sm"
>Refund
</q-btn>
<q-btn
outline
color="grey"
@click="downloadRefundFile(statusDialog.data.swap_id)"
v-if="!statusDialog.data.reverse"
class="q-ml-sm"
>Download refundfile</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>

View File

@ -0,0 +1,58 @@
<q-dialog v-model="submarineSwapDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendSubmarineSwapFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="submarineSwapDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
:disable="submarineSwapDialog.data.id ? true : false"
>
</q-select>
<q-input
filled
dense
emit-value
v-model.trim="submarineSwapDialog.data.amount"
:label="amountLabel()"
type="number"
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="submarineSwapDialog.data.refund_address"
type="string"
label="Onchain address to receive funds if swap fails"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="submarineSwapDialog.data.id"
unelevated
color="primary"
type="submit"
label="Update Swap"
></q-btn>
<q-btn
v-else
unelevated
color="primary"
:disable="disableSubmarineSwapDialog()"
type="submit"
label="Create Swap (IN)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
@click="resetSubmarineSwapDialog"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>

View File

@ -0,0 +1,78 @@
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Onchain -> Lightning</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportSubmarineSwapCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="submarineSwaps"
row-key="id"
:columns="submarineSwapTable.columns"
:pagination.sync="submarineSwapTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>open swap onchain details</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="info"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openStatusDialog(props.row.id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>open swap status info</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.id)"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>

View File

@ -1,531 +1,19 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
label="Swap (In)"
unelevated
color="primary"
@click="submarineSwapDialog.show = true"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send onchain funds offchain (BTC -> LN)
</q-tooltip>
</q-btn>
<q-btn
label="Reverse Swap (Out)"
unelevated
color="primary"
@click="reverseSubmarineSwapDialog.show = true"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send offchain funds to onchain address (LN -> BTC)
</q-tooltip>
</q-btn>
<q-btn
label="Check Swaps"
icon="cached"
unelevated
color="primary"
@click="checkSwaps"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Check all pending swaps if they can be refunded.
</q-tooltip>
</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Swaps (In)</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportSubmarineSwapCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="submarineSwaps"
row-key="id"
:columns="submarineSwapTable.columns"
:pagination.sync="submarineSwapTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open swap onchain details</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="info"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openStatusDialog(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open swap status info</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Reverse Swaps (Out)</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportReverseSubmarineSwapCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="reverseSubmarineSwaps"
row-key="id"
:columns="reverseSubmarineSwapTable.columns"
:pagination.sync="reverseSubmarineSwapTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="info"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openStatusDialog(props.row.id, true)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open swap status info</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<div class="col-12 col-md-8 q-gutter-y-md">
{% include "boltz/_buttons.html" %} {% include
"boltz/_submarineSwapList.html" %} {% include
"boltz/_reverseSubmarineSwapList.html" %} {% include
"boltz/_autoReverseSwapList.html" %}
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Boltz extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "boltz/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
<div class="col-12 col-md-4 q-gutter-y-md">
{% include "boltz/_api_docs.html" %}
</div>
<q-dialog v-model="submarineSwapDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendSubmarineSwapFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="submarineSwapDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
:disable="submarineSwapDialog.data.id ? true : false"
>
</q-select>
<q-input
filled
dense
emit-value
v-model.trim="submarineSwapDialog.data.amount"
:label="amountLabel()"
type="number"
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="submarineSwapDialog.data.refund_address"
type="string"
label="Onchain address to receive funds if swap fails"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="submarineSwapDialog.data.id"
unelevated
color="primary"
type="submit"
label="Update Swap"
></q-btn>
<q-btn
v-else
unelevated
color="primary"
:disable="disableSubmarineSwapDialog()"
type="submit"
label="Create Swap (IN)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
@click="resetSubmarineSwapDialog"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="reverseSubmarineSwapDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendReverseSubmarineSwapFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="reverseSubmarineSwapDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
:disable="reverseSubmarineSwapDialog.data.id ? true : false"
>
</q-select>
<q-input
filled
dense
emit-value
:label="amountLabel()"
v-model.trim="reverseSubmarineSwapDialog.data.amount"
type="number"
></q-input>
<div class="row">
<div class="col">
<q-checkbox
v-model="reverseSubmarineSwapDialog.data.instant_settlement"
value="false"
label="Instant settlement"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Create Onchain TX when transaction is in mempool, but not
confirmed yet.
</q-tooltip>
</q-checkbox>
</div>
</div>
<q-input
filled
dense
emit-value
v-model.trim="reverseSubmarineSwapDialog.data.onchain_address"
type="string"
label="Onchain address to receive funds"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="reverseSubmarineSwapDialog.data.id"
unelevated
color="primary"
type="submit"
label="Update Swap"
></q-btn>
<q-btn
v-else
unelevated
color="primary"
:disable="disableReverseSubmarineSwapDialog()"
type="submit"
label="Create Reverse Swap (OUT)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
@click="resetReverseSubmarineSwapDialog"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="qrCodeDialog.data.bip21"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div>
{% raw %}
<b>Bitcoin On-Chain TX</b><br />
<b>Expected amount (sats): </b> {{ qrCodeDialog.data.expected_amount }}
<br />
<b>Expected amount (btc): </b> {{ qrCodeDialog.data.expected_amount_btc
}} <br />
<b>Onchain Address: </b> {{ qrCodeDialog.data.address }} <br />
{% endraw %}
</div>
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.address, 'Onchain address copied to clipboard!')"
class="q-ml-sm"
>Copy On-Chain Address</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="statusDialog.show" position="top">
<q-card v-if="statusDialog.data" class="q-pa-lg lnbits__dialog-card">
<div>
{% raw %}
<b>Wallet: </b> {{ statusDialog.data.wallet }} <br />
<b>Boltz Status: </b> {{ statusDialog.data.boltz }} <br />
<b>Mempool Status: </b> {{ statusDialog.data.mempool }} <br />
<b>Blockheight timeout: </b> {{ statusDialog.data.timeout_block_height
}} <br />
{% endraw %}
</div>
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="refundSwap(statusDialog.data.swap_id)"
v-if="!statusDialog.data.reverse"
class="q-ml-sm"
>Refund
</q-btn>
<q-btn
outline
color="grey"
@click="downloadRefundFile(statusDialog.data.swap_id)"
v-if="!statusDialog.data.reverse"
class="q-ml-sm"
>Download refundfile</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="allStatusDialog.show" maximized position="top">
<q-card v-if="allStatusDialog.data" class="q-pa-lg lnbits__dialog-card">
<h5>pending swaps</h5>
<q-table
dense
flat
:data="allStatusDialog.data.swaps"
row-key="id"
:columns="allStatusTable.columns"
:rows-per-page-options="[0]"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="cached"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="refundSwap(props.row.swap_id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>refund swap</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="download"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="downloadRefundFile(props.row.swap_id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>dowload refund file</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.swap_id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
<h5>pending reverse swaps</h5>
<q-table
dense
flat
:data="allStatusDialog.data.reverse_swaps"
row-key="id"
:columns="allStatusTable.columns"
:rows-per-page-options="[0]"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.swap_id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
<div class="row q-mt-lg q-gutter-sm">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
{% include "boltz/_submarineSwapDialog.html" %} {% include
"boltz/_reverseSubmarineSwapDialog.html" %} {% include
"boltz/_autoReverseSwapDialog.html" %} {% include "boltz/_qrDialog.html" %} {%
include "boltz/_statusDialog.html" %}
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
@ -539,6 +27,7 @@
boltzConfig: {},
submarineSwaps: [],
reverseSubmarineSwaps: [],
autoReverseSubmarineSwaps: [],
statuses: [],
submarineSwapDialog: {
show: false,
@ -550,6 +39,13 @@
instant_settlement: true
}
},
autoReverseSubmarineSwapDialog: {
show: false,
data: {
balance: 100,
instant_settlement: true
}
},
qrCodeDialog: {
show: false,
data: {}
@ -558,40 +54,36 @@
show: false,
data: {}
},
allStatusDialog: {
show: false,
data: {}
},
allStatusTable: {
columns: [
{
name: 'swap_id',
align: 'left',
label: 'swap_id',
label: 'Swap ID',
field: 'swap_id'
},
{
name: 'status',
align: 'left',
label: 'status',
label: 'Status',
field: 'message'
},
{
name: 'boltz',
align: 'left',
label: 'boltz',
label: 'Boltz',
field: 'boltz'
},
{
name: 'mempool',
align: 'left',
label: 'mempool',
label: 'Mempool',
field: 'mempool'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'block height',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
@ -599,12 +91,60 @@
rowsPerPage: 10
}
},
autoReverseSubmarineSwapTable: {
columns: [
{
name: 'time',
align: 'left',
label: 'Time',
field: 'time',
sortable: true,
format: function (val, row) {
return new Date(val * 1000).toUTCString()
}
},
{
name: 'wallet',
align: 'left',
label: 'Wallet',
field: data => {
let wallet = _.findWhere(this.g.user.wallets, {
id: data.wallet
})
if (wallet) {
return wallet.name
}
}
},
{
name: 'balance',
align: 'left',
label: 'Balance',
field: 'balance'
},
{
name: 'amount',
align: 'left',
label: 'Amount',
field: 'amount'
},
{
name: 'onchain_address',
align: 'left',
label: 'Onchain address',
field: 'onchain_address'
}
],
pagination: {
rowsPerPage: 10
}
},
reverseSubmarineSwapTable: {
columns: [
{
name: 'time',
align: 'left',
label: 'time',
label: 'Time',
field: 'time',
sortable: true,
format: function (val, row) {
@ -614,7 +154,7 @@
{
name: 'wallet',
align: 'left',
label: 'wallet',
label: 'Wallet',
field: data => {
let wallet = _.findWhere(this.g.user.wallets, {
id: data.wallet
@ -627,25 +167,25 @@
{
name: 'status',
align: 'left',
label: 'status',
label: 'Status',
field: 'status'
},
{
name: 'boltz_id',
align: 'left',
label: 'boltz id',
label: 'Boltz ID',
field: 'boltz_id'
},
{
name: 'onchain_amount',
align: 'left',
label: 'onchain amount',
label: 'Onchain amount',
field: 'onchain_amount'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'timeout block height',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
@ -658,7 +198,7 @@
{
name: 'time',
align: 'left',
label: 'time',
label: 'Time',
field: 'time',
sortable: true,
format: function (val, row) {
@ -668,7 +208,7 @@
{
name: 'wallet',
align: 'left',
label: 'wallet',
label: 'Wallet',
field: data => {
let wallet = _.findWhere(this.g.user.wallets, {
id: data.wallet
@ -681,25 +221,25 @@
{
name: 'status',
align: 'left',
label: 'status',
label: 'Status',
field: 'status'
},
{
name: 'boltz_id',
align: 'left',
label: 'boltz id',
label: 'Boltz ID',
field: 'boltz_id'
},
{
name: 'expected_amount',
align: 'left',
label: 'expected amount',
label: 'Expected amount',
field: 'expected_amount'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'timeout block height',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
@ -711,11 +251,10 @@
},
methods: {
getLimits() {
const cfg = this.boltzConfig.data
if (cfg) {
if (this.boltzConfig) {
return {
min: cfg.limits.minimal,
max: cfg.limits.maximal
min: this.boltzConfig.minimal,
max: this.boltzConfig.maximal
}
}
return {
@ -753,6 +292,19 @@
data.amount > limits.max
)
},
disableAutoReverseSubmarineSwapDialog() {
const data = this.autoReverseSubmarineSwapDialog.data
let limits = this.getLimits()
return (
data.onchain_address == null ||
data.onchain_address.search(
/^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/
) !== 0 ||
data.wallet == null ||
data.amount < limits.min ||
data.amount > limits.max
)
},
downloadRefundFile(swapId) {
let swap = _.findWhere(this.submarineSwaps, {id: swapId})
let json = {
@ -816,6 +368,7 @@
swap_id: swap_id,
wallet: res.data.wallet,
boltz: res.data.boltz,
status: res.data.status,
mempool: res.data.mempool,
timeout_block_height: res.data.timeout_block_height,
date: new Date().toUTCString()
@ -847,12 +400,6 @@
data: {}
}
},
resetAllStatusDialog() {
this.allStatusDialog = {
show: false,
data: {}
}
},
resetSubmarineSwapDialog() {
this.submarineSwapDialog = {
show: false,
@ -865,6 +412,12 @@
data: {}
}
},
resetAutoReverseSubmarineSwapDialog() {
this.autoReverseSubmarineSwapDialog = {
show: false,
data: {}
}
},
sendReverseSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.reverseSubmarineSwapDialog.data.wallet
@ -872,6 +425,13 @@
let data = this.reverseSubmarineSwapDialog.data
this.createReverseSubmarineSwap(wallet, data)
},
sendAutoReverseSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.autoReverseSubmarineSwapDialog.data.wallet
})
let data = this.autoReverseSubmarineSwapDialog.data
this.createAutoReverseSubmarineSwap(wallet, data)
},
sendSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.submarineSwapDialog.data.wallet
@ -891,6 +451,12 @@
this.reverseSubmarineSwaps
)
},
exportAutoReverseSubmarineSwapCSV() {
LNbits.utils.exportCSV(
this.autoReverseSubmarineSwapTable.columns,
this.autoReverseSubmarineSwaps
)
},
createSubmarineSwap(wallet, data) {
LNbits.api
.request(
@ -924,6 +490,40 @@
LNbits.utils.notifyApiError(error)
})
},
createAutoReverseSubmarineSwap(wallet, data) {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap/reverse/auto',
this.g.user.wallets[0].adminkey,
data
)
.then(res => {
this.autoReverseSubmarineSwaps.unshift(res.data)
this.resetAutoReverseSubmarineSwapDialog()
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
deleteAutoReverseSwap(swap_id) {
LNbits.api
.request(
'DELETE',
'/boltz/api/v1/swap/reverse/auto/' + swap_id,
this.g.user.wallets[0].adminkey
)
.then(res => {
let i = this.autoReverseSubmarineSwaps.findIndex(
swap => swap.id === swap_id
)
this.autoReverseSubmarineSwaps.splice(i, 1)
})
.catch(error => {
console.log(error)
LNbits.utils.notifyApiError(error)
})
},
getSubmarineSwap() {
LNbits.api
.request(
@ -952,6 +552,20 @@
LNbits.utils.notifyApiError(error)
})
},
getAutoReverseSubmarineSwap() {
LNbits.api
.request(
'GET',
'/boltz/api/v1/swap/reverse/auto?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.autoReverseSubmarineSwaps = response.data
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getMempool() {
LNbits.api
.request('GET', '/boltz/api/v1/swap/mempool')
@ -967,26 +581,7 @@
LNbits.api
.request('GET', '/boltz/api/v1/swap/boltz')
.then(res => {
this.boltzConfig = res
})
.catch(error => {
console.log('error', error)
LNbits.utils.notifyApiError(error)
})
},
checkSwaps() {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap/check',
this.g.user.wallets[0].adminkey
)
.then(res => {
this.allStatusDialog.data = {
swaps: _.where(res.data, {reverse: false}),
reverse_swaps: _.where(res.data, {reverse: true})
}
this.allStatusDialog.show = true
this.boltzConfig = res.data
})
.catch(error => {
console.log('error', error)
@ -999,6 +594,7 @@
this.getBoltzConfig()
this.getSubmarineSwap()
this.getReverseSubmarineSwap()
this.getAutoReverseSubmarineSwap()
}
})
</script>

View File

@ -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))

View File

@ -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},

View File

@ -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}

View File

@ -224,7 +224,7 @@
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Boltz extension</h6>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Deezy extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>

26
poetry.lock generated
View File

@ -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"

View File

@ -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]]

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"