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:
parent
d8b5e3872b
commit
d89a6a337a
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
+ """
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -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(...)
|
||||
|
|
|
@ -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)}"
|
||||
)
|
||||
|
|
|
@ -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": <string>, "onchain_address": <string>,
|
||||
"amount": <integer>, "instant_settlement":
|
||||
<boolean>}</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": <string>, "refund_address": <string>,
|
||||
"amount": <integer>}</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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
35
lnbits/extensions/boltz/templates/boltz/_buttons.html
Normal file
35
lnbits/extensions/boltz/templates/boltz/_buttons.html
Normal 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>
|
113
lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html
Normal file
113
lnbits/extensions/boltz/templates/boltz/_checkSwapDialog.html
Normal 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>
|
31
lnbits/extensions/boltz/templates/boltz/_qrDialog.html
Normal file
31
lnbits/extensions/boltz/templates/boltz/_qrDialog.html
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
29
lnbits/extensions/boltz/templates/boltz/_statusDialog.html
Normal file
29
lnbits/extensions/boltz/templates/boltz/_statusDialog.html
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
26
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
Loading…
Reference in New Issue
Block a user