mirror of
https://github.com/lnbits/lnbits.git
synced 2025-05-06 01:30:18 +02:00
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 typing import List, Optional, Union
|
||||||
|
|
||||||
|
from boltz_client.boltz import BoltzReverseSwapResponse, BoltzSwapResponse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import (
|
from .models import (
|
||||||
|
AutoReverseSubmarineSwap,
|
||||||
|
CreateAutoReverseSubmarineSwap,
|
||||||
CreateReverseSubmarineSwap,
|
CreateReverseSubmarineSwap,
|
||||||
CreateSubmarineSwap,
|
CreateSubmarineSwap,
|
||||||
ReverseSubmarineSwap,
|
ReverseSubmarineSwap,
|
||||||
SubmarineSwap,
|
SubmarineSwap,
|
||||||
)
|
)
|
||||||
|
from .utils import create_boltz_client, execute_reverse_swap
|
||||||
"""
|
|
||||||
Submarine Swaps
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[SubmarineSwap]:
|
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]
|
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]:
|
async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
f"SELECT * FROM boltz.submarineswap WHERE status='pending' order by time DESC",
|
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]
|
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(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM boltz.submarineswap WHERE id = ?", (swap_id,)
|
"SELECT * FROM boltz.submarineswap WHERE id = ?", (swap_id,)
|
||||||
)
|
)
|
||||||
return SubmarineSwap(**row) if row else None
|
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(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
@ -80,26 +73,22 @@ async def create_submarine_swap(swap: SubmarineSwap) -> Optional[SubmarineSwap]:
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
swap_id,
|
||||||
|
data.wallet,
|
||||||
|
payment_hash,
|
||||||
|
"pending",
|
||||||
swap.id,
|
swap.id,
|
||||||
swap.wallet,
|
refund_privkey_wif,
|
||||||
swap.payment_hash,
|
data.refund_address,
|
||||||
swap.status,
|
swap.expectedAmount,
|
||||||
swap.boltz_id,
|
swap.timeoutBlockHeight,
|
||||||
swap.refund_privkey,
|
|
||||||
swap.refund_address,
|
|
||||||
swap.expected_amount,
|
|
||||||
swap.timeout_block_height,
|
|
||||||
swap.address,
|
swap.address,
|
||||||
swap.bip21,
|
swap.bip21,
|
||||||
swap.redeem_script,
|
swap.redeemScript,
|
||||||
swap.amount,
|
data.amount,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return await get_submarine_swap(swap.id)
|
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,))
|
|
||||||
|
|
||||||
|
|
||||||
async def get_reverse_submarine_swaps(
|
async def get_reverse_submarine_swaps(
|
||||||
@ -117,21 +106,6 @@ async def get_reverse_submarine_swaps(
|
|||||||
return [ReverseSubmarineSwap(**row) for row in rows]
|
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]:
|
async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
f"SELECT * FROM boltz.reverse_submarineswap WHERE status='pending' order by time DESC"
|
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]
|
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(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM boltz.reverse_submarineswap WHERE id = ?", (swap_id,)
|
"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(
|
async def create_reverse_submarine_swap(
|
||||||
swap: ReverseSubmarineSwap,
|
data: CreateReverseSubmarineSwap,
|
||||||
) -> Optional[ReverseSubmarineSwap]:
|
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(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
@ -172,36 +169,93 @@ async def create_reverse_submarine_swap(
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
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.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.onchain_address,
|
||||||
swap.timeout_block_height,
|
swap.instant_settlement,
|
||||||
swap.redeem_script,
|
swap.balance,
|
||||||
swap.amount,
|
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):
|
async def update_swap_status(swap_id: str, status: str):
|
||||||
|
|
||||||
reverse = ""
|
|
||||||
swap = await get_submarine_swap(swap_id)
|
swap = await get_submarine_swap(swap_id)
|
||||||
if swap is None:
|
if swap:
|
||||||
swap = await get_reverse_submarine_swap(swap_id)
|
|
||||||
|
|
||||||
if swap is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if type(swap) == SubmarineSwap:
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE boltz.submarineswap SET status='"
|
"UPDATE boltz.submarineswap SET status='"
|
||||||
+ status
|
+ status
|
||||||
@ -209,17 +263,23 @@ async def update_swap_status(swap_id: str, status: str):
|
|||||||
+ swap.id
|
+ swap.id
|
||||||
+ "'"
|
+ "'"
|
||||||
)
|
)
|
||||||
if type(swap) == ReverseSubmarineSwap:
|
logger.info(
|
||||||
reverse = "reverse"
|
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(
|
await db.execute(
|
||||||
"UPDATE boltz.reverse_submarineswap SET status='"
|
"UPDATE boltz.reverse_submarineswap SET status='"
|
||||||
+ status
|
+ status
|
||||||
+ "' WHERE id='"
|
+ "' 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}"
|
return None
|
||||||
logger.info(message)
|
|
||||||
|
|
||||||
return swap
|
|
||||||
|
@ -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 fastapi import Query
|
||||||
from typing import Dict, List, Optional
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from fastapi.params import Query
|
|
||||||
from pydantic.main import BaseModel
|
|
||||||
from sqlalchemy.engine import base
|
|
||||||
|
|
||||||
|
|
||||||
class SubmarineSwap(BaseModel):
|
class SubmarineSwap(BaseModel):
|
||||||
@ -51,25 +47,22 @@ class CreateReverseSubmarineSwap(BaseModel):
|
|||||||
wallet: str = Query(...)
|
wallet: str = Query(...)
|
||||||
amount: int = Query(...)
|
amount: int = Query(...)
|
||||||
instant_settlement: bool = Query(...)
|
instant_settlement: bool = Query(...)
|
||||||
# validate on-address, bcrt1 for regtest addresses
|
onchain_address: str = Query(...)
|
||||||
onchain_address: str = Query(
|
|
||||||
..., regex="^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SwapStatus(BaseModel):
|
class AutoReverseSubmarineSwap(BaseModel):
|
||||||
swap_id: str
|
id: str
|
||||||
wallet: str
|
wallet: str
|
||||||
status: str = ""
|
amount: int
|
||||||
message: str = ""
|
balance: int
|
||||||
boltz: str = ""
|
onchain_address: str
|
||||||
mempool: str = ""
|
instant_settlement: bool
|
||||||
address: str = ""
|
time: int
|
||||||
block_height: int = 0
|
|
||||||
timeout_block_height: str = ""
|
|
||||||
lockup: Optional[dict] = {}
|
class CreateAutoReverseSubmarineSwap(BaseModel):
|
||||||
has_lockup: bool = False
|
wallet: str = Query(...)
|
||||||
hit_timeout: bool = False
|
amount: int = Query(...)
|
||||||
confirmed: bool = True
|
balance: int = Query(0)
|
||||||
exists: bool = True
|
instant_settlement: bool = Query(...)
|
||||||
reverse: bool = False
|
onchain_address: str = Query(...)
|
||||||
|
@ -1,129 +1,25 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import httpx
|
from boltz_client.boltz import BoltzNotFoundException, BoltzSwapStatusException
|
||||||
|
from boltz_client.mempool import MempoolBlockHeightException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.models import Payment
|
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.helpers import get_current_extension_name
|
||||||
from lnbits.tasks import register_invoice_listener
|
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 (
|
from .crud import (
|
||||||
|
create_reverse_submarine_swap,
|
||||||
get_all_pending_reverse_submarine_swaps,
|
get_all_pending_reverse_submarine_swaps,
|
||||||
get_all_pending_submarine_swaps,
|
get_all_pending_submarine_swaps,
|
||||||
get_reverse_submarine_swap,
|
get_auto_reverse_submarine_swap_by_wallet,
|
||||||
get_submarine_swap,
|
get_submarine_swap,
|
||||||
update_swap_status,
|
update_swap_status,
|
||||||
)
|
)
|
||||||
|
from .models import CreateReverseSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap
|
||||||
"""
|
from .utils import create_boltz_client, execute_reverse_swap
|
||||||
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)}")
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
async def wait_for_paid_invoices():
|
||||||
@ -136,19 +32,149 @@ async def wait_for_paid_invoices():
|
|||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
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
|
# not a boltz invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
await payment.set_pending(False)
|
await payment.set_pending(False)
|
||||||
swap_id = payment.extra.get("swap_id")
|
|
||||||
swap = await get_submarine_swap(swap_id)
|
|
||||||
|
|
||||||
if not swap:
|
if payment.extra:
|
||||||
logger.error(f"swap_id: {swap_id} not found.")
|
swap_id = payment.extra.get("swap_id")
|
||||||
return
|
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(
|
logger.info(
|
||||||
f"Boltz - lightning invoice is paid, normal swap completed. swap_id: {swap_id}"
|
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
|
||||||
|
|
||||||
|
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)}"
|
||||||
)
|
)
|
||||||
await update_swap_status(swap_id, "complete")
|
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
<q-expansion-item
|
<q-card>
|
||||||
group="extras"
|
|
||||||
icon="swap_vertical_circle"
|
|
||||||
label="About Boltz"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<img
|
<img src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg" alt="" />
|
||||||
src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<img
|
<img
|
||||||
src="https://boltz.exchange/static/media/Boltz.02fb7acb.svg"
|
src="https://boltz.exchange/static/media/Boltz.02fb7acb.svg"
|
||||||
style="padding: 5px 9px"
|
style="padding: 5px 9px"
|
||||||
@ -19,224 +10,26 @@
|
|||||||
Boltz.exchange: Do onchain to offchain and vice-versa swaps
|
Boltz.exchange: Do onchain to offchain and vice-versa swaps
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange
|
Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange API<br />
|
||||||
API<br />
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Link :
|
Link :
|
||||||
<a class="text-secondary" target="_blank" href="https://boltz.exchange"
|
<a target="_blank" href="https://boltz.exchange"
|
||||||
>https://boltz.exchange
|
>https://boltz.exchange
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
class="text-secondary"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltz"
|
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
||||||
>More details</a
|
>More details</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<small
|
<small
|
||||||
>Created by,
|
>Created by,
|
||||||
<a
|
<a target="_blank" href="https://github.com/dni">dni</a></small
|
||||||
class="text-secondary"
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/dni"
|
|
||||||
>dni</a
|
|
||||||
></small
|
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</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>
|
|
||||||
|
@ -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
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}
|
%} {% block page %}
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||||
<q-card>
|
{% include "boltz/_buttons.html" %} {% include
|
||||||
<q-card-section>
|
"boltz/_submarineSwapList.html" %} {% include
|
||||||
<q-btn
|
"boltz/_reverseSubmarineSwapList.html" %} {% include
|
||||||
label="Swap (In)"
|
"boltz/_autoReverseSwapList.html" %}
|
||||||
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>
|
||||||
<div class="col-auto">
|
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||||
<q-btn flat color="grey" @click="exportSubmarineSwapCSV"
|
{% include "boltz/_api_docs.html" %}
|
||||||
>Export to CSV</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% include "boltz/_submarineSwapDialog.html" %} {% include
|
||||||
<q-table
|
"boltz/_reverseSubmarineSwapDialog.html" %} {% include
|
||||||
dense
|
"boltz/_autoReverseSwapDialog.html" %} {% include "boltz/_qrDialog.html" %} {%
|
||||||
flat
|
include "boltz/_statusDialog.html" %}
|
||||||
: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>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script>
|
||||||
@ -539,6 +27,7 @@
|
|||||||
boltzConfig: {},
|
boltzConfig: {},
|
||||||
submarineSwaps: [],
|
submarineSwaps: [],
|
||||||
reverseSubmarineSwaps: [],
|
reverseSubmarineSwaps: [],
|
||||||
|
autoReverseSubmarineSwaps: [],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
submarineSwapDialog: {
|
submarineSwapDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
@ -550,6 +39,13 @@
|
|||||||
instant_settlement: true
|
instant_settlement: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
autoReverseSubmarineSwapDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
balance: 100,
|
||||||
|
instant_settlement: true
|
||||||
|
}
|
||||||
|
},
|
||||||
qrCodeDialog: {
|
qrCodeDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {}
|
||||||
@ -558,40 +54,36 @@
|
|||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {}
|
||||||
},
|
},
|
||||||
allStatusDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
allStatusTable: {
|
allStatusTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'swap_id',
|
name: 'swap_id',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'swap_id',
|
label: 'Swap ID',
|
||||||
field: 'swap_id'
|
field: 'swap_id'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'status',
|
label: 'Status',
|
||||||
field: 'message'
|
field: 'message'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'boltz',
|
name: 'boltz',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'boltz',
|
label: 'Boltz',
|
||||||
field: 'boltz'
|
field: 'boltz'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'mempool',
|
name: 'mempool',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'mempool',
|
label: 'Mempool',
|
||||||
field: 'mempool'
|
field: 'mempool'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeout_block_height',
|
name: 'timeout_block_height',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'block height',
|
label: 'Timeout block height',
|
||||||
field: 'timeout_block_height'
|
field: 'timeout_block_height'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -599,12 +91,60 @@
|
|||||||
rowsPerPage: 10
|
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: {
|
reverseSubmarineSwapTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'time',
|
label: 'Time',
|
||||||
field: 'time',
|
field: 'time',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
format: function (val, row) {
|
format: function (val, row) {
|
||||||
@ -614,7 +154,7 @@
|
|||||||
{
|
{
|
||||||
name: 'wallet',
|
name: 'wallet',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'wallet',
|
label: 'Wallet',
|
||||||
field: data => {
|
field: data => {
|
||||||
let wallet = _.findWhere(this.g.user.wallets, {
|
let wallet = _.findWhere(this.g.user.wallets, {
|
||||||
id: data.wallet
|
id: data.wallet
|
||||||
@ -627,25 +167,25 @@
|
|||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'status',
|
label: 'Status',
|
||||||
field: 'status'
|
field: 'status'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'boltz_id',
|
name: 'boltz_id',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'boltz id',
|
label: 'Boltz ID',
|
||||||
field: 'boltz_id'
|
field: 'boltz_id'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'onchain_amount',
|
name: 'onchain_amount',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'onchain amount',
|
label: 'Onchain amount',
|
||||||
field: 'onchain_amount'
|
field: 'onchain_amount'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeout_block_height',
|
name: 'timeout_block_height',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'timeout block height',
|
label: 'Timeout block height',
|
||||||
field: 'timeout_block_height'
|
field: 'timeout_block_height'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -658,7 +198,7 @@
|
|||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'time',
|
label: 'Time',
|
||||||
field: 'time',
|
field: 'time',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
format: function (val, row) {
|
format: function (val, row) {
|
||||||
@ -668,7 +208,7 @@
|
|||||||
{
|
{
|
||||||
name: 'wallet',
|
name: 'wallet',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'wallet',
|
label: 'Wallet',
|
||||||
field: data => {
|
field: data => {
|
||||||
let wallet = _.findWhere(this.g.user.wallets, {
|
let wallet = _.findWhere(this.g.user.wallets, {
|
||||||
id: data.wallet
|
id: data.wallet
|
||||||
@ -681,25 +221,25 @@
|
|||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'status',
|
label: 'Status',
|
||||||
field: 'status'
|
field: 'status'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'boltz_id',
|
name: 'boltz_id',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'boltz id',
|
label: 'Boltz ID',
|
||||||
field: 'boltz_id'
|
field: 'boltz_id'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'expected_amount',
|
name: 'expected_amount',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'expected amount',
|
label: 'Expected amount',
|
||||||
field: 'expected_amount'
|
field: 'expected_amount'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeout_block_height',
|
name: 'timeout_block_height',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'timeout block height',
|
label: 'Timeout block height',
|
||||||
field: 'timeout_block_height'
|
field: 'timeout_block_height'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -711,11 +251,10 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getLimits() {
|
getLimits() {
|
||||||
const cfg = this.boltzConfig.data
|
if (this.boltzConfig) {
|
||||||
if (cfg) {
|
|
||||||
return {
|
return {
|
||||||
min: cfg.limits.minimal,
|
min: this.boltzConfig.minimal,
|
||||||
max: cfg.limits.maximal
|
max: this.boltzConfig.maximal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -753,6 +292,19 @@
|
|||||||
data.amount > limits.max
|
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) {
|
downloadRefundFile(swapId) {
|
||||||
let swap = _.findWhere(this.submarineSwaps, {id: swapId})
|
let swap = _.findWhere(this.submarineSwaps, {id: swapId})
|
||||||
let json = {
|
let json = {
|
||||||
@ -816,6 +368,7 @@
|
|||||||
swap_id: swap_id,
|
swap_id: swap_id,
|
||||||
wallet: res.data.wallet,
|
wallet: res.data.wallet,
|
||||||
boltz: res.data.boltz,
|
boltz: res.data.boltz,
|
||||||
|
status: res.data.status,
|
||||||
mempool: res.data.mempool,
|
mempool: res.data.mempool,
|
||||||
timeout_block_height: res.data.timeout_block_height,
|
timeout_block_height: res.data.timeout_block_height,
|
||||||
date: new Date().toUTCString()
|
date: new Date().toUTCString()
|
||||||
@ -847,12 +400,6 @@
|
|||||||
data: {}
|
data: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetAllStatusDialog() {
|
|
||||||
this.allStatusDialog = {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetSubmarineSwapDialog() {
|
resetSubmarineSwapDialog() {
|
||||||
this.submarineSwapDialog = {
|
this.submarineSwapDialog = {
|
||||||
show: false,
|
show: false,
|
||||||
@ -865,6 +412,12 @@
|
|||||||
data: {}
|
data: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
resetAutoReverseSubmarineSwapDialog() {
|
||||||
|
this.autoReverseSubmarineSwapDialog = {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
sendReverseSubmarineSwapFormData() {
|
sendReverseSubmarineSwapFormData() {
|
||||||
let wallet = _.findWhere(this.g.user.wallets, {
|
let wallet = _.findWhere(this.g.user.wallets, {
|
||||||
id: this.reverseSubmarineSwapDialog.data.wallet
|
id: this.reverseSubmarineSwapDialog.data.wallet
|
||||||
@ -872,6 +425,13 @@
|
|||||||
let data = this.reverseSubmarineSwapDialog.data
|
let data = this.reverseSubmarineSwapDialog.data
|
||||||
this.createReverseSubmarineSwap(wallet, 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() {
|
sendSubmarineSwapFormData() {
|
||||||
let wallet = _.findWhere(this.g.user.wallets, {
|
let wallet = _.findWhere(this.g.user.wallets, {
|
||||||
id: this.submarineSwapDialog.data.wallet
|
id: this.submarineSwapDialog.data.wallet
|
||||||
@ -891,6 +451,12 @@
|
|||||||
this.reverseSubmarineSwaps
|
this.reverseSubmarineSwaps
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
exportAutoReverseSubmarineSwapCSV() {
|
||||||
|
LNbits.utils.exportCSV(
|
||||||
|
this.autoReverseSubmarineSwapTable.columns,
|
||||||
|
this.autoReverseSubmarineSwaps
|
||||||
|
)
|
||||||
|
},
|
||||||
createSubmarineSwap(wallet, data) {
|
createSubmarineSwap(wallet, data) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
@ -924,6 +490,40 @@
|
|||||||
LNbits.utils.notifyApiError(error)
|
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() {
|
getSubmarineSwap() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
@ -952,6 +552,20 @@
|
|||||||
LNbits.utils.notifyApiError(error)
|
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() {
|
getMempool() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/boltz/api/v1/swap/mempool')
|
.request('GET', '/boltz/api/v1/swap/mempool')
|
||||||
@ -967,26 +581,7 @@
|
|||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/boltz/api/v1/swap/boltz')
|
.request('GET', '/boltz/api/v1/swap/boltz')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.boltzConfig = res
|
this.boltzConfig = res.data
|
||||||
})
|
|
||||||
.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
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log('error', error)
|
console.log('error', error)
|
||||||
@ -999,6 +594,7 @@
|
|||||||
this.getBoltzConfig()
|
this.getBoltzConfig()
|
||||||
this.getSubmarineSwap()
|
this.getSubmarineSwap()
|
||||||
this.getReverseSubmarineSwap()
|
this.getReverseSubmarineSwap()
|
||||||
|
this.getAutoReverseSubmarineSwap()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,10 +1,25 @@
|
|||||||
|
import asyncio
|
||||||
import calendar
|
import calendar
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Awaitable
|
||||||
|
|
||||||
import httpx
|
from boltz_client.boltz import BoltzClient, BoltzConfig
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
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:
|
async def check_balance(data) -> bool:
|
||||||
@ -23,22 +38,50 @@ def get_timestamp():
|
|||||||
return calendar.timegm(date.utctimetuple())
|
return calendar.timegm(date.utctimetuple())
|
||||||
|
|
||||||
|
|
||||||
def req_wrap(funcname, *args, **kwargs):
|
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:
|
try:
|
||||||
try:
|
awaited = await awaitable
|
||||||
func = getattr(httpx, funcname)
|
await update_swap_status(swap_id, "complete")
|
||||||
except AttributeError:
|
return awaited
|
||||||
logger.error('httpx function not found "%s"' % funcname)
|
except asyncio.exceptions.CancelledError:
|
||||||
else:
|
"""lnbits process was exited, do nothing and handle it in startup script"""
|
||||||
res = func(*args, timeout=30, **kwargs)
|
except:
|
||||||
res.raise_for_status()
|
wstask.cancel()
|
||||||
return res
|
await update_swap_status(swap_id, "failed")
|
||||||
except httpx.RequestError as exc:
|
|
||||||
msg = f"Unreachable: {exc.request.url!r}."
|
return asyncio.create_task(_pay_invoice(awaitable))
|
||||||
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
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.params import Depends
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.responses import HTMLResponse
|
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 lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
from . import boltz_ext, boltz_renderer
|
from . import boltz_ext, boltz_renderer
|
||||||
@ -16,7 +15,6 @@ templates = Jinja2Templates(directory="templates")
|
|||||||
@boltz_ext.get("/", response_class=HTMLResponse)
|
@boltz_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
root_url = urlparse(str(request.url)).netloc
|
root_url = urlparse(str(request.url)).netloc
|
||||||
wallet_ids = [wallet.id for wallet in user.wallets]
|
|
||||||
return boltz_renderer().TemplateResponse(
|
return boltz_renderer().TemplateResponse(
|
||||||
"boltz/index.html",
|
"boltz/index.html",
|
||||||
{"request": request, "user": user.dict(), "root_url": root_url},
|
{"request": request, "user": user.dict(), "root_url": root_url},
|
||||||
|
@ -1,34 +1,23 @@
|
|||||||
from datetime import datetime
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import httpx
|
from fastapi import Depends, Query, status
|
||||||
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 starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
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.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from . import boltz_ext
|
from . import boltz_ext
|
||||||
from .boltz import (
|
|
||||||
create_refund_tx,
|
|
||||||
create_reverse_swap,
|
|
||||||
create_swap,
|
|
||||||
get_boltz_pairs,
|
|
||||||
get_swap_status,
|
|
||||||
)
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
create_auto_reverse_submarine_swap,
|
||||||
create_reverse_submarine_swap,
|
create_reverse_submarine_swap,
|
||||||
create_submarine_swap,
|
create_submarine_swap,
|
||||||
get_pending_reverse_submarine_swaps,
|
delete_auto_reverse_submarine_swap,
|
||||||
get_pending_submarine_swaps,
|
get_auto_reverse_submarine_swap_by_wallet,
|
||||||
|
get_auto_reverse_submarine_swaps,
|
||||||
get_reverse_submarine_swap,
|
get_reverse_submarine_swap,
|
||||||
get_reverse_submarine_swaps,
|
get_reverse_submarine_swaps,
|
||||||
get_submarine_swap,
|
get_submarine_swap,
|
||||||
@ -36,12 +25,14 @@ from .crud import (
|
|||||||
update_swap_status,
|
update_swap_status,
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
|
AutoReverseSubmarineSwap,
|
||||||
|
CreateAutoReverseSubmarineSwap,
|
||||||
CreateReverseSubmarineSwap,
|
CreateReverseSubmarineSwap,
|
||||||
CreateSubmarineSwap,
|
CreateSubmarineSwap,
|
||||||
ReverseSubmarineSwap,
|
ReverseSubmarineSwap,
|
||||||
SubmarineSwap,
|
SubmarineSwap,
|
||||||
)
|
)
|
||||||
from .utils import check_balance
|
from .utils import check_balance, create_boltz_client, execute_reverse_swap
|
||||||
|
|
||||||
|
|
||||||
@boltz_ext.get(
|
@boltz_ext.get(
|
||||||
@ -76,17 +67,8 @@ async def api_submarineswap(
|
|||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
wallet_ids = user.wallet_ids if user else []
|
||||||
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")
|
|
||||||
|
|
||||||
return [swap.dict() for swap in await get_submarine_swaps(wallet_ids)]
|
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(
|
async def api_submarineswap_refund(swap_id: str):
|
||||||
swap_id: str,
|
if not swap_id:
|
||||||
g: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
):
|
|
||||||
if swap_id == None:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="swap_id missing"
|
status_code=HTTPStatus.BAD_REQUEST, detail="swap_id missing"
|
||||||
)
|
)
|
||||||
|
|
||||||
swap = await get_submarine_swap(swap_id)
|
swap = await get_submarine_swap(swap_id)
|
||||||
if swap == None:
|
if not swap:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
if swap.status != "pending":
|
if swap.status != "pending":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="swap is not pending."
|
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="swap is not pending."
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
client = create_boltz_client()
|
||||||
await create_refund_tx(swap)
|
await client.refund_swap(
|
||||||
except httpx.RequestError as exc:
|
privkey_wif=swap.refund_privkey,
|
||||||
raise HTTPException(
|
lockup_address=swap.address,
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
receive_address=swap.refund_address,
|
||||||
detail=f"Unreachable: {exc.request.url!r}.",
|
redeem_script_hex=swap.redeem_script,
|
||||||
|
timeout_block_height=swap.timeout_block_height,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
|
|
||||||
|
|
||||||
await update_swap_status(swap.id, "refunded")
|
await update_swap_status(swap.id, "refunded")
|
||||||
return swap
|
return swap
|
||||||
@ -153,37 +129,43 @@ async def api_submarineswap_refund(
|
|||||||
""",
|
""",
|
||||||
response_description="create swap",
|
response_description="create swap",
|
||||||
response_model=SubmarineSwap,
|
response_model=SubmarineSwap,
|
||||||
|
dependencies=[Depends(require_admin_key)],
|
||||||
responses={
|
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"},
|
500: {"description": "boltz error"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def api_submarineswap_create(
|
async def api_submarineswap_create(data: CreateSubmarineSwap):
|
||||||
data: CreateSubmarineSwap,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
auto_swap = await get_auto_reverse_submarine_swap_by_wallet(data.wallet)
|
||||||
):
|
if auto_swap:
|
||||||
try:
|
|
||||||
swap_data = await create_swap(data)
|
|
||||||
except httpx.RequestError as exc:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
|
||||||
detail=f"Unreachable: {exc.request.url!r}.",
|
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))
|
client = create_boltz_client()
|
||||||
except httpx.HTTPStatusError as exc:
|
swap_id = urlsafe_short_hash()
|
||||||
raise HTTPException(
|
payment_hash, payment_request = await create_invoice(
|
||||||
status_code=exc.response.status_code, detail=exc.response.json()["error"]
|
wallet_id=data.wallet,
|
||||||
|
amount=data.amount,
|
||||||
|
memo=f"swap of {data.amount} sats on boltz.exchange",
|
||||||
|
extra={"tag": "boltz", "swap_id": swap_id},
|
||||||
)
|
)
|
||||||
swap = await create_submarine_swap(swap_data)
|
refund_privkey_wif, swap = client.create_swap(payment_request)
|
||||||
return swap.dict()
|
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
|
# REVERSE SWAP
|
||||||
@boltz_ext.get(
|
@boltz_ext.get(
|
||||||
"/api/v1/swap/reverse",
|
"/api/v1/swap/reverse",
|
||||||
name=f"boltz.get /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="""
|
description="""
|
||||||
This endpoint gets a list of reverse swaps.
|
This endpoint gets a list of reverse swaps.
|
||||||
""",
|
""",
|
||||||
@ -192,13 +174,14 @@ async def api_submarineswap_create(
|
|||||||
response_model=List[ReverseSubmarineSwap],
|
response_model=List[ReverseSubmarineSwap],
|
||||||
)
|
)
|
||||||
async def api_reverse_submarineswap(
|
async def api_reverse_submarineswap(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), # type:ignore
|
g: WalletTypeInfo = Depends(get_key_type),
|
||||||
all_wallets: bool = Query(False),
|
all_wallets: bool = Query(False),
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
return [swap.dict() for swap in await get_reverse_submarine_swaps(wallet_ids)]
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
return [swap for swap in await get_reverse_submarine_swaps(wallet_ids)]
|
||||||
|
|
||||||
|
|
||||||
@boltz_ext.post(
|
@boltz_ext.post(
|
||||||
@ -211,6 +194,7 @@ async def api_reverse_submarineswap(
|
|||||||
""",
|
""",
|
||||||
response_description="create reverse swap",
|
response_description="create reverse swap",
|
||||||
response_model=ReverseSubmarineSwap,
|
response_model=ReverseSubmarineSwap,
|
||||||
|
dependencies=[Depends(require_admin_key)],
|
||||||
responses={
|
responses={
|
||||||
405: {"description": "not allowed method, insufficient balance"},
|
405: {"description": "not allowed method, insufficient balance"},
|
||||||
500: {"description": "boltz error"},
|
500: {"description": "boltz error"},
|
||||||
@ -218,30 +202,88 @@ async def api_reverse_submarineswap(
|
|||||||
)
|
)
|
||||||
async def api_reverse_submarineswap_create(
|
async def api_reverse_submarineswap_create(
|
||||||
data: CreateReverseSubmarineSwap,
|
data: CreateReverseSubmarineSwap,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
) -> ReverseSubmarineSwap:
|
||||||
):
|
|
||||||
|
|
||||||
if not await check_balance(data):
|
if not await check_balance(data):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Insufficient balance."
|
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Insufficient balance."
|
||||||
)
|
)
|
||||||
|
client = create_boltz_client()
|
||||||
try:
|
claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
|
||||||
swap_data, task = await create_reverse_swap(data)
|
amount=data.amount
|
||||||
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:
|
new_swap = await create_reverse_submarine_swap(
|
||||||
raise HTTPException(
|
data, claim_privkey_wif, preimage_hex, swap
|
||||||
status_code=exc.response.status_code, detail=exc.response.json()["error"]
|
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
await execute_reverse_swap(client, new_swap)
|
||||||
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
|
return new_swap
|
||||||
|
|
||||||
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(
|
@boltz_ext.post(
|
||||||
@ -252,65 +294,22 @@ async def api_reverse_submarineswap_create(
|
|||||||
This endpoint attempts to get the status of the swap.
|
This endpoint attempts to get the status of the swap.
|
||||||
""",
|
""",
|
||||||
response_description="status of swap json",
|
response_description="status of swap json",
|
||||||
|
dependencies=[Depends(require_admin_key)],
|
||||||
responses={
|
responses={
|
||||||
404: {"description": "when swap_id is not found"},
|
404: {"description": "when swap_id is not found"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def api_swap_status(
|
async def api_swap_status(swap_id: str):
|
||||||
swap_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap(
|
swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap(
|
||||||
swap_id
|
swap_id
|
||||||
)
|
)
|
||||||
if swap == None:
|
if not swap:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
|
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
|
|
||||||
|
|
||||||
|
client = create_boltz_client()
|
||||||
@boltz_ext.post(
|
status = client.swap_status(swap.boltz_id)
|
||||||
"/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)
|
|
||||||
)
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
@ -325,14 +324,5 @@ async def api_check_swaps(
|
|||||||
response_model=dict,
|
response_model=dict,
|
||||||
)
|
)
|
||||||
async def api_boltz_config():
|
async def api_boltz_config():
|
||||||
try:
|
client = create_boltz_client()
|
||||||
res = get_boltz_pairs()
|
return {"minimal": client.limit_minimal, "maximal": client.limit_maximal}
|
||||||
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"]
|
|
||||||
|
@ -224,7 +224,7 @@
|
|||||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<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>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
<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)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
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]]
|
[[package]]
|
||||||
name = "cashu"
|
name = "cashu"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@ -532,10 +550,10 @@ files = [
|
|||||||
cffi = ">=1.12"
|
cffi = ">=1.12"
|
||||||
|
|
||||||
[package.extras]
|
[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)"]
|
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
|
||||||
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
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)"]
|
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"]
|
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-pymssql = ["pymssql"]
|
||||||
mssql-pyodbc = ["pyodbc"]
|
mssql-pyodbc = ["pyodbc"]
|
||||||
mysql = ["mysqlclient"]
|
mysql = ["mysqlclient"]
|
||||||
oracle = ["cx-oracle"]
|
oracle = ["cx_oracle"]
|
||||||
postgresql = ["psycopg2"]
|
postgresql = ["psycopg2"]
|
||||||
postgresql-pg8000 = ["pg8000 (<1.16.6)"]
|
postgresql-pg8000 = ["pg8000 (<1.16.6)"]
|
||||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||||
@ -2094,4 +2112,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
|
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"
|
Cerberus = "^1.3.4"
|
||||||
async-timeout = "^4.0.2"
|
async-timeout = "^4.0.2"
|
||||||
pyln-client = "0.11.1"
|
pyln-client = "0.11.1"
|
||||||
cashu = "0.8.2"
|
cashu = "^0.8.2"
|
||||||
|
boltz-client = "^0.1.2"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
@ -88,8 +89,7 @@ profile = "black"
|
|||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
files = "lnbits"
|
files = "lnbits"
|
||||||
exclude = """(?x)(
|
exclude = """(?x)(
|
||||||
^lnbits/extensions/boltz.
|
^lnbits/wallets/lnd_grpc_files.
|
||||||
| ^lnbits/wallets/lnd_grpc_files.
|
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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
|
import pytest_asyncio
|
||||||
|
|
||||||
from lnbits.core.crud import create_account, create_wallet, get_wallet
|
from lnbits.extensions.boltz.models import CreateReverseSubmarineSwap
|
||||||
from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap
|
|
||||||
from lnbits.extensions.boltz.models import (
|
|
||||||
CreateReverseSubmarineSwap,
|
|
||||||
CreateSubmarineSwap,
|
|
||||||
)
|
|
||||||
from tests.mocks import WALLET
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="session")
|
@pytest_asyncio.fixture(scope="session")
|
||||||
@ -22,4 +11,4 @@ async def reverse_swap(from_wallet):
|
|||||||
onchain_address="bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43",
|
onchain_address="bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43",
|
||||||
amount=20_000,
|
amount=20_000,
|
||||||
)
|
)
|
||||||
return await create_reverse_swap(data)
|
return data
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from tests.helpers import is_fake, is_regtest
|
from tests.helpers import is_fake
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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…
x
Reference in New Issue
Block a user