remove boltz

This commit is contained in:
dni ⚡ 2023-02-15 10:06:21 +01:00
parent 891227b279
commit 7baa248204
No known key found for this signature in database
GPG key ID: 886317704CC4E618
26 changed files with 0 additions and 2584 deletions

View file

@ -1,42 +0,0 @@
# Swap on [Boltz](https://boltz.exchange)
providing **trustless** and **account-free** swap services since **2018.**
move **IN** and **OUT** of the **lightning network** and remain in control of your bitcoin, at all times.
* [Lightning Node](https://amboss.space/node/026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2)
* [Documentation](https://docs.boltz.exchange/en/latest/)
* [Discord](https://discord.gg/d6EK85KK)
* [Twitter](https://twitter.com/Boltzhq)
* [FAQ](https://www.notion.so/Frequently-Asked-Questions-585328ae43944e2eba351050790d5eec) very cool!
# usage
This extension lets you create swaps, reverse swaps and in the case of failure refund your onchain funds.
## create normal swap (Onchain -> Lightning)
1. click on "Swap (IN)" button to open following dialog, select a wallet, choose a proper amount in the min-max range and choose a onchain address to do your refund to if the swap fails after you already commited onchain funds.
---
![create swap](https://imgur.com/OyOh3Nm.png)
---
2. after you confirm your inputs, following dialog with the QR code for the onchain transaction, onchain- address and amount, will pop up.
---
![pay onchain tx](https://imgur.com/r2UhwCY.png)
---
3. after you pay this onchain address with the correct amount, boltz will see it and will pay your invoice and the sats will appear on your wallet.
if anything goes wrong when boltz is trying to pay your invoice, the swap will fail and you will need to refund your onchain funds after the timeout block height hit. (if boltz can pay the invoice, it wont be able to redeem your onchain funds either).
## create reverse swap (Lightning -> Onchain)
1. click on "Swap (OUT)" button to open following dialog, select a wallet, choose a proper amount in the min-max range and choose a onchain address to receive your funds to. Instant settlement: means that LNbits will create the onchain claim transaction if it sees the boltz lockup transaction in the mempool, but it is not confirmed yet. it is advised to leave this checked because it is faster and the longer is takes to settle, the higher the chances are that the lightning invoice expires and the swap fails.
---
![reverse swap](https://imgur.com/UEAPpbs.png)
---
if this swap fails, boltz is doing the onchain refunding, because they have to commit onchain funds.
# refund locked onchain funds from a normal swap (Onchain -> Lightning)
if for some reason the normal swap fails and you already paid onchain, you can easily refund your btc.
this can happen if boltz is not able to pay your lightning invoice after you locked up your funds.
in case that happens, there is a info icon in the Swap (In) List which opens following dialog.
---
![refund](https://imgur.com/pN81ltf.png)
----
if the timeout block height is exceeded you can either press refund and lnbits will do the refunding to the address you specified when creating the swap. Or download the refundfile so you can manually refund your onchain directly on the boltz.exchange website.
if you think there is something wrong and/or you are unsure, you can ask for help either in LNbits telegram or in Boltz [Discord](https://discord.gg/d6EK85KK).
In a recent update we made *automated check*, every 15 minutes, to check if LNbits can refund your failed swap.

View file

@ -1,35 +0,0 @@
import asyncio
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_boltz")
boltz_ext: APIRouter = APIRouter(prefix="/boltz", tags=["boltz"])
def boltz_renderer():
return template_renderer(["lnbits/extensions/boltz/templates"])
boltz_static_files = [
{
"path": "/boltz/static",
"app": StaticFiles(directory="lnbits/extensions/boltz/static"),
"name": "boltz_static",
}
]
from .tasks import check_for_pending_swaps, wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
def boltz_start():
loop = asyncio.get_event_loop()
loop.create_task(check_for_pending_swaps())
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View file

@ -1,6 +0,0 @@
{
"name": "Boltz",
"short_description": "Perform onchain/offchain swaps",
"tile": "/boltz/static/image/boltz.png",
"contributors": ["dni"]
}

View file

@ -1,284 +0,0 @@
import time
from typing import List, Optional, Union
from boltz_client.boltz import BoltzReverseSwapResponse, BoltzSwapResponse
from loguru import logger
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import (
AutoReverseSubmarineSwap,
CreateAutoReverseSubmarineSwap,
CreateReverseSubmarineSwap,
CreateSubmarineSwap,
ReverseSubmarineSwap,
SubmarineSwap,
)
async def get_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}) order by time DESC",
(*wallet_ids,),
)
return [SubmarineSwap(**row) for row in rows]
async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]:
rows = await db.fetchall(
"SELECT * FROM boltz.submarineswap WHERE status='pending' order by time DESC",
)
return [SubmarineSwap(**row) for row in rows]
async def get_submarine_swap(swap_id) -> Optional[SubmarineSwap]:
row = await db.fetchone(
"SELECT * FROM boltz.submarineswap WHERE id = ?", (swap_id,)
)
return SubmarineSwap(**row) if row else None
async def create_submarine_swap(
data: CreateSubmarineSwap,
swap: BoltzSwapResponse,
swap_id: str,
refund_privkey_wif: str,
payment_hash: str,
) -> Optional[SubmarineSwap]:
await db.execute(
"""
INSERT INTO boltz.submarineswap (
id,
wallet,
payment_hash,
status,
boltz_id,
refund_privkey,
refund_address,
expected_amount,
timeout_block_height,
address,
bip21,
redeem_script,
amount
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
swap_id,
data.wallet,
payment_hash,
"pending",
swap.id,
refund_privkey_wif,
data.refund_address,
swap.expectedAmount,
swap.timeoutBlockHeight,
swap.address,
swap.bip21,
swap.redeemScript,
data.amount,
),
)
return await get_submarine_swap(swap_id)
async def get_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}) order by time DESC",
(*wallet_ids,),
)
return [ReverseSubmarineSwap(**row) for row in rows]
async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap]:
rows = await db.fetchall(
"SELECT * FROM boltz.reverse_submarineswap WHERE status='pending' order by time DESC"
)
return [ReverseSubmarineSwap(**row) for row in rows]
async def get_reverse_submarine_swap(swap_id) -> Optional[ReverseSubmarineSwap]:
row = await db.fetchone(
"SELECT * FROM boltz.reverse_submarineswap WHERE id = ?", (swap_id,)
)
return ReverseSubmarineSwap(**row) if row else None
async def create_reverse_submarine_swap(
data: CreateReverseSubmarineSwap,
claim_privkey_wif: str,
preimage_hex: str,
swap: BoltzReverseSwapResponse,
) -> ReverseSubmarineSwap:
swap_id = urlsafe_short_hash()
reverse_swap = ReverseSubmarineSwap(
id=swap_id,
wallet=data.wallet,
status="pending",
boltz_id=swap.id,
instant_settlement=data.instant_settlement,
preimage=preimage_hex,
claim_privkey=claim_privkey_wif,
lockup_address=swap.lockupAddress,
invoice=swap.invoice,
onchain_amount=swap.onchainAmount,
onchain_address=data.onchain_address,
timeout_block_height=swap.timeoutBlockHeight,
redeem_script=swap.redeemScript,
amount=data.amount,
time=int(time.time()),
)
await db.execute(
"""
INSERT INTO boltz.reverse_submarineswap (
id,
wallet,
status,
boltz_id,
instant_settlement,
preimage,
claim_privkey,
lockup_address,
invoice,
onchain_amount,
onchain_address,
timeout_block_height,
redeem_script,
amount
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
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.onchain_address,
swap.instant_settlement,
swap.balance,
swap.amount,
),
)
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):
swap = await get_submarine_swap(swap_id)
if swap:
await db.execute(
"UPDATE boltz.submarineswap SET status='"
+ status
+ "' WHERE id='"
+ swap.id
+ "'"
)
logger.info(
f"Boltz - swap status change: {status}. boltz_id: {swap.boltz_id}, wallet: {swap.wallet}"
)
return swap
reverse_swap = await get_reverse_submarine_swap(swap_id)
if reverse_swap:
await db.execute(
"UPDATE boltz.reverse_submarineswap SET status='"
+ status
+ "' WHERE id='"
+ 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
return None

View file

@ -1,64 +0,0 @@
async def m001_initial(db):
await db.execute(
f"""
CREATE TABLE boltz.submarineswap (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
payment_hash TEXT NOT NULL,
amount {db.big_int} NOT NULL,
status TEXT NOT NULL,
boltz_id TEXT NOT NULL,
refund_address TEXT NOT NULL,
refund_privkey TEXT NOT NULL,
expected_amount {db.big_int} NOT NULL,
timeout_block_height INT NOT NULL,
address TEXT NOT NULL,
bip21 TEXT NOT NULL,
redeem_script TEXT NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
await db.execute(
f"""
CREATE TABLE boltz.reverse_submarineswap (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
onchain_address TEXT NOT NULL,
amount {db.big_int} NOT NULL,
instant_settlement BOOLEAN NOT NULL,
status TEXT NOT NULL,
boltz_id TEXT NOT NULL,
timeout_block_height INT NOT NULL,
redeem_script TEXT NOT NULL,
preimage TEXT NOT NULL,
claim_privkey TEXT NOT NULL,
lockup_address TEXT NOT NULL,
invoice TEXT NOT NULL,
onchain_amount {db.big_int} NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
async def m002_auto_swaps(db):
await db.execute(
"""
CREATE TABLE boltz.auto_reverse_submarineswap (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
onchain_address TEXT NOT NULL,
amount INT NOT NULL,
balance INT NOT NULL,
instant_settlement BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)

View file

@ -1,68 +0,0 @@
from fastapi import Query
from pydantic import BaseModel
class SubmarineSwap(BaseModel):
id: str
wallet: str
amount: int
payment_hash: str
time: int
status: str
refund_privkey: str
refund_address: str
boltz_id: str
expected_amount: int
timeout_block_height: int
address: str
bip21: str
redeem_script: str
class CreateSubmarineSwap(BaseModel):
wallet: str = Query(...)
refund_address: str = Query(...)
amount: int = Query(...)
class ReverseSubmarineSwap(BaseModel):
id: str
wallet: str
amount: int
onchain_address: str
instant_settlement: bool
time: int
status: str
boltz_id: str
preimage: str
claim_privkey: str
lockup_address: str
invoice: str
onchain_amount: int
timeout_block_height: int
redeem_script: str
class CreateReverseSubmarineSwap(BaseModel):
wallet: str = Query(...)
amount: int = Query(...)
instant_settlement: bool = Query(...)
onchain_address: str = Query(...)
class AutoReverseSubmarineSwap(BaseModel):
id: str
wallet: str
amount: int
balance: int
onchain_address: str
instant_settlement: bool
time: int
class CreateAutoReverseSubmarineSwap(BaseModel):
wallet: str = Query(...)
amount: int = Query(...)
balance: int = Query(0)
instant_settlement: bool = Query(...)
onchain_address: str = Query(...)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View file

@ -1,180 +0,0 @@
import asyncio
from boltz_client.boltz import BoltzNotFoundException, BoltzSwapStatusException
from boltz_client.mempool import MempoolBlockHeightException
from loguru import logger
from lnbits.core.crud import get_wallet
from lnbits.core.models import Payment
from lnbits.core.services import check_transaction_status, fee_reserve
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import (
create_reverse_submarine_swap,
get_all_pending_reverse_submarine_swaps,
get_all_pending_submarine_swaps,
get_auto_reverse_submarine_swap_by_wallet,
get_submarine_swap,
update_swap_status,
)
from .models import CreateReverseSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap
from .utils import create_boltz_client, execute_reverse_swap
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name())
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
await check_for_auto_swap(payment)
if payment.extra.get("tag") != "boltz":
# not a boltz invoice
return
await payment.set_pending(False)
if payment.extra:
swap_id = payment.extra.get("swap_id")
if swap_id:
swap = await get_submarine_swap(swap_id)
if swap:
await update_swap_status(swap_id, "complete")
async def check_for_auto_swap(payment: Payment) -> None:
auto_swap = await get_auto_reverse_submarine_swap_by_wallet(payment.wallet_id)
if auto_swap:
wallet = await get_wallet(payment.wallet_id)
if wallet:
reserve = fee_reserve(wallet.balance_msat) / 1000
balance = wallet.balance_msat / 1000
amount = balance - auto_swap.balance - reserve
if amount >= auto_swap.amount:
client = create_boltz_client()
claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
amount=int(amount)
)
new_swap = await create_reverse_submarine_swap(
CreateReverseSubmarineSwap(
wallet=auto_swap.wallet,
amount=int(amount),
instant_settlement=auto_swap.instant_settlement,
onchain_address=auto_swap.onchain_address,
),
claim_privkey_wif,
preimage_hex,
swap,
)
await execute_reverse_swap(client, new_swap)
logger.info(
f"Boltz: auto reverse swap created with amount: {amount}, boltz_id: {new_swap.boltz_id}"
)
"""
testcases for boltz startup
A. normal swaps
1. test: create -> kill -> start -> startup invoice listeners -> pay onchain funds -> should complete
2. test: create -> kill -> pay onchain funds -> mine block -> start -> startup check -> should complete
3. test: create -> kill -> mine blocks and hit timeout -> start -> should go timeout/failed
4. test: create -> kill -> pay to less onchain funds -> mine blocks hit timeout -> start lnbits -> should be refunded
B. reverse swaps
1. test: create instant -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should claim/complete
2. test: create -> kill -> boltz does lockup -> not confirmed -> start lnbits -> mine blocks -> should claim/complete
3. test: create -> kill -> boltz does lockup -> confirmed -> start lnbits -> should claim/complete
"""
async def check_for_pending_swaps():
try:
swaps = await get_all_pending_submarine_swaps()
reverse_swaps = await get_all_pending_reverse_submarine_swaps()
if len(swaps) > 0 or len(reverse_swaps) > 0:
logger.debug("Boltz - startup swap check")
except:
logger.error(
"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:
logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
await update_swap_status(swap.id, "failed")
except MempoolBlockHeightException:
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:
logger.debug(f"Boltz - reverse swap: {reverse_swap.boltz_id} does not exist.")
await update_swap_status(reverse_swap.id, "failed")
except Exception as exc:
logger.error(
f"Boltz - unhandled exception, reverse swap: {reverse_swap.id} - {str(exc)}"
)

View file

@ -1,109 +0,0 @@
<q-card>
<q-card-section>
<img src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg" alt="" />
<img
src="https://boltz.exchange/static/media/Boltz.02fb7acb.svg"
style="padding: 5px 9px"
alt=""
/>
<p><b>NON CUSTODIAL atomic swap service</b></p>
<h5 class="text-subtitle1 q-my-none">
Providing trustless and account-free swap services since 2018. Move IN and
OUT of the lightning network and remain in control of your bitcoin, at all
time.
</h5>
<p>
Link:
<a target="_blank" href="https://boltz.exchange"
>https://boltz.exchange
</a>
<br />
README:
<a
target="_blank"
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
>read more</a
>
</p>
<p>
<small
>Extension created by,
<a target="_blank" href="https://github.com/dni">dni</a></small
>
</p>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<h3 class="text-subtitle1 q-my-none">
<b>Fee Information</b>
</h3>
<span>
{% raw %} Every swap consists of 2 onchain transactions, lockup and claim
/ refund, routing fees and a Boltz fee of
<b>{{ boltzConfig.fee_percentage }}%</b>. {% endraw %}
</span>
</q-card-section>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Fee example: Lightning -> Onchain"
:content-inset-level="0.5"
>
<q-card-section>
{% raw %} You want to swap out {{ boltzExample.amount }} sats, Lightning
to Onchain:
<ul style="padding-left: 12px">
<li>Onchain lockup tx fee: ~{{ boltzExample.onchain_boltz }} sats</li>
<li>
Onchain claim tx fee: {{ boltzExample.onchain_lnbits }} sats
(hardcoded)
</li>
<li>Routing fees (paid by you): unknown</li>
<li>
Boltz fees: {{ boltzExample.boltz_fee }} sats ({{
boltzConfig.fee_percentage }}%)
</li>
<li>
Fees total: {{ boltzExample.reverse_fee_total }} sats + routing fees
</li>
<li>You receive: {{ boltzExample.reverse_receive }} sats</li>
</ul>
<p>
onchain_amount_received = amount - (amount * boltz_fee / 100) -
lockup_fee - claim_fee
</p>
{% endraw %}
</q-card-section>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Fee example: Onchain -> Lightning"
:content-inset-level="0.5"
>
<q-card-section>
{% raw %} You want to swap in {{ boltzExample.amount }} sats, Onchain to
Lightning:
<ul style="padding-left: 12px">
<li>Onchain lockup tx fee: whatever you choose when paying</li>
<li>Onchain claim tx fee: ~{{ boltzExample.onchain_boltz }} sats</li>
<li>Routing fees (paid by boltz): unknown</li>
<li>
Boltz fees: {{ boltzExample.boltz_fee }} sats ({{
boltzConfig.fee_percentage }}%)
</li>
<li>
Fees total: {{ boltzExample.normal_fee_total }} sats + lockup_fee
</li>
<li>
You pay onchain: {{ boltzExample.normal_expected_amount }} sats +
lockup_fee
</li>
<li>You receive lightning: {{ boltzExample.amount }} sats</li>
</ul>
<p>onchain_payment = amount + (amount * boltz_fee / 100) + claim_fee</p>
{% endraw %}
</q-card-section>
</q-expansion-item>
</q-card>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,621 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 q-gutter-y-md">
{% include "boltz/_buttons.html" %} {% include
"boltz/_submarineSwapList.html" %} {% include
"boltz/_reverseSubmarineSwapList.html" %} {% include
"boltz/_autoReverseSwapList.html" %}
</div>
<div class="col-12 col-md-4 q-gutter-y-md">
{% include "boltz/_api_docs.html" %}
</div>
{% include "boltz/_submarineSwapDialog.html" %} {% include
"boltz/_reverseSubmarineSwapDialog.html" %} {% include
"boltz/_autoReverseSwapDialog.html" %} {% include "boltz/_qrDialog.html" %} {%
include "boltz/_statusDialog.html" %}
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
mempool: '',
boltzConfig: {},
submarineSwaps: [],
reverseSubmarineSwaps: [],
autoReverseSubmarineSwaps: [],
statuses: [],
submarineSwapDialog: {
show: false,
data: {}
},
reverseSubmarineSwapDialog: {
show: false,
data: {
instant_settlement: true
}
},
autoReverseSubmarineSwapDialog: {
show: false,
data: {
balance: 100,
instant_settlement: true
}
},
qrCodeDialog: {
show: false,
data: {}
},
statusDialog: {
show: false,
data: {}
},
allStatusTable: {
columns: [
{
name: 'swap_id',
align: 'left',
label: 'Swap ID',
field: 'swap_id'
},
{
name: 'status',
align: 'left',
label: 'Status',
field: 'message'
},
{
name: 'boltz',
align: 'left',
label: 'Boltz',
field: 'boltz'
},
{
name: 'mempool',
align: 'left',
label: 'Mempool',
field: 'mempool'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
pagination: {
rowsPerPage: 10
}
},
autoReverseSubmarineSwapTable: {
columns: [
{
name: 'time',
align: 'left',
label: 'Time',
field: 'time',
sortable: true,
format: function (val, row) {
return new Date(val * 1000).toUTCString()
}
},
{
name: 'wallet',
align: 'left',
label: 'Wallet',
field: data => {
let wallet = _.findWhere(this.g.user.wallets, {
id: data.wallet
})
if (wallet) {
return wallet.name
}
}
},
{
name: 'balance',
align: 'left',
label: 'Balance',
field: 'balance'
},
{
name: 'amount',
align: 'left',
label: 'Amount',
field: 'amount'
},
{
name: 'onchain_address',
align: 'left',
label: 'Onchain address',
field: 'onchain_address'
}
],
pagination: {
rowsPerPage: 10
}
},
reverseSubmarineSwapTable: {
columns: [
{
name: 'time',
align: 'left',
label: 'Time',
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: 'status',
align: 'left',
label: 'Status',
field: 'status'
},
{
name: 'boltz_id',
align: 'left',
label: 'Boltz ID',
field: 'boltz_id'
},
{
name: 'onchain_amount',
align: 'left',
label: 'Onchain amount',
field: 'onchain_amount'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
pagination: {
rowsPerPage: 10
}
},
submarineSwapTable: {
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: 'status',
align: 'left',
label: 'Status',
field: 'status'
},
{
name: 'boltz_id',
align: 'left',
label: 'Boltz ID',
field: 'boltz_id'
},
{
name: 'expected_amount',
align: 'left',
label: 'Expected amount',
field: 'expected_amount'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
pagination: {
rowsPerPage: 10
}
}
}
},
computed: {
boltzExample() {
let amount = 100000
let onchain_lnbits = 1000
let onchain_boltz = 500
let boltz_fee = (amount * this.boltzConfig.fee_percentage) / 100
let normal_fee_total = onchain_boltz + boltz_fee
let reverse_fee_total = onchain_boltz + boltz_fee + onchain_lnbits
return {
amount: amount,
boltz_fee: boltz_fee,
reverse_fee_total: reverse_fee_total,
reverse_receive: amount - reverse_fee_total,
onchain_lnbits: onchain_lnbits,
onchain_boltz: onchain_boltz,
normal_fee_total: normal_fee_total,
normal_expected_amount: amount + normal_fee_total
}
}
},
methods: {
getLimits() {
if (this.boltzConfig) {
return {
min: this.boltzConfig.minimal,
max: this.boltzConfig.maximal
}
}
return {
min: 0,
max: 0
}
},
amountLabel() {
let limits = this.getLimits()
return 'min: (' + limits.min + '), max: (' + limits.max + ')'
},
disableSubmarineSwapDialog() {
const data = this.submarineSwapDialog.data
let limits = this.getLimits()
return (
data.wallet == null ||
data.refund_address == null ||
data.refund_address.search(
/^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/
) !== 0 ||
data.amount < limits.min ||
data.amount > limits.max
)
},
disableReverseSubmarineSwapDialog() {
const data = this.reverseSubmarineSwapDialog.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
)
},
disableAutoReverseSubmarineSwapDialog() {
const data = this.autoReverseSubmarineSwapDialog.data
let limits = this.getLimits()
return (
data.onchain_address == null ||
data.onchain_address.search(
/^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/
) !== 0 ||
data.wallet == null ||
data.amount < limits.min ||
data.amount > limits.max
)
},
downloadRefundFile(swapId) {
let swap = _.findWhere(this.submarineSwaps, {id: swapId})
let json = {
id: swap.boltz_id,
currency: 'BTC',
redeemScript: swap.redeem_script,
privateKey: swap.refund_privkey,
timeoutBlockHeight: swap.timeout_block_height
}
let hiddenElement = document.createElement('a')
hiddenElement.href =
'data:application/json;charset=utf-8,' +
encodeURI(JSON.stringify(json))
hiddenElement.target = '_blank'
hiddenElement.download = 'boltz-refund-' + swap.boltz_id + '.json'
hiddenElement.click()
},
refundSwap(swapId) {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap/refund?swap_id=' + swapId,
this.g.user.wallets[0].adminkey
)
.then(res => {
this.resetStatusDialog()
})
.catch(error => {
console.log('error', error)
LNbits.utils.notifyApiError(error)
})
},
openMempool(swap_id) {
var swap = _.findWhere(this.submarineSwaps, {id: swap_id})
if (swap === undefined) {
var swap = _.findWhere(this.reverseSubmarineSwaps, {id: swap_id})
var address = swap.lockup_address
} else {
var address = swap.address
}
var mempool_address = this.mempool
// used for development, replace docker hosts with localhost
if (mempool_address.search('mempool-web') !== -1) {
mempool_address = mempool_address.replace('mempool-web', 'localhost')
}
window.open(mempool_address + '/address/' + address, '_blank')
},
openStatusDialog(swap_id, reverse) {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap/status?swap_id=' + swap_id,
this.g.user.wallets[0].adminkey
)
.then(res => {
this.resetStatusDialog()
this.statusDialog.data = {
reverse: reverse,
swap_id: swap_id,
wallet: res.data.wallet,
boltz: res.data.boltz,
status: res.data.status,
mempool: res.data.mempool,
timeout_block_height: res.data.timeout_block_height,
date: new Date().toUTCString()
}
this.statusDialog.show = true
})
.catch(error => {
console.log('error', error)
LNbits.utils.notifyApiError(error)
})
},
openQrCodeDialog(submarineSwapId) {
var swap = _.findWhere(this.submarineSwaps, {id: submarineSwapId})
if (swap === undefined) {
return console.assert('swap is undefined, this should not happen')
}
this.qrCodeDialog.data = {
id: swap.id,
expected_amount: swap.expected_amount,
expected_amount_btc: swap.expected_amount / 100000000,
bip21: swap.bip21,
address: swap.address
}
this.qrCodeDialog.show = true
},
resetStatusDialog() {
this.statusDialog = {
show: false,
data: {}
}
},
resetSubmarineSwapDialog() {
this.submarineSwapDialog = {
show: false,
data: {}
}
},
resetReverseSubmarineSwapDialog() {
this.reverseSubmarineSwapDialog = {
show: false,
data: {}
}
},
resetAutoReverseSubmarineSwapDialog() {
this.autoReverseSubmarineSwapDialog = {
show: false,
data: {}
}
},
sendReverseSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.reverseSubmarineSwapDialog.data.wallet
})
let data = this.reverseSubmarineSwapDialog.data
this.createReverseSubmarineSwap(wallet, data)
},
sendAutoReverseSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.autoReverseSubmarineSwapDialog.data.wallet
})
let data = this.autoReverseSubmarineSwapDialog.data
this.createAutoReverseSubmarineSwap(wallet, data)
},
sendSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.submarineSwapDialog.data.wallet
})
let data = this.submarineSwapDialog.data
this.createSubmarineSwap(wallet, data)
},
exportSubmarineSwapCSV() {
LNbits.utils.exportCSV(
this.submarineSwapTable.columns,
this.submarineSwaps
)
},
exportReverseSubmarineSwapCSV() {
LNbits.utils.exportCSV(
this.reverseSubmarineSwapTable.columns,
this.reverseSubmarineSwaps
)
},
exportAutoReverseSubmarineSwapCSV() {
LNbits.utils.exportCSV(
this.autoReverseSubmarineSwapTable.columns,
this.autoReverseSubmarineSwaps
)
},
createSubmarineSwap(wallet, data) {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap',
this.g.user.wallets[0].adminkey,
data
)
.then(res => {
this.submarineSwaps.unshift(res.data)
this.resetSubmarineSwapDialog()
this.openQrCodeDialog(res.data.id)
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
createReverseSubmarineSwap(wallet, data) {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap/reverse',
this.g.user.wallets[0].adminkey,
data
)
.then(res => {
this.reverseSubmarineSwaps.unshift(res.data)
this.resetReverseSubmarineSwapDialog()
})
.catch(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() {
LNbits.api
.request(
'GET',
'/boltz/api/v1/swap?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.submarineSwaps = response.data
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getReverseSubmarineSwap() {
LNbits.api
.request(
'GET',
'/boltz/api/v1/swap/reverse?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.reverseSubmarineSwaps = response.data
})
.catch(function (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() {
LNbits.api
.request('GET', '/boltz/api/v1/swap/mempool')
.then(res => {
this.mempool = res.data
})
.catch(error => {
console.log('error', error)
LNbits.utils.notifyApiError(error)
})
},
getBoltzConfig() {
LNbits.api
.request('GET', '/boltz/api/v1/swap/boltz')
.then(res => {
this.boltzConfig = res.data
})
.catch(error => {
console.log('error', error)
LNbits.utils.notifyApiError(error)
})
}
},
created: function () {
this.getMempool()
this.getBoltzConfig()
this.getSubmarineSwap()
this.getReverseSubmarineSwap()
this.getAutoReverseSubmarineSwap()
}
})
</script>
{% endblock %}

View file

@ -1,87 +0,0 @@
import asyncio
import calendar
import datetime
from typing import Awaitable
from boltz_client.boltz import BoltzClient, BoltzConfig
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:
# check if we can pay the invoice before we create the actual swap on boltz
amount_msat = data.amount * 1000
fee_reserve_msat = fee_reserve(amount_msat)
wallet = await get_wallet(data.wallet)
assert wallet
if wallet.balance_msat - fee_reserve_msat < amount_msat:
return False
return True
def get_timestamp():
date = datetime.datetime.utcnow()
return calendar.timegm(date.utctimetuple())
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:
awaited = await awaitable
await update_swap_status(swap_id, "complete")
return awaited
except asyncio.exceptions.CancelledError:
"""lnbits process was exited, do nothing and handle it in startup script"""
except:
wstask.cancel()
await update_swap_status(swap_id, "failed")
return asyncio.create_task(_pay_invoice(awaitable))

View file

@ -1,21 +0,0 @@
from urllib.parse import urlparse
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import boltz_ext, boltz_renderer
templates = Jinja2Templates(directory="templates")
@boltz_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
root_url = urlparse(str(request.url)).netloc
return boltz_renderer().TemplateResponse(
"boltz/index.html",
{"request": request, "user": user.dict(), "root_url": root_url},
)

View file

@ -1,332 +0,0 @@
from http import HTTPStatus
from typing import List
from fastapi import Depends, Query, status
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import settings
from . import boltz_ext
from .crud import (
create_auto_reverse_submarine_swap,
create_reverse_submarine_swap,
create_submarine_swap,
delete_auto_reverse_submarine_swap,
get_auto_reverse_submarine_swap_by_wallet,
get_auto_reverse_submarine_swaps,
get_reverse_submarine_swap,
get_reverse_submarine_swaps,
get_submarine_swap,
get_submarine_swaps,
update_swap_status,
)
from .models import (
AutoReverseSubmarineSwap,
CreateAutoReverseSubmarineSwap,
CreateReverseSubmarineSwap,
CreateSubmarineSwap,
ReverseSubmarineSwap,
SubmarineSwap,
)
from .utils import check_balance, create_boltz_client, execute_reverse_swap
@boltz_ext.get(
"/api/v1/swap/mempool",
name="boltz.get /swap/mempool",
summary="get a the mempool url",
description="""
This endpoint gets the URL from mempool.space
""",
response_description="mempool.space url",
response_model=str,
)
async def api_mempool_url():
return settings.boltz_mempool_space_url
# NORMAL SWAP
@boltz_ext.get(
"/api/v1/swap",
name="boltz.get /swap",
summary="get a list of swaps a swap",
description="""
This endpoint gets a list of normal swaps.
""",
response_description="list of normal swaps",
dependencies=[Depends(get_key_type)],
response_model=List[SubmarineSwap],
)
async def api_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_submarine_swaps(wallet_ids)]
@boltz_ext.post(
"/api/v1/swap/refund",
name="boltz.swap_refund",
summary="refund of a swap",
description="""
This endpoint attempts to refund a normal swaps, creates onchain tx and sets swap status ro refunded.
""",
response_description="refunded swap with status set to refunded",
dependencies=[Depends(require_admin_key)],
response_model=SubmarineSwap,
responses={
400: {"description": "when swap_id is missing"},
404: {"description": "when swap is not found"},
405: {"description": "when swap is not pending"},
500: {
"description": "when something goes wrong creating the refund onchain tx"
},
},
)
async def api_submarineswap_refund(swap_id: str):
if not swap_id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="swap_id missing"
)
swap = await get_submarine_swap(swap_id)
if not swap:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
)
if swap.status != "pending":
raise HTTPException(
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="swap is not pending."
)
client = create_boltz_client()
await client.refund_swap(
privkey_wif=swap.refund_privkey,
lockup_address=swap.address,
receive_address=swap.refund_address,
redeem_script_hex=swap.redeem_script,
timeout_block_height=swap.timeout_block_height,
)
await update_swap_status(swap.id, "refunded")
return swap
@boltz_ext.post(
"/api/v1/swap",
status_code=status.HTTP_201_CREATED,
name="boltz.post /swap",
summary="create a submarine swap",
description="""
This endpoint creates a submarine swap
""",
response_description="create swap",
response_model=SubmarineSwap,
dependencies=[Depends(require_admin_key)],
responses={
405: {
"description": "auto reverse swap is active, a swap would immediatly be swapped out again."
},
500: {"description": "boltz error"},
},
)
async def api_submarineswap_create(data: CreateSubmarineSwap):
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, a swap would immediatly be swapped out again.",
)
client = create_boltz_client()
swap_id = urlsafe_short_hash()
payment_hash, payment_request = await create_invoice(
wallet_id=data.wallet,
amount=data.amount,
memo=f"swap of {data.amount} sats on boltz.exchange",
extra={"tag": "boltz", "swap_id": swap_id},
)
refund_privkey_wif, swap = client.create_swap(payment_request)
new_swap = await create_submarine_swap(
data, swap, swap_id, refund_privkey_wif, payment_hash
)
return new_swap.dict() if new_swap else None
# REVERSE SWAP
@boltz_ext.get(
"/api/v1/swap/reverse",
name="boltz.get /swap/reverse",
summary="get a list of reverse swaps",
description="""
This endpoint gets a list of reverse swaps.
""",
response_description="list of reverse swaps",
dependencies=[Depends(get_key_type)],
response_model=List[ReverseSubmarineSwap],
)
async def api_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 for swap in await get_reverse_submarine_swaps(wallet_ids)]
@boltz_ext.post(
"/api/v1/swap/reverse",
status_code=status.HTTP_201_CREATED,
name="boltz.post /swap/reverse",
summary="create a reverse submarine swap",
description="""
This endpoint creates a reverse submarine swap
""",
response_description="create reverse swap",
response_model=ReverseSubmarineSwap,
dependencies=[Depends(require_admin_key)],
responses={
405: {"description": "not allowed method, insufficient balance"},
500: {"description": "boltz error"},
},
)
async def api_reverse_submarineswap_create(
data: CreateReverseSubmarineSwap,
) -> ReverseSubmarineSwap:
if not await check_balance(data):
raise HTTPException(
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Insufficient balance."
)
client = create_boltz_client()
claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
amount=data.amount
)
new_swap = await create_reverse_submarine_swap(
data, claim_privkey_wif, preimage_hex, swap
)
await execute_reverse_swap(client, new_swap)
return new_swap
@boltz_ext.get(
"/api/v1/swap/reverse/auto",
name="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="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="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(
"/api/v1/swap/status",
name="boltz.swap_status",
summary="shows the status of a swap",
description="""
This endpoint attempts to get the status of the swap.
""",
response_description="status of swap json",
dependencies=[Depends(require_admin_key)],
responses={
404: {"description": "when swap_id is not found"},
},
)
async def api_swap_status(swap_id: str):
swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap(
swap_id
)
if not swap:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
)
client = create_boltz_client()
status = client.swap_status(swap.boltz_id)
return status
@boltz_ext.get(
"/api/v1/swap/boltz",
name="boltz.get /swap/boltz",
summary="get a boltz configuration",
description="""
This endpoint gets configuration for boltz. (limits, fees...)
""",
response_description="dict of boltz config",
response_model=dict,
)
async def api_boltz_config():
client = create_boltz_client()
return {
"minimal": client.limit_minimal,
"maximal": client.limit_maximal,
"fee_percentage": client.fee_percentage,
}

View file

@ -1,14 +0,0 @@
import pytest_asyncio
from lnbits.extensions.boltz.models import CreateReverseSubmarineSwap
@pytest_asyncio.fixture(scope="session")
async def reverse_swap(from_wallet):
data = CreateReverseSubmarineSwap(
wallet=from_wallet.id,
instant_settlement=True,
onchain_address="bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43",
amount=20_000,
)
return data

View file

@ -1,102 +0,0 @@
import pytest
from tests.helpers import is_fake
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
async def test_mempool_url(client):
response = await client.get("/boltz/api/v1/swap/mempool")
assert response.status_code == 200
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
async def test_boltz_config(client):
response = await client.get("/boltz/api/v1/swap/boltz")
assert response.status_code == 200
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
async def test_endpoints_unauthenticated(client):
response = await client.get("/boltz/api/v1/swap?all_wallets=true")
assert response.status_code == 401
response = await client.get("/boltz/api/v1/swap/reverse?all_wallets=true")
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap")
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap/reverse")
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap/status")
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap/check")
assert response.status_code == 401
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
async def test_endpoints_inkey(client, inkey_headers_to):
response = await client.get(
"/boltz/api/v1/swap?all_wallets=true", headers=inkey_headers_to
)
assert response.status_code == 200
response = await client.get(
"/boltz/api/v1/swap/reverse?all_wallets=true", headers=inkey_headers_to
)
assert response.status_code == 200
response = await client.post("/boltz/api/v1/swap", headers=inkey_headers_to)
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap/reverse", headers=inkey_headers_to)
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap/refund", headers=inkey_headers_to)
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap/status", headers=inkey_headers_to)
assert response.status_code == 401
response = await client.post("/boltz/api/v1/swap/check", headers=inkey_headers_to)
assert response.status_code == 401
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
async def test_endpoints_adminkey_badrequest(client, adminkey_headers_to):
response = await client.post("/boltz/api/v1/swap", headers=adminkey_headers_to)
assert response.status_code == 400
response = await client.post(
"/boltz/api/v1/swap/reverse", headers=adminkey_headers_to
)
assert response.status_code == 400
response = await client.post(
"/boltz/api/v1/swap/refund", headers=adminkey_headers_to
)
assert response.status_code == 400
response = await client.post(
"/boltz/api/v1/swap/status", headers=adminkey_headers_to
)
assert response.status_code == 400
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
async def test_endpoints_adminkey_regtest(client, from_wallet, adminkey_headers_to):
swap = {
"wallet": from_wallet.id,
"refund_address": "bcrt1q3cwq33y435h52gq3qqsdtczh38ltlnf69zvypm",
"amount": 50_000,
}
response = await client.post(
"/boltz/api/v1/swap", json=swap, headers=adminkey_headers_to
)
assert response.status_code == 201
reverse_swap = {
"wallet": from_wallet.id,
"instant_settlement": True,
"onchain_address": "bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43",
"amount": 50_000,
}
response = await client.post(
"/boltz/api/v1/swap/reverse", json=reverse_swap, headers=adminkey_headers_to
)
assert response.status_code == 201