Merge branch 'main' into extension_install_02

This commit is contained in:
Vlad Stan 2023-01-25 09:50:54 +02:00 committed by GitHub
commit 9cca87f738
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 2637 additions and 1993 deletions

View File

@ -1,7 +1,9 @@
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
HOST=127.0.0.1
PORT=5000
# uvicorn variable, allow https behind a proxy
# uvicorn variable, uncomment to allow https behind a proxy
# FORWARDED_ALLOW_IPS="*"
DEBUG=false

View File

@ -24,7 +24,9 @@ jobs:
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install packages
run: poetry install
run: |
poetry config virtualenvs.create false
poetry install
- name: Check black
run: make checkblack
- name: Check isort

View File

@ -36,6 +36,7 @@ jobs:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies
run: |
poetry config virtualenvs.create false
poetry install
sudo apt install unzip
- name: Run migrations

View File

@ -21,6 +21,7 @@ jobs:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies
run: |
poetry config virtualenvs.create false
poetry install
- name: Run tests
run: poetry run mypy

View File

@ -29,6 +29,7 @@ jobs:
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry config virtualenvs.create false
poetry install
- name: Run tests
env:
@ -72,6 +73,7 @@ jobs:
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry config virtualenvs.create false
poetry install
- name: Run tests
env:
@ -116,6 +118,7 @@ jobs:
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry config virtualenvs.create false
poetry install
- name: Run tests
env:

View File

@ -44,6 +44,7 @@ jobs:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies
run: |
poetry config virtualenvs.create false
poetry install
- name: Run tests
run: make test
@ -80,6 +81,7 @@ jobs:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies
run: |
poetry config virtualenvs.create false
poetry install
- name: Run tests
env:

View File

@ -7,6 +7,6 @@
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"jsxBracketSameLine": false,
"bracketSameLine": false,
"bracketSpacing": false
}

View File

@ -9,6 +9,9 @@ check: mypy checkprettier checkisort checkblack
prettier: $(shell find lnbits -name "*.js" -o -name ".html")
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
pyright:
./node_modules/.bin/pyright
black:
poetry run black .

268
docs/guide/faq.md Normal file
View File

@ -0,0 +1,268 @@
---
layout: default
title: FAQ
nav_order: 5
---
# FAQ - Frequently Asked Questions
## Install options
<ul><p>LNbits is not a node management software but a ⚡LN only accounting system on top of a funding source.</p>
<details><summary>Funding my LNbits wallet from my node it doesn't work.</summary>
<p>If you want to send sats from the same node that is the funding source of your LNbits, you will need to edit the lnd.conf file. The parameters to be included are:</p>
```
allow-circular-route=1
allow-self-payment=1
```
</details>
<details><summary>Funding source only available via tor (e.g. Umbrel)</summary>
<p>If you want your setup to stay behind tor then only apps, pos and wallets that have tor activated can communicate with your wallets. Most likely you will have trouble when people try to redeem your voucher through onion or when importing your lnbits wallets into a wallet-app that doesnt support tor. If you plan to let LNbits wallets interact with plain internet shops and services you should consider <a href="https://github.com/TrezorHannes/Dual-LND-Hybrid-VPS">setting up hybrid mode for your node</a>.</p>
</details>
<details><summary>Funding source is in a cloud</summary>
<p>This means that you might not have access to some files which would allow certain administrative functions. E.g. on <a href="https://voltage.cloud/">Voltage</a> lnd.conf can not be edited. Payments from your node to LNbits wallets can therefore not be configurated in this case atm so you will need to take an extra wallet to send from funding source->wallet x->LNbits wallet (only) for the initial funding of the wallet.</p>
</details>
<details><summary>LNbits via clearnet domain</summary>
<p><a href="https://github.com/TrezorHannes/Dual-LND-Hybrid-VPS">Step by step guide how to convert your Tor only node</a> into a clearnet node to make apps like LNbits accessible via https.</p>
</details>
<details><summary>Which funding sources can I use for LNbits?</summary>
<p>There are several ways to run a LNbits instance funded from different sources. It is importan to choose a source that has a good liquidity and good peers connected. If you use LNbits for public services your users´ payments can then flow happily in both directions. If you would like to fund your LNbits wallet via btc please see section Troubleshooting.</p>
<p>The <a href="http://docs.lnbits.org/guide/wallets.html">LNbits manual</a> shows you which sources can be used and how to configure each: CLN, LND, LNPay, Cliche, OpenNode as well as bots.</p>
</details>
<!--Later to be added
<details><summary>Advanced setup options</summary>
<p>more text coming soon...</p>
</details>
-->
<details><summary>Can I prevent others from generating wallets on my node?</summary>
<p>When you run your LNbits in clearnet basically everyone can generate a wallet on it. Since the funds of your node are bound to these wallets you might want to prevent that. There are two ways to do so:</p>
<ul>
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/lnbits/blob/main/.env.example">in the .env file</a></li>
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/usermanager">via the Usermanager-Extension</a>. You can find <a href="http://docs.lnbits.org/guide/admin_ui.html">more info about the superuser and the admin extension here</a></li>
</ul>
<p>Please note that all entries in the .env file will not be the taken into account once you activated the admin extension.</p>
</details>
</ul>
## Troubleshooting
<ul><details><summary>Message "https error" or network error" when scanning a LNbits QR</summary>
<p>Bad news, this is a routing error that might have quite a lot of reasons. Let´s try a few of the most possible problems and their solutions. First choose your setup</p>
<ul>
<li>
<details><summary>LNbits is running via Tor only, you can't open it on a public domain like lnbits.yourdomain.com</summary>
<ul>
<li>Given that you want your setup to stay like this open your LNbits wallet using the .onion URI and create it again. In this way the QR is generated to be accessible via this .onion URI so via tor only. Do not generate that QR from a .local URI, because it will not be reachable via internet - only from within your home-LAN.</li>
<li>Open your LN wallet app that you used to scan that QR and this time by using tor (see wallet app settings).
If the app doesn't offer tor, you can use Orbot (Android) instead. See section Installation->Clearnet for detailed instructions.</li>
</ul>
</details>
</li>
<li>
<details><summary>LNbits is running via Tor only, you want to offer public LN services via https</summary>
<ul>
<li>For this we need to partially open LNbits to a clearnet (domain/IP) through a https SSL certificate. Follow the instructions from <a href="https://docs.lnbits.org/guide/installation.html#reverse-proxy-with-automatic-https-using-caddy">this LNbits caddy installation instruction</a>.
You need to have a domain and to be able to configure a CNAME for your DNS record as well as generate a subdomain dedicated to your LNbits instance like eg. lnbits.yourdomain.com.
You also need access to your internet router to open the https port (usually 443) and forward it your LNbits IP within your LAN (usually 80). The ports might depend on your node implementation if those ports do not work please ask for them in a help group of your node supplier.</li>
<li>You can also follow the Apache installation option, explained in the <a href="https://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https">LNbits installation manual</a>.</li>
<li>If you run LNbits from a bundle node (Umbrel, myNode, Embassy, Raspiblitz etc), you can follow <a href="https://github.com/TrezorHannes/vps-lnbits">this extensive guide</a> with many options to switch your Tor only LNbits into a clearnet LNbits. For Citadel there is a HTTPS Option in your manual to activate https for LNbits in the newest version.</li>
</ul>
</details>
</li>
</ul>
</details>
<details><summary>Wallet-URL deleted, are my funds safu ?</summary>
<ul>
<li>
<details><summary>Wallet on demo server legend.lnbits</summary>
<p>Always save a copy of your wallet-URL, Export2phone-QR or LNDhub for your own wallets in a safe place. LNbits CANNOT help you to recover them when lost.</p>
</details>
</li>
<li>
<details><summary>Wallet on your own funding source/node</summary>
<p>Always save a copy of your wallet-URL, Export2phone-QR or LNDhub for your own wallets in a safe place.
You can find all LNbits users and wallet-IDs in your LNbits user manager extension or in your sqlite database.
To edit or read the LNbits database, go to the LNbits /data folder and look for the file called sqlite.db.
You can open and edit it with excel or with a dedicated SQL-Editor like <a href="https://sqlitebrowser.org/">SQLite browser</a>.</p>
</details>
</li>
</ul>
</details>
<details><summary>Configure a comment that people see when paying to my LNURLp QR</summary>
<p>When you create a LNURL-p, by default the comment box is not filled. That means comments are not allowed to be attached to payments.<p>
<p>In order to allow comments, add the characters lenght of the box, from 1 to 250. Once you put a number there,
the comment box will be displayed in the payment process. You can also edit a LNURL-p already created and add that number.</p>
![lnbits-lnurl-comment.png](https://i.postimg.cc/HkJQ9xKr/lnbits-lnurl-comment.png)
</details>
<details><summary>Can I deposit onchain BTC to LNbits ?</summary>
<p>There are multiple ways to exchange sats from onchain btc to LN btc (resp. to LNbits).</p>
<ul>
<li>
<details><summary>A - Via an external swap service</summary>
<p>If the user do not have full acceess of your LNbits, is just an external user, can use swap services like <a href="https://boltz.exchange/">Boltz</a>, <a href="https://fixedfloat.com/">FixedFloat</a>, <a href="https://swap.diamondhands.technology/">DiamondHands</a> or <a href="https://zigzag.io/">ZigZag</a>.</p>
<p>This is useful if you provide only LNURL/LN invoices from your LNbits instance, but a payer only has onchain sats so
they will have to the swap those sats first on their side.</p>
<p>The procedure is simple: user sends onchain btc to the swap service and provides the LNURL / LN invoice from LNbits as destination of the swap.</p>
</details>
</li>
<li>
<details><summary>B - Using the Onchain LNbits extension</summary>
<p>Keep in mind that this is a separate wallet, not the LN btc one that is represented by LNbits as "your wallet" upon your LN funding source.
This onchain wallet can be used also to swap LN btc to (e.g. your hardwarewallet) by using the LNbits Boltz or Deezy extension.
If you run a webshop that is linked to your LNbits for LN payments, it is very handy to regularily drain all the sats from LN into onchain.
This leads to more space in your LN channels to be able to receive new fresh sats.</p>
<p>Procedure:</p>
<ul>
<li>Use Electrum or Sparrow wallet to create a new onchain wallet and save the backup seed in a safe place</li>
<li>Go to wallet information and copy the xpub</li>
<li>Go to LNbits - Onchain extension and create a new watch-only wallet with that xpub</li>
<li>Go to LNbits - Tipjar extension and create a new Tipjar. Select also the onchain option besides the LN wallet.</li>
<li>Optional - Go to LNbits - SatsPay extension and create a new charge for onchain btc.
You can choose between onchain and LN or both. It will then create an invoice that can be shared.</li>
<li>Optional - If you use your LNbits linked to a Wordpress + Woocommerce page, once you create/link a watch-only wallet to your LN btc shop wallet,
the customer will have both options to pay on the same screen.</li>
</ul>
</details>
</li>
</ul>
</details>
<details><summary>Where can I see payment details?</summary>
<p>When you receive a payment in LNbits, the transaction log will display only a resumed type of the transaction.
![lnbits-tx-log.png](https://i.postimg.cc/gk2FMFG9/lnbits-tx-log.png)
<p>In your transaction overview you will find a little green arrow for received and a red arrow for sended funds.<p>
<p>If you click on those arrows, a details popup shows attached messages as well as the sender´s name if given.</p>
</details>
<details><summary>Can I configure a name to the payments i make?</summary>
<p>In LNbits this is currently not possible to do - but to receive. This is only possible if the sender's LN wallet supports <a href="https://github.com/lnurl/luds">LUD-18</a> (nameDesc) like e.g. <a href="https://darthcoin.substack.com/p/obw-open-bitcoin-wallet">Open Bitcion Wallet - OBW</a> does. You will then see an alias/pseudonym in the details section of your LNbits transactions (click the arrows). Note that you can give any name there and it might not be related to the real sender´s name(!) if your receive such.</p>
![lnbits-tx-details.png](https://i.postimg.cc/yYnvyK4w/lnbits-tx-details.png)
</p>
</details>
<details><summary>How can I use a LNbits lndhub account in other wallet apps?</summary>
<p>Open your LNbits with the account / wallet you want to use, go to "manage extensions" and activate the LNDHUB extension.</p>
<p>Then open the LNDHUB extension, choose the wallet you want to use and scan the QR code you want to use: "admin" or "invoice only", depending on the security level you want for that wallet.</p>
<p>You can use <a href="https://zeusln.app">Zeus</a> or <a href="https://bluewallet.io">Bluewallet</a> as wallet apps for a lndhub account.</p>
<p>Keep in mind: if your LNbits instance is Tor only, you must use also theose apps behind Tor and open the LNbits page through your Tor .onion address.</p>
</details>
</ul>
</ul>
## Building hardware tools
<ul> <p>LNbits has all sorts of open APIs and tools to program and connect to a lot of different devices for a gazillion of use-cases. Let us know what you did with it ! Come to the <a href="https://t.me/makerbits">Makerbits Telegram Group</a> if you are interested in building or if you need help with a project - we got you!</p>
<details><summary>ATM - deposit and withdraw in your shop or at your meetup</summary>
<p>This is a do-it-yourself project consisting of a mini-computer (Raspberry Pi Zero), a coin acceptor, a display, a 3D printed case, and a Bitcoin Lightning wallet as a funding source. It exchanges fiat coins for valuable Bitcoin Lightning ⚡ Satoshis. The user can pick up the Satoshis via QR code (LNURL-withdraw) on a mobile phone wallet. It works based on BTCPayer server, LNTXBOT is not longer an option. You can get the components as individual parts and build the case yourself e.g. from <a href="https://www.Fulmo.org">Fulmo</a> who also made a <a href="https://blog.fulmo.org/the-lightningatm-pocket-edition/">guide</a> on it. The shop offers payments in Bitcoin and Lightning ⚡. The code can be found on <a href="https://github.com/21isenough/LightningATM">the ATM github project page></a>.</p>
</details>
<details><summary>POS Terminal - an offline terminal for merchants</summary>
<p>The LNpos is a self-sufficient point of sale terminal which allows offline onchain payments and an offline Lightning ATM for withdrawals. Free and open source software, free from intermediaries, with integrated battery, WLAN, DIY. You can get the 3D print as well as the whole kit from the LNbits shop from 👇 Ressources. It allows
<li>LNPoS Online interactive Lightning payments</li>
<li>LNURLPoS Offline Lightning Payments. Passive interaction, sharing a secret as evidence</li>
<li>OnChain For onchain payments. Generates an address and displays a link for verification</li>
<li>LNURLATM Offline Lightning Payouts. Generates LNURLw link to do withdrawals</li>
<p>
<img width="285" alt="Bildschirm­foto 2023-01-20 um 18 09 34" src="https://user-images.githubusercontent.com/63317640/213761202-4c4d8531-7184-4e53-8645-fe0f08ac7d17.png">
</p>
</p>
</details>
<details><summary>Hardware Wallet- build your own, stack harder</summary>
<p>The hardwarewallet is a very cheap solution for builders. The projects´ <a hrel="https://github.com/lnbits/hardware-wallet">code and installation instructions for the LNbits hardware wallet can be found on github</a></p>
<p>
<img width="546" alt="Bildschirm­foto 2023-01-20 um 18 08 37" src="https://user-images.githubusercontent.com/63317640/213760948-38fd77b0-9247-4505-9433-f5af1b223527.png">
</p>
</details>
<details><summary>Bitcoin Switch - turn things on with bitcoin</summary>
<p>Candy dispenser, vending machines (online), grabbing machines, jukeboxes, bandits and <a href="https://github.com/cryptoteun/awesome-lnbits">all sorts of other things have already been build with LNbits´ tools</a>. Further info see 👇 Ressources.</p>
<p>
<img width="549" alt="Bildschirm­foto 2023-01-20 um 18 11 55" src="https://user-images.githubusercontent.com/63317640/213761646-d25d4745-e50d-4389-98e5-f83237a8cf6b.png">
</p>
</details>
<details><summary>Vending machine (offline)</summary>
<p>This code works similar to the LNpos. Note that the <a href=" https://www.youtube.com/watch?v=Fg0UuuzsYXc&t=762s">setup-video for the vending machine</a> misses the new way of installing it via the new LNURLdevices extension. The <a href="https://github.com/arcbtc/LNURLVend">vending machine project code resides on github</a>.</p>
<p>
<img width="753" alt="Bildschirm­foto 2023-01-20 um 18 13 22" src="https://user-images.githubusercontent.com/63317640/213761946-5025a7b8-c6d4-40cf-a6d3-d298593e79f6.png">
</p>
</details>
<details><summary><b>Resources - Building hardware tools</b></summary>
<ul>
<li><a href="https://t.me/makerbits'">MakerBits</a> - Telegram support group</li>
<li><a href="https://ereignishorizont.xyz/">Instructions for LNpos, Switch, ATM, BTCticker</a> - guides in DE & EN</li>
<li><a href="https://shop.lnbits.com/">LNbits shop</a> - eadymade hardware gimmicks from the community</li>
<li><a href="https://github.com/cryptoteun/awesome-lnbits#hardware-projects-utilizing-lnbits">Collection of hardware projects using LNbits</a></li>
</ul>
</details>
</ul>
## Use cases of LNbits
<ul><details><summary>Merchant</summary>
<p>LNbits is a powerful solution for merchants, due to the easy setup with various extensions, that can be used for many scenarios.</p>
<p><a href="https://darthcoin.substack.com/p/lnbits-for-small-merchants">Here is an overview of the LNbits tools available for a small restaurant as well as a hotel</a></p>
</details>
<details><summary>Swapping ⚡LN BTC to a BTC address</summary>
<p>LNbits has two swap extensions integrated: <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltz">Boltz</a> and <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/deezy">Deezy</a>.</p>
<p>For a merchant that uses LNbits to receive BTC payments through LN, this is very handy to move the received sats from LN channels into onchain wallets. It not only helps you HODLing but is also freeing up "space in your channels" so you are ready to receive more sats.</p>
<p>Boltz has an option to setup an automated swap triggered by a certain amount received.</p>
</details>
<details><summary>Voucher</summary>
<p>Printed voucher links or tippingcards</p>
<p>To generate voucher you will need LNbits to be available in clearnet. Please consider running your own LNbits instance for this.</p>
<p>LNURLw are strings that represent a faucet-link to a wallet. By scanning it, everyone will be able to withdraw sats from it. A LNURLw can be either a QR that leads to a static link or to one that responds with new invoices every time it is scanned (click "no assmilking"). You can create these QR by adding the LNURLw extension and generate the vouchertype you need.</p>
<ul>
<li>Voucher can as well be printed directly from LNbits. After you created it, click the "eye" next to the link. By pressing the printer-button you print the plain QR but you could as well integrate it into a nice tippincard or voucher template by choosing "Advanced voucher" -> "Use custom voucher design". We collected some designs as well as templates to make your own ones under <a href="https://youtu.be/c5EV9UNgVqk">this LNbits voucher video-guide.</a>. You will be able to create and print as much voucher as you like with it. Happy orangepilling!</li>
<li> Note that your LNbits needs to be reachable in clearnet to offer vouchers to others.</li>
</ul>
</details>
<details><summary>NFC Cards, Badges, Rings etc.</summary>
<p>Creating a NFC card for a wallet</p>
<p>To generate links for your cards you will need LNbits to be available in clearnet. Please consider running your own LNbits instance for this.</p>
<ul>
<li>On top to just printing voucher for your wallet you can also <a href="https://youtu.be/CQz1ILcK0PY">write these LNURLw to a simple NFC card fromon NTAG216</a> by not clicking the printer but the NFC symbol on android/chrome and tapping your card against the device. This will enable the cardholder to directly spend those sats at a tpos, pos or wallet-app another one uses that can handle lightning payments via NFC. </li>
<li>If you run an event and want to hand out bigger amounts of cards with simple voucher links on consider this <a hrel="nfc-brrr.com/">NFC-brrr batch tool</a> as well as using NTAG424 cards, so that your customers can rewrite them later with an own wallet and the boltcard service (see ff)</li>
<li>For bigger amounts the Boltcard-Extension should be used. It will generate a link that sends a new invoice every time it is used for payments and keeps track too if the allowed card-ID is redeeming funds. Hence the setup of Boltcards is a bit safer but it needs some additional tools. You can find <a href="https://plebtag.com/write-tags/">further infos on creating or updating boltcards here</a>.</li>
</ul><p>
<ul><details><summary>Resources - NFC & LNbits</summary>
<ul>
<li><a href="https://www.boltcard.org">Coincorner Boltcard</a></li>
<li><a href="https://www.plebtag.com">PlebTag (infos, Lasercards, Badges)</a></li>
<li><a href="https://www.lasereyes.cards">Lasercards</a></li>
<li><a href="https://www.bitcoin-ring.com">Bitcoin Ring</a></li>
<li><a href="https://github.com/taxmeifyoucan/HCPP2021-Badge">Badge</a></li>
<li><a href="https://github.com/cryptoteun/awesome-lnbits#powered-by-lnbits">Powered by LNbits examples</a></li>
</ul>
</ul>
</p>
</details>
</details>
</ul>
## Developing for LNbits
<ul>
<li><a href="http://docs.lnbits.org/devs/development.html">Making Estension / how to use Websockets / API reference</a></li>
<li><a href="https://t.me/lnbits">Telegram LNbits Support Group</a></li></ul>
</ul>

View File

@ -4,13 +4,15 @@ from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
from uuid import uuid4
import shortuuid
from lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection
from lnbits.extension_manager import InstallableExtension
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
from . import db
from .models import BalanceCheck, Payment, User, Wallet
from .models import BalanceCheck, Payment, TinyURL, User, Wallet
# accounts
# --------
@ -730,4 +732,43 @@ async def update_migration_version(conn, db_name, version):
ON CONFLICT (db) DO UPDATE SET version = ?
""",
(db_name, version, version),
=======
# tinyurl
# -------
async def create_tinyurl(domain: str, endless: bool, wallet: str):
tinyurl_id = shortuuid.uuid()[:8]
await db.execute(
f"INSERT INTO tiny_url (id, url, endless, wallet) VALUES (?, ?, ?, ?)",
(
tinyurl_id,
domain,
endless,
wallet,
),
)
return await get_tinyurl(tinyurl_id)
async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
row = await db.fetchone(
f"SELECT * FROM tiny_url WHERE id = ?",
(tinyurl_id,),
)
return TinyURL.from_row(row) if row else None
async def get_tinyurl_by_url(url: str) -> List[TinyURL]:
rows = await db.fetchall(
f"SELECT * FROM tiny_url WHERE url = ?",
(url,),
)
return [TinyURL.from_row(row) for row in rows]
async def delete_tinyurl(tinyurl_id: str):
row = await db.execute(
f"DELETE FROM tiny_url WHERE id = ?",
(tinyurl_id,),
)

View File

@ -270,8 +270,20 @@ async def m008_create_admin_settings_table(db):
"""
)
async def m009_create_tinyurl_table(db):
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS tiny_url (
id TEXT PRIMARY KEY,
url TEXT,
endless BOOL NOT NULL DEFAULT false,
wallet TEXT,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
async def m009_create_installed_extensions_table(db):
async def m010_create_installed_extensions_table(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS installed_extensions (

View File

@ -216,3 +216,14 @@ class BalanceCheck(BaseModel):
class CoreAppExtra:
register_new_ext_routes: Callable
class TinyURL(BaseModel):
id: str
url: str
endless: bool
wallet: str
time: float
@classmethod
def from_row(cls, row: Row):
return cls(**dict(row))

View File

@ -433,9 +433,8 @@ async def check_admin_settings():
for key, value in settings.dict(exclude_none=True).items():
logger.debug(f"{key}: {value}")
http = "https" if settings.lnbits_force_https else "http"
admin_url = (
f"{http}://{settings.host}:{settings.port}/wallet?usr={settings.super_user}"
f"http://{settings.host}:{settings.port}/wallet?usr={settings.super_user}"
)
logger.success(f"✔️ Access super user account at: {admin_url}")

View File

@ -26,7 +26,7 @@ from loguru import logger
from pydantic import BaseModel
from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse
from starlette.responses import StreamingResponse
from starlette.responses import RedirectResponse, StreamingResponse
from lnbits import bolt11, lnurl
from lnbits.core.helpers import migrate_extension_database
@ -58,8 +58,12 @@ from ..crud import (
add_installed_extension,
delete_installed_extension,
get_dbversions,
create_tinyurl,
delete_tinyurl,
get_payments,
get_standalone_payment,
get_tinyurl,
get_tinyurl_by_url,
get_total_balance,
get_wallet_for_key,
save_balance_check,
@ -814,4 +818,74 @@ async def get_extension_releases(ext_id: str, user: User = Depends(check_admin))
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
############################TINYURL##################################
@core_app.post("/api/v1/tinyurl")
async def api_create_tinyurl(
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(get_key_type)
):
tinyurls = await get_tinyurl_by_url(url)
try:
for tinyurl in tinyurls:
if tinyurl:
if tinyurl.wallet == wallet.wallet.inkey:
return tinyurl
return await create_tinyurl(url, endless, wallet.wallet.inkey)
except:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to create tinyurl"
)
@core_app.get("/api/v1/tinyurl/{tinyurl_id}")
async def api_get_tinyurl(
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
try:
tinyurl = await get_tinyurl(tinyurl_id)
if tinyurl:
if tinyurl.wallet == wallet.wallet.inkey:
return tinyurl
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
)
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Unable to fetch tinyurl"
)
@core_app.delete("/api/v1/tinyurl/{tinyurl_id}")
async def api_delete_tinyurl(
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
try:
tinyurl = await get_tinyurl(tinyurl_id)
if tinyurl:
if tinyurl.wallet == wallet.wallet.inkey:
await delete_tinyurl(tinyurl_id)
return {"deleted": True}
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
)
except:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to delete"
)
@core_app.get("/t/{tinyurl_id}")
async def api_tinyurl(tinyurl_id: str):
try:
tinyurl = await get_tinyurl(tinyurl_id)
if tinyurl:
response = RedirectResponse(url=tinyurl.url)
return response
else:
return
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="unable to find tinyurl"
)

View File

@ -46,6 +46,15 @@ async def home(request: Request, lightning: str = ""):
)
@core_html_routes.get("/robots.txt", response_class=HTMLResponse)
async def robots():
data = """
User-agent: *
Disallow: /
"""
return HTMLResponse(content=data, media_type="text/plain")
@core_html_routes.get(
"/extensions", name="core.extensions", response_class=HTMLResponse
)

View File

@ -5,11 +5,12 @@ move **IN** and **OUT** of the **lightning network** and remain in control of yo
* [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
## 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)
@ -22,14 +23,14 @@ This extension lets you create swaps, reverse swaps and in the case of failure r
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
## 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
# 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.
@ -37,4 +38,5 @@ in case that happens, there is a info icon in the Swap (In) List which opens fol
![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)
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,421 +0,0 @@
import asyncio
import os
from hashlib import sha256
from typing import Awaitable, Union
import httpx
from embit import ec, script
from embit.networks import NETWORKS
from embit.transaction import SIGHASH, Transaction, TransactionInput, TransactionOutput
from loguru import logger
from lnbits.core.services import create_invoice, pay_invoice
from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import settings
from .crud import update_swap_status
from .mempool import (
get_fee_estimation,
get_mempool_blockheight,
get_mempool_fees,
get_mempool_tx,
get_mempool_tx_from_txs,
send_onchain_tx,
wait_for_websocket_message,
)
from .models import (
CreateReverseSubmarineSwap,
CreateSubmarineSwap,
ReverseSubmarineSwap,
SubmarineSwap,
SwapStatus,
)
from .utils import check_balance, get_timestamp, req_wrap
net = NETWORKS[settings.boltz_network]
async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:
if not check_boltz_limits(data.amount):
msg = f"Boltz - swap not in boltz limits"
logger.warning(msg)
raise Exception(msg)
swap_id = urlsafe_short_hash()
try:
payment_hash, payment_request = await create_invoice(
wallet_id=data.wallet,
amount=data.amount,
memo=f"swap of {data.amount} sats on boltz.exchange",
extra={"tag": "boltz", "swap_id": swap_id},
)
except Exception as exc:
msg = f"Boltz - create_invoice failed {str(exc)}"
logger.error(msg)
raise
refund_privkey = ec.PrivateKey(os.urandom(32), True, net)
refund_pubkey_hex = bytes.hex(refund_privkey.sec()).decode()
res = req_wrap(
"post",
f"{settings.boltz_url}/createswap",
json={
"type": "submarine",
"pairId": "BTC/BTC",
"orderSide": "sell",
"refundPublicKey": refund_pubkey_hex,
"invoice": payment_request,
"referralId": "lnbits",
},
headers={"Content-Type": "application/json"},
)
res = res.json()
logger.info(
f"Boltz - created normal swap, boltz_id: {res['id']}. wallet: {data.wallet}"
)
return SubmarineSwap(
id=swap_id,
time=get_timestamp(),
wallet=data.wallet,
amount=data.amount,
payment_hash=payment_hash,
refund_privkey=refund_privkey.wif(net),
refund_address=data.refund_address,
boltz_id=res["id"],
status="pending",
address=res["address"],
expected_amount=res["expectedAmount"],
timeout_block_height=res["timeoutBlockHeight"],
bip21=res["bip21"],
redeem_script=res["redeemScript"],
)
"""
explanation taken from electrum
send on Lightning, receive on-chain
- User generates preimage, RHASH. Sends RHASH to server.
- Server creates an LN invoice for RHASH.
- User pays LN invoice - except server needs to hold the HTLC as preimage is unknown.
- Server creates on-chain output locked to RHASH.
- User spends on-chain output, revealing preimage.
- Server fulfills HTLC using preimage.
Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee.
"""
async def create_reverse_swap(
data: CreateReverseSubmarineSwap,
) -> [ReverseSubmarineSwap, asyncio.Task]:
if not check_boltz_limits(data.amount):
msg = f"Boltz - reverse swap not in boltz limits"
logger.warning(msg)
raise Exception(msg)
swap_id = urlsafe_short_hash()
if not await check_balance(data):
logger.error(f"Boltz - reverse swap, insufficient balance.")
return False
claim_privkey = ec.PrivateKey(os.urandom(32), True, net)
claim_pubkey_hex = bytes.hex(claim_privkey.sec()).decode()
preimage = os.urandom(32)
preimage_hash = sha256(preimage).hexdigest()
res = req_wrap(
"post",
f"{settings.boltz_url}/createswap",
json={
"type": "reversesubmarine",
"pairId": "BTC/BTC",
"orderSide": "buy",
"invoiceAmount": data.amount,
"preimageHash": preimage_hash,
"claimPublicKey": claim_pubkey_hex,
"referralId": "lnbits",
},
headers={"Content-Type": "application/json"},
)
res = res.json()
logger.info(
f"Boltz - created reverse swap, boltz_id: {res['id']}. wallet: {data.wallet}"
)
swap = ReverseSubmarineSwap(
id=swap_id,
amount=data.amount,
wallet=data.wallet,
onchain_address=data.onchain_address,
instant_settlement=data.instant_settlement,
claim_privkey=claim_privkey.wif(net),
preimage=preimage.hex(),
status="pending",
boltz_id=res["id"],
timeout_block_height=res["timeoutBlockHeight"],
lockup_address=res["lockupAddress"],
onchain_amount=res["onchainAmount"],
redeem_script=res["redeemScript"],
invoice=res["invoice"],
time=get_timestamp(),
)
logger.debug(f"Boltz - waiting for onchain tx, reverse swap_id: {swap.id}")
task = create_task_log_exception(
swap.id, wait_for_onchain_tx(swap, swap_websocket_callback_initial)
)
return swap, task
def start_onchain_listener(swap: ReverseSubmarineSwap) -> asyncio.Task:
return create_task_log_exception(
swap.id, wait_for_onchain_tx(swap, swap_websocket_callback_restart)
)
async def start_confirmation_listener(
swap: ReverseSubmarineSwap, mempool_lockup_tx
) -> asyncio.Task:
logger.debug(f"Boltz - reverse swap, waiting for confirmation...")
tx, txid, *_ = mempool_lockup_tx
confirmed = await wait_for_websocket_message({"track-tx": txid}, "txConfirmed")
if confirmed:
logger.debug(f"Boltz - reverse swap lockup transaction confirmed! claiming...")
await create_claim_tx(swap, mempool_lockup_tx)
else:
logger.debug(f"Boltz - reverse swap lockup transaction still not confirmed.")
def create_task_log_exception(swap_id: str, awaitable: Awaitable) -> asyncio.Task:
async def _log_exception(awaitable):
try:
return await awaitable
except Exception as e:
logger.error(f"Boltz - reverse swap failed!: {swap_id} - {e}")
await update_swap_status(swap_id, "failed")
return asyncio.create_task(_log_exception(awaitable))
async def swap_websocket_callback_initial(swap):
wstask = asyncio.create_task(
wait_for_websocket_message(
{"track-address": swap.lockup_address}, "address-transactions"
)
)
logger.debug(
f"Boltz - created task, waiting on mempool websocket for address: {swap.lockup_address}"
)
# create_task is used because pay_invoice is stuck as long as boltz does not
# see the onchain claim tx and it ends up in deadlock
task: asyncio.Task = create_task_log_exception(
swap.id,
pay_invoice(
wallet_id=swap.wallet,
payment_request=swap.invoice,
description=f"reverse swap for {swap.amount} sats on boltz.exchange",
extra={"tag": "boltz", "swap_id": swap.id, "reverse": True},
),
)
logger.debug(f"Boltz - task pay_invoice created, reverse swap_id: {swap.id}")
done, pending = await asyncio.wait(
[task, wstask], return_when=asyncio.FIRST_COMPLETED
)
message = done.pop().result()
# pay_invoice already failed, do not wait for onchain tx anymore
if message is None:
logger.debug(f"Boltz - pay_invoice already failed cancel websocket task.")
wstask.cancel()
raise
return task, message
async def swap_websocket_callback_restart(swap):
logger.debug(f"Boltz - swap_websocket_callback_restart called...")
message = await wait_for_websocket_message(
{"track-address": swap.lockup_address}, "address-transactions"
)
return None, message
async def wait_for_onchain_tx(swap: ReverseSubmarineSwap, callback):
task, txs = await callback(swap)
mempool_lockup_tx = get_mempool_tx_from_txs(txs, swap.lockup_address)
if mempool_lockup_tx:
tx, txid, *_ = mempool_lockup_tx
if swap.instant_settlement or tx["status"]["confirmed"]:
logger.debug(
f"Boltz - reverse swap instant settlement, claiming immediatly..."
)
await create_claim_tx(swap, mempool_lockup_tx)
else:
await start_confirmation_listener(swap, mempool_lockup_tx)
try:
if task:
await task
except:
logger.error(
f"Boltz - could not await pay_invoice task, but sent onchain. should never happen!"
)
else:
logger.error(f"Boltz - mempool lockup tx not found.")
async def create_claim_tx(swap: ReverseSubmarineSwap, mempool_lockup_tx):
tx = await create_onchain_tx(swap, mempool_lockup_tx)
await send_onchain_tx(tx)
logger.debug(f"Boltz - onchain tx sent, reverse swap completed")
await update_swap_status(swap.id, "complete")
async def create_refund_tx(swap: SubmarineSwap):
mempool_lockup_tx = get_mempool_tx(swap.address)
tx = await create_onchain_tx(swap, mempool_lockup_tx)
await send_onchain_tx(tx)
def check_block_height(block_height: int):
current_block_height = get_mempool_blockheight()
if current_block_height <= block_height:
msg = f"refund not possible, timeout_block_height ({block_height}) is not yet exceeded ({current_block_height})"
logger.debug(msg)
raise Exception(msg)
"""
a submarine swap consists of 2 onchain tx's a lockup and a redeem tx.
we create a tx to redeem the funds locked by the onchain lockup tx.
claim tx for reverse swaps, refund tx for normal swaps they are the same
onchain redeem tx, the difference between them is the private key, onchain_address,
input sequence and input script_sig
"""
async def create_onchain_tx(
swap: Union[ReverseSubmarineSwap, SubmarineSwap], mempool_lockup_tx
) -> Transaction:
is_refund_tx = type(swap) == SubmarineSwap
if is_refund_tx:
check_block_height(swap.timeout_block_height)
privkey = ec.PrivateKey.from_wif(swap.refund_privkey)
onchain_address = swap.refund_address
preimage = b""
sequence = 0xFFFFFFFE
else:
privkey = ec.PrivateKey.from_wif(swap.claim_privkey)
preimage = bytes.fromhex(swap.preimage)
onchain_address = swap.onchain_address
sequence = 0xFFFFFFFF
locktime = swap.timeout_block_height
redeem_script = bytes.fromhex(swap.redeem_script)
fees = get_fee_estimation()
tx, txid, vout_cnt, vout_amount = mempool_lockup_tx
script_pubkey = script.address_to_scriptpubkey(onchain_address)
vin = [TransactionInput(bytes.fromhex(txid), vout_cnt, sequence=sequence)]
vout = [TransactionOutput(vout_amount - fees, script_pubkey)]
tx = Transaction(vin=vin, vout=vout)
if is_refund_tx:
tx.locktime = locktime
# TODO: 2 rounds for fee calculation, look at vbytes after signing and do another TX
s = script.Script(data=redeem_script)
for i, inp in enumerate(vin):
if is_refund_tx:
rs = bytes([34]) + bytes([0]) + bytes([32]) + sha256(redeem_script).digest()
tx.vin[i].script_sig = script.Script(data=rs)
h = tx.sighash_segwit(i, s, vout_amount)
sig = privkey.sign(h).serialize() + bytes([SIGHASH.ALL])
witness_items = [sig, preimage, redeem_script]
tx.vin[i].witness = script.Witness(items=witness_items)
return tx
def get_swap_status(swap: Union[SubmarineSwap, ReverseSubmarineSwap]) -> SwapStatus:
swap_status = SwapStatus(
wallet=swap.wallet,
swap_id=swap.id,
)
try:
boltz_request = get_boltz_status(swap.boltz_id)
swap_status.boltz = boltz_request["status"]
except httpx.HTTPStatusError as exc:
json = exc.response.json()
swap_status.boltz = json["error"]
if "could not find" in swap_status.boltz:
swap_status.exists = False
if type(swap) == SubmarineSwap:
swap_status.reverse = False
swap_status.address = swap.address
else:
swap_status.reverse = True
swap_status.address = swap.lockup_address
swap_status.block_height = get_mempool_blockheight()
swap_status.timeout_block_height = (
f"{str(swap.timeout_block_height)} -> current: {str(swap_status.block_height)}"
)
if swap_status.block_height >= swap.timeout_block_height:
swap_status.hit_timeout = True
mempool_tx = get_mempool_tx(swap_status.address)
swap_status.lockup = mempool_tx
if mempool_tx == None:
swap_status.has_lockup = False
swap_status.confirmed = False
swap_status.mempool = "transaction.unknown"
swap_status.message = "lockup tx not in mempool"
else:
swap_status.has_lockup = True
tx, *_ = mempool_tx
if tx["status"]["confirmed"] == True:
swap_status.mempool = "transaction.confirmed"
swap_status.confirmed = True
else:
swap_status.confirmed = False
swap_status.mempool = "transaction.unconfirmed"
return swap_status
def check_boltz_limits(amount):
try:
pairs = get_boltz_pairs()
limits = pairs["pairs"]["BTC/BTC"]["limits"]
return amount >= limits["minimal"] and amount <= limits["maximal"]
except:
return False
def get_boltz_pairs():
res = req_wrap(
"get",
f"{settings.boltz_url}/getpairs",
headers={"Content-Type": "application/json"},
)
return res.json()
def get_boltz_status(boltzid):
res = req_wrap(
"post",
f"{settings.boltz_url}/swapstatus",
json={"id": boltzid},
)
return res.json()

View File

@ -1,21 +1,21 @@
from http import HTTPStatus
import time
from typing import List, Optional, Union
from boltz_client.boltz import BoltzReverseSwapResponse, BoltzSwapResponse
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import (
AutoReverseSubmarineSwap,
CreateAutoReverseSubmarineSwap,
CreateReverseSubmarineSwap,
CreateSubmarineSwap,
ReverseSubmarineSwap,
SubmarineSwap,
)
"""
Submarine Swaps
"""
async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[SubmarineSwap]:
if isinstance(wallet_ids, str):
@ -30,20 +30,6 @@ async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[Submari
return [SubmarineSwap(**row) for row in rows]
async def get_pending_submarine_swaps(
wallet_ids: Union[str, List[str]]
) -> List[SubmarineSwap]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM boltz.submarineswap WHERE wallet IN ({q}) and status='pending' order by time DESC",
(*wallet_ids,),
)
return [SubmarineSwap(**row) for row in rows]
async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]:
rows = await db.fetchall(
f"SELECT * FROM boltz.submarineswap WHERE status='pending' order by time DESC",
@ -51,14 +37,20 @@ async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]:
return [SubmarineSwap(**row) for row in rows]
async def get_submarine_swap(swap_id) -> SubmarineSwap:
async def get_submarine_swap(swap_id) -> Optional[SubmarineSwap]:
row = await db.fetchone(
"SELECT * FROM boltz.submarineswap WHERE id = ?", (swap_id,)
)
return SubmarineSwap(**row) if row else None
async def create_submarine_swap(swap: SubmarineSwap) -> Optional[SubmarineSwap]:
async def create_submarine_swap(
data: CreateSubmarineSwap,
swap: BoltzSwapResponse,
swap_id: str,
refund_privkey_wif: str,
payment_hash: str,
) -> Optional[SubmarineSwap]:
await db.execute(
"""
@ -80,26 +72,22 @@ async def create_submarine_swap(swap: SubmarineSwap) -> Optional[SubmarineSwap]:
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
swap_id,
data.wallet,
payment_hash,
"pending",
swap.id,
swap.wallet,
swap.payment_hash,
swap.status,
swap.boltz_id,
swap.refund_privkey,
swap.refund_address,
swap.expected_amount,
swap.timeout_block_height,
refund_privkey_wif,
data.refund_address,
swap.expectedAmount,
swap.timeoutBlockHeight,
swap.address,
swap.bip21,
swap.redeem_script,
swap.amount,
swap.redeemScript,
data.amount,
),
)
return await get_submarine_swap(swap.id)
async def delete_submarine_swap(swap_id):
await db.execute("DELETE FROM boltz.submarineswap WHERE id = ?", (swap_id,))
return await get_submarine_swap(swap_id)
async def get_reverse_submarine_swaps(
@ -117,21 +105,6 @@ async def get_reverse_submarine_swaps(
return [ReverseSubmarineSwap(**row) for row in rows]
async def get_pending_reverse_submarine_swaps(
wallet_ids: Union[str, List[str]]
) -> List[ReverseSubmarineSwap]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM boltz.reverse_submarineswap WHERE wallet IN ({q}) and status='pending' order by time DESC",
(*wallet_ids,),
)
return [ReverseSubmarineSwap(**row) for row in rows]
async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap]:
rows = await db.fetchall(
f"SELECT * FROM boltz.reverse_submarineswap WHERE status='pending' order by time DESC"
@ -140,7 +113,7 @@ async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap
return [ReverseSubmarineSwap(**row) for row in rows]
async def get_reverse_submarine_swap(swap_id) -> SubmarineSwap:
async def get_reverse_submarine_swap(swap_id) -> Optional[ReverseSubmarineSwap]:
row = await db.fetchone(
"SELECT * FROM boltz.reverse_submarineswap WHERE id = ?", (swap_id,)
)
@ -148,8 +121,31 @@ async def get_reverse_submarine_swap(swap_id) -> SubmarineSwap:
async def create_reverse_submarine_swap(
swap: ReverseSubmarineSwap,
) -> Optional[ReverseSubmarineSwap]:
data: CreateReverseSubmarineSwap,
claim_privkey_wif: str,
preimage_hex: str,
swap: BoltzReverseSwapResponse,
) -> ReverseSubmarineSwap:
swap_id = urlsafe_short_hash()
reverse_swap = ReverseSubmarineSwap(
id=swap_id,
wallet=data.wallet,
status="pending",
boltz_id=swap.id,
instant_settlement=data.instant_settlement,
preimage=preimage_hex,
claim_privkey=claim_privkey_wif,
lockup_address=swap.lockupAddress,
invoice=swap.invoice,
onchain_amount=swap.onchainAmount,
onchain_address=data.onchain_address,
timeout_block_height=swap.timeoutBlockHeight,
redeem_script=swap.redeemScript,
amount=data.amount,
time=int(time.time()),
)
await db.execute(
"""
@ -172,36 +168,93 @@ async def create_reverse_submarine_swap(
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
swap.id,
reverse_swap.id,
reverse_swap.wallet,
reverse_swap.status,
reverse_swap.boltz_id,
reverse_swap.instant_settlement,
reverse_swap.preimage,
reverse_swap.claim_privkey,
reverse_swap.lockup_address,
reverse_swap.invoice,
reverse_swap.onchain_amount,
reverse_swap.onchain_address,
reverse_swap.timeout_block_height,
reverse_swap.redeem_script,
reverse_swap.amount,
),
)
return reverse_swap
async def get_auto_reverse_submarine_swaps(
wallet_ids: List[str],
) -> List[AutoReverseSubmarineSwap]:
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM boltz.auto_reverse_submarineswap WHERE wallet IN ({q}) order by time DESC",
(*wallet_ids,),
)
return [AutoReverseSubmarineSwap(**row) for row in rows]
async def get_auto_reverse_submarine_swap(
swap_id,
) -> Optional[AutoReverseSubmarineSwap]:
row = await db.fetchone(
"SELECT * FROM boltz.auto_reverse_submarineswap WHERE id = ?", (swap_id,)
)
return AutoReverseSubmarineSwap(**row) if row else None
async def get_auto_reverse_submarine_swap_by_wallet(
wallet_id,
) -> Optional[AutoReverseSubmarineSwap]:
row = await db.fetchone(
"SELECT * FROM boltz.auto_reverse_submarineswap WHERE wallet = ?", (wallet_id,)
)
return AutoReverseSubmarineSwap(**row) if row else None
async def create_auto_reverse_submarine_swap(
swap: CreateAutoReverseSubmarineSwap,
) -> Optional[AutoReverseSubmarineSwap]:
swap_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO boltz.auto_reverse_submarineswap (
id,
wallet,
onchain_address,
instant_settlement,
balance,
amount
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
swap_id,
swap.wallet,
swap.status,
swap.boltz_id,
swap.instant_settlement,
swap.preimage,
swap.claim_privkey,
swap.lockup_address,
swap.invoice,
swap.onchain_amount,
swap.onchain_address,
swap.timeout_block_height,
swap.redeem_script,
swap.instant_settlement,
swap.balance,
swap.amount,
),
)
return await get_reverse_submarine_swap(swap.id)
return await get_auto_reverse_submarine_swap(swap_id)
async def delete_auto_reverse_submarine_swap(swap_id):
await db.execute(
"DELETE FROM boltz.auto_reverse_submarineswap WHERE id = ?", (swap_id,)
)
async def update_swap_status(swap_id: str, status: str):
reverse = ""
swap = await get_submarine_swap(swap_id)
if swap is None:
swap = await get_reverse_submarine_swap(swap_id)
if swap is None:
return None
if type(swap) == SubmarineSwap:
if swap:
await db.execute(
"UPDATE boltz.submarineswap SET status='"
+ status
@ -209,17 +262,23 @@ async def update_swap_status(swap_id: str, status: str):
+ swap.id
+ "'"
)
if type(swap) == ReverseSubmarineSwap:
reverse = "reverse"
logger.info(
f"Boltz - swap status change: {status}. boltz_id: {swap.boltz_id}, wallet: {swap.wallet}"
)
return swap
reverse_swap = await get_reverse_submarine_swap(swap_id)
if reverse_swap:
await db.execute(
"UPDATE boltz.reverse_submarineswap SET status='"
+ status
+ "' WHERE id='"
+ swap.id
+ reverse_swap.id
+ "'"
)
logger.info(
f"Boltz - reverse swap status change: {status}. boltz_id: {reverse_swap.boltz_id}, wallet: {reverse_swap.wallet}"
)
return reverse_swap
message = f"Boltz - {reverse} swap status change: {status}. boltz_id: {swap.boltz_id}, wallet: {swap.wallet}"
logger.info(message)
return swap
return None

View File

@ -1,93 +0,0 @@
import asyncio
import json
import httpx
import websockets
from embit.transaction import Transaction
from loguru import logger
from lnbits.settings import settings
from .utils import req_wrap
websocket_url = f"{settings.boltz_mempool_space_url_ws}/api/v1/ws"
async def wait_for_websocket_message(send, message_string):
async for websocket in websockets.connect(websocket_url):
try:
await websocket.send(json.dumps({"action": "want", "data": ["blocks"]}))
await websocket.send(json.dumps(send))
async for raw in websocket:
message = json.loads(raw)
if message_string in message:
return message.get(message_string)
except websockets.ConnectionClosed:
continue
def get_mempool_tx(address):
res = req_wrap(
"get",
f"{settings.boltz_mempool_space_url}/api/address/{address}/txs",
headers={"Content-Type": "text/plain"},
)
txs = res.json()
return get_mempool_tx_from_txs(txs, address)
def get_mempool_tx_from_txs(txs, address):
if len(txs) == 0:
return None
tx = txid = vout_cnt = vout_amount = None
for a_tx in txs:
for i, vout in enumerate(a_tx["vout"]):
if vout["scriptpubkey_address"] == address:
tx = a_tx
txid = a_tx["txid"]
vout_cnt = i
vout_amount = vout["value"]
# should never happen
if tx == None:
raise Exception("mempool tx not found")
if txid == None:
raise Exception("mempool txid not found")
return tx, txid, vout_cnt, vout_amount
def get_fee_estimation() -> int:
# TODO: hardcoded maximum tx size, in the future we try to get the size of the tx via embit
# we need a function like Transaction.vsize()
tx_size_vbyte = 200
mempool_fees = get_mempool_fees()
return mempool_fees * tx_size_vbyte
def get_mempool_fees() -> int:
res = req_wrap(
"get",
f"{settings.boltz_mempool_space_url}/api/v1/fees/recommended",
headers={"Content-Type": "text/plain"},
)
fees = res.json()
return int(fees["economyFee"])
def get_mempool_blockheight() -> int:
res = req_wrap(
"get",
f"{settings.boltz_mempool_space_url}/api/blocks/tip/height",
headers={"Content-Type": "text/plain"},
)
return int(res.text)
async def send_onchain_tx(tx: Transaction):
raw = bytes.hex(tx.serialize())
logger.debug(f"Boltz - mempool sending onchain tx...")
req_wrap(
"post",
f"{settings.boltz_mempool_space_url}/api/tx",
headers={"Content-Type": "text/plain"},
content=raw,
)

View File

@ -44,3 +44,21 @@ async def m001_initial(db):
);
"""
)
async def m002_auto_swaps(db):
await db.execute(
"""
CREATE TABLE boltz.auto_reverse_submarineswap (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
onchain_address TEXT NOT NULL,
amount INT NOT NULL,
balance INT NOT NULL,
instant_settlement BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)

View File

@ -1,9 +1,5 @@
import json
from typing import Dict, List, Optional
from fastapi.params import Query
from pydantic.main import BaseModel
from sqlalchemy.engine import base
from fastapi import Query
from pydantic import BaseModel
class SubmarineSwap(BaseModel):
@ -51,25 +47,22 @@ class CreateReverseSubmarineSwap(BaseModel):
wallet: str = Query(...)
amount: int = Query(...)
instant_settlement: bool = Query(...)
# validate on-address, bcrt1 for regtest addresses
onchain_address: str = Query(
..., regex="^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$"
)
onchain_address: str = Query(...)
class SwapStatus(BaseModel):
swap_id: str
class AutoReverseSubmarineSwap(BaseModel):
id: str
wallet: str
status: str = ""
message: str = ""
boltz: str = ""
mempool: str = ""
address: str = ""
block_height: int = 0
timeout_block_height: str = ""
lockup: Optional[dict] = {}
has_lockup: bool = False
hit_timeout: bool = False
confirmed: bool = True
exists: bool = True
reverse: bool = False
amount: int
balance: int
onchain_address: str
instant_settlement: bool
time: int
class CreateAutoReverseSubmarineSwap(BaseModel):
wallet: str = Query(...)
amount: int = Query(...)
balance: int = Query(0)
instant_settlement: bool = Query(...)
onchain_address: str = Query(...)

View File

@ -1,129 +1,25 @@
import asyncio
import httpx
from boltz_client.boltz import BoltzNotFoundException, BoltzSwapStatusException
from boltz_client.mempool import MempoolBlockHeightException
from loguru import logger
from lnbits.core.crud import get_wallet
from lnbits.core.models import Payment
from lnbits.core.services import check_transaction_status
from lnbits.core.services import check_transaction_status, fee_reserve
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .boltz import (
create_claim_tx,
create_refund_tx,
get_swap_status,
start_confirmation_listener,
start_onchain_listener,
)
from .crud import (
create_reverse_submarine_swap,
get_all_pending_reverse_submarine_swaps,
get_all_pending_submarine_swaps,
get_reverse_submarine_swap,
get_auto_reverse_submarine_swap_by_wallet,
get_submarine_swap,
update_swap_status,
)
"""
testcases for boltz startup
A. normal swaps
1. test: create -> kill -> start -> startup invoice listeners -> pay onchain funds -> should complete
2. test: create -> kill -> pay onchain funds -> start -> startup check -> should complete
3. test: create -> kill -> mine blocks and hit timeout -> start -> should go timeout/failed
4. test: create -> kill -> pay to less onchain funds -> mine blocks hit timeout -> start lnbits -> should be refunded
B. reverse swaps
1. test: create instant -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should claim/complete
2. test: create instant -> kill -> no lockup -> start lnbits -> should start onchain listener -> boltz does lockup -> should claim/complete (difficult to test)
3. test: create -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should start tx listener -> after confirmation -> should claim/complete
4. test: create -> kill -> boltz does lockup -> confirmed -> start lnbits -> should claim/complete
5. test: create -> kill -> boltz does lockup -> hit timeout -> boltz refunds -> start -> should timeout
"""
async def check_for_pending_swaps():
try:
swaps = await get_all_pending_submarine_swaps()
reverse_swaps = await get_all_pending_reverse_submarine_swaps()
if len(swaps) > 0 or len(reverse_swaps) > 0:
logger.debug(f"Boltz - startup swap check")
except:
# database is not created yet, do nothing
return
if len(swaps) > 0:
logger.debug(f"Boltz - {len(swaps)} pending swaps")
for swap in swaps:
try:
swap_status = get_swap_status(swap)
# should only happen while development when regtest is reset
if swap_status.exists is False:
logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
await update_swap_status(swap.id, "failed")
continue
payment_status = await check_transaction_status(
swap.wallet, swap.payment_hash
)
if payment_status.paid:
logger.debug(
f"Boltz - swap: {swap.boltz_id} got paid while offline."
)
await update_swap_status(swap.id, "complete")
else:
if swap_status.hit_timeout:
if not swap_status.has_lockup:
logger.debug(
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
)
await update_swap_status(swap.id, "timeout")
else:
logger.debug(f"Boltz - refunding swap: {swap.id}...")
await create_refund_tx(swap)
await update_swap_status(swap.id, "refunded")
except Exception as exc:
logger.error(f"Boltz - swap: {swap.id} - {str(exc)}")
if len(reverse_swaps) > 0:
logger.debug(f"Boltz - {len(reverse_swaps)} pending reverse swaps")
for reverse_swap in reverse_swaps:
try:
swap_status = get_swap_status(reverse_swap)
if swap_status.exists is False:
logger.debug(
f"Boltz - reverse_swap: {reverse_swap.boltz_id} does not exist."
)
await update_swap_status(reverse_swap.id, "failed")
continue
# if timeout hit, boltz would have already refunded
if swap_status.hit_timeout:
logger.debug(
f"Boltz - reverse_swap: {reverse_swap.boltz_id} timeout."
)
await update_swap_status(reverse_swap.id, "timeout")
continue
if not swap_status.has_lockup:
# start listener for onchain address
logger.debug(
f"Boltz - reverse_swap: {reverse_swap.boltz_id} restarted onchain address listener."
)
await start_onchain_listener(reverse_swap)
continue
if reverse_swap.instant_settlement or swap_status.confirmed:
await create_claim_tx(reverse_swap, swap_status.lockup)
else:
logger.debug(
f"Boltz - reverse_swap: {reverse_swap.boltz_id} restarted confirmation listener."
)
await start_confirmation_listener(reverse_swap, swap_status.lockup)
except Exception as exc:
logger.error(f"Boltz - reverse swap: {reverse_swap.id} - {str(exc)}")
from .models import CreateReverseSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap
from .utils import create_boltz_client, execute_reverse_swap
async def wait_for_paid_invoices():
@ -136,19 +32,149 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
if "boltz" != payment.extra.get("tag"):
await check_for_auto_swap(payment)
if payment.extra.get("tag") != "boltz":
# not a boltz invoice
return
await payment.set_pending(False)
swap_id = payment.extra.get("swap_id")
swap = await get_submarine_swap(swap_id)
if not swap:
logger.error(f"swap_id: {swap_id} not found.")
return
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 - 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")

View File

@ -1,242 +1,93 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="About Boltz"
:content-inset-level="0.5"
>
<q-card>
<q-card>
<q-card-section>
<img
src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg"
alt=""
/>
<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">
Boltz.exchange: Do onchain to offchain and vice-versa swaps
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>
Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange
API<br />
</p>
<p>
Link :
<a class="text-secondary" target="_blank" href="https://boltz.exchange"
Link:
<a target="_blank" href="https://boltz.exchange"
>https://boltz.exchange
</a>
</p>
<p>
<br />
README:
<a
class="text-secondary"
target="_blank"
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltz"
>More details</a
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
>read more</a
>
</p>
<p>
<small
>Created by,
<a
class="text-secondary"
target="_blank"
href="https://github.com/dni"
>dni</a
></small
>Extension created by,
<a target="_blank" href="https://github.com/dni">dni</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
</q-card>
<q-card>
<q-card-section>
<h3 class="text-subtitle1 q-my-none">
<b>Fee Information</b>
</h3>
<span>
Every swap consists of 2 onchain transactions, lockup and claim / refund,
routing fees and a Boltz fee of <b>0.5%</b>.
</span>
</q-card-section>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
label="Fee example: Lightning -> Onchain"
: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>
You want to swap out 100.000 sats, Lightning to Onchain:
<ul style="padding-left: 12px">
<li>Onchain lockup tx fee: ~500 sats</li>
<li>Onchain claim tx fee: 1000 sats (hardcoded)</li>
<li>Routing fees (paid by you): unknown</li>
<li>Boltz fees: 500 sats</li>
<li>Fees total: 2000 sats + routing fees</li>
<li>You receive: 98.000 sats</li>
</ul>
<p>
onchain_amount_received = amount - (amount * boltz_fee / 100) -
lockup_fee - claim_fee
</p>
<p>98.000 = 100.000 - 500 - 500 - 1000</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="POST swap/reverse"
group="extras"
icon="swap_vertical_circle"
label="Fee example: Onchain -> Lightning"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span>
/boltz/api/v1/swap/reverse</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"wallet": &lt;string&gt;, "onchain_address": &lt;string&gt;,
"amount": &lt;integer&gt;, "instant_settlement":
&lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON create a reverse-submarine swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ root_url }}/boltz/api/v1/swap/reverse -H "X-Api-Key:
{{ user.wallets[0].adminkey }}"
</code>
You want to swap in 100.000 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: ~500 sats</li>
<li>Routing fees (paid by boltz): unknown</li>
<li>Boltz fees: 500 sats (0.5%)</li>
<li>Fees total: 1000 sats + lockup_fee</li>
<li>You pay onchain: 101.000 sats + lockup_fee</li>
<li>You receive lightning: 100.000 sats</li>
</ul>
<p>
onchain_payment + lockup_fee = amount + (amount * boltz_fee / 100) +
claim_fee + lockup_fee
</p>
<p>101.000 + lockup_fee = 100.000 + 500 + 500 + lockup_fee</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap">
<q-card>
<q-card-section>
<code><span class="text-light-blue">GET</span> /boltz/api/v1/swap</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON list of submarine swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST swap">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span> /boltz/api/v1/swap</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"wallet": &lt;string&gt;, "refund_address": &lt;string&gt;,
"amount": &lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON create a submarine swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap/refund">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span>
/boltz/api/v1/swap/refund/{swap_id}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON submarine swap</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/refund/{swap_id} -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap/status">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">POST</span>
/boltz/api/v1/swap/status/{swap_id}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (text/plain)
</h5>
<code>swap status</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/status/{swap_id} -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET swap/check">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/boltz/api/v1/swap/check</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>JSON pending swaps</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/check -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET boltz-config">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/boltz/api/v1/swap/boltz</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (text/plain)
</h5>
<code>JSON boltz config</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/boltz -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET mempool-url">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/boltz/api/v1/swap/mempool</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (text/plain)
</h5>
<code>mempool url</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ root_url }}/boltz/api/v1/swap/mempool -H "X-Api-Key:
{{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
</q-card>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,531 +1,19 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
label="Swap (In)"
unelevated
color="primary"
@click="submarineSwapDialog.show = true"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send onchain funds offchain (BTC -> LN)
</q-tooltip>
</q-btn>
<q-btn
label="Reverse Swap (Out)"
unelevated
color="primary"
@click="reverseSubmarineSwapDialog.show = true"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send offchain funds to onchain address (LN -> BTC)
</q-tooltip>
</q-btn>
<q-btn
label="Check Swaps"
icon="cached"
unelevated
color="primary"
@click="checkSwaps"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Check all pending swaps if they can be refunded.
</q-tooltip>
</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Swaps (In)</h5>
<div 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-auto">
<q-btn flat color="grey" @click="exportSubmarineSwapCSV"
>Export to CSV</q-btn
>
<div class="col-12 col-md-4 q-gutter-y-md">
{% include "boltz/_api_docs.html" %}
</div>
</div>
<q-table
dense
flat
:data="submarineSwaps"
row-key="id"
:columns="submarineSwapTable.columns"
:pagination.sync="submarineSwapTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open swap onchain details</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="info"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openStatusDialog(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open swap status info</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Reverse Swaps (Out)</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportReverseSubmarineSwapCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="reverseSubmarineSwaps"
row-key="id"
:columns="reverseSubmarineSwapTable.columns"
:pagination.sync="reverseSubmarineSwapTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td style="width: 10%">
<q-btn
unelevated
dense
size="xs"
icon="info"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openStatusDialog(props.row.id, true)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open swap status info</q-tooltip
>
</q-btn>
<q-btn
unelevated
dense
size="xs"
icon="flip_to_front"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openMempool(props.row.id)"
>
<q-tooltip
class="bg-grey-8"
anchor="bottom left"
self="top left"
>open tx on mempool.space</q-tooltip
>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<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>
{% include "boltz/_submarineSwapDialog.html" %} {% include
"boltz/_reverseSubmarineSwapDialog.html" %} {% include
"boltz/_autoReverseSwapDialog.html" %} {% include "boltz/_qrDialog.html" %} {%
include "boltz/_statusDialog.html" %}
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
@ -539,6 +27,7 @@
boltzConfig: {},
submarineSwaps: [],
reverseSubmarineSwaps: [],
autoReverseSubmarineSwaps: [],
statuses: [],
submarineSwapDialog: {
show: false,
@ -550,6 +39,13 @@
instant_settlement: true
}
},
autoReverseSubmarineSwapDialog: {
show: false,
data: {
balance: 100,
instant_settlement: true
}
},
qrCodeDialog: {
show: false,
data: {}
@ -558,40 +54,36 @@
show: false,
data: {}
},
allStatusDialog: {
show: false,
data: {}
},
allStatusTable: {
columns: [
{
name: 'swap_id',
align: 'left',
label: 'swap_id',
label: 'Swap ID',
field: 'swap_id'
},
{
name: 'status',
align: 'left',
label: 'status',
label: 'Status',
field: 'message'
},
{
name: 'boltz',
align: 'left',
label: 'boltz',
label: 'Boltz',
field: 'boltz'
},
{
name: 'mempool',
align: 'left',
label: 'mempool',
label: 'Mempool',
field: 'mempool'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'block height',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
@ -599,12 +91,60 @@
rowsPerPage: 10
}
},
autoReverseSubmarineSwapTable: {
columns: [
{
name: 'time',
align: 'left',
label: 'Time',
field: 'time',
sortable: true,
format: function (val, row) {
return new Date(val * 1000).toUTCString()
}
},
{
name: 'wallet',
align: 'left',
label: 'Wallet',
field: data => {
let wallet = _.findWhere(this.g.user.wallets, {
id: data.wallet
})
if (wallet) {
return wallet.name
}
}
},
{
name: 'balance',
align: 'left',
label: 'Balance',
field: 'balance'
},
{
name: 'amount',
align: 'left',
label: 'Amount',
field: 'amount'
},
{
name: 'onchain_address',
align: 'left',
label: 'Onchain address',
field: 'onchain_address'
}
],
pagination: {
rowsPerPage: 10
}
},
reverseSubmarineSwapTable: {
columns: [
{
name: 'time',
align: 'left',
label: 'time',
label: 'Time',
field: 'time',
sortable: true,
format: function (val, row) {
@ -614,7 +154,7 @@
{
name: 'wallet',
align: 'left',
label: 'wallet',
label: 'Wallet',
field: data => {
let wallet = _.findWhere(this.g.user.wallets, {
id: data.wallet
@ -627,25 +167,25 @@
{
name: 'status',
align: 'left',
label: 'status',
label: 'Status',
field: 'status'
},
{
name: 'boltz_id',
align: 'left',
label: 'boltz id',
label: 'Boltz ID',
field: 'boltz_id'
},
{
name: 'onchain_amount',
align: 'left',
label: 'onchain amount',
label: 'Onchain amount',
field: 'onchain_amount'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'timeout block height',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
@ -658,7 +198,7 @@
{
name: 'time',
align: 'left',
label: 'time',
label: 'Time',
field: 'time',
sortable: true,
format: function (val, row) {
@ -668,7 +208,7 @@
{
name: 'wallet',
align: 'left',
label: 'wallet',
label: 'Wallet',
field: data => {
let wallet = _.findWhere(this.g.user.wallets, {
id: data.wallet
@ -681,25 +221,25 @@
{
name: 'status',
align: 'left',
label: 'status',
label: 'Status',
field: 'status'
},
{
name: 'boltz_id',
align: 'left',
label: 'boltz id',
label: 'Boltz ID',
field: 'boltz_id'
},
{
name: 'expected_amount',
align: 'left',
label: 'expected amount',
label: 'Expected amount',
field: 'expected_amount'
},
{
name: 'timeout_block_height',
align: 'left',
label: 'timeout block height',
label: 'Timeout block height',
field: 'timeout_block_height'
}
],
@ -711,11 +251,10 @@
},
methods: {
getLimits() {
const cfg = this.boltzConfig.data
if (cfg) {
if (this.boltzConfig) {
return {
min: cfg.limits.minimal,
max: cfg.limits.maximal
min: this.boltzConfig.minimal,
max: this.boltzConfig.maximal
}
}
return {
@ -753,6 +292,19 @@
data.amount > limits.max
)
},
disableAutoReverseSubmarineSwapDialog() {
const data = this.autoReverseSubmarineSwapDialog.data
let limits = this.getLimits()
return (
data.onchain_address == null ||
data.onchain_address.search(
/^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/
) !== 0 ||
data.wallet == null ||
data.amount < limits.min ||
data.amount > limits.max
)
},
downloadRefundFile(swapId) {
let swap = _.findWhere(this.submarineSwaps, {id: swapId})
let json = {
@ -816,6 +368,7 @@
swap_id: swap_id,
wallet: res.data.wallet,
boltz: res.data.boltz,
status: res.data.status,
mempool: res.data.mempool,
timeout_block_height: res.data.timeout_block_height,
date: new Date().toUTCString()
@ -847,12 +400,6 @@
data: {}
}
},
resetAllStatusDialog() {
this.allStatusDialog = {
show: false,
data: {}
}
},
resetSubmarineSwapDialog() {
this.submarineSwapDialog = {
show: false,
@ -865,6 +412,12 @@
data: {}
}
},
resetAutoReverseSubmarineSwapDialog() {
this.autoReverseSubmarineSwapDialog = {
show: false,
data: {}
}
},
sendReverseSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.reverseSubmarineSwapDialog.data.wallet
@ -872,6 +425,13 @@
let data = this.reverseSubmarineSwapDialog.data
this.createReverseSubmarineSwap(wallet, data)
},
sendAutoReverseSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.autoReverseSubmarineSwapDialog.data.wallet
})
let data = this.autoReverseSubmarineSwapDialog.data
this.createAutoReverseSubmarineSwap(wallet, data)
},
sendSubmarineSwapFormData() {
let wallet = _.findWhere(this.g.user.wallets, {
id: this.submarineSwapDialog.data.wallet
@ -891,6 +451,12 @@
this.reverseSubmarineSwaps
)
},
exportAutoReverseSubmarineSwapCSV() {
LNbits.utils.exportCSV(
this.autoReverseSubmarineSwapTable.columns,
this.autoReverseSubmarineSwaps
)
},
createSubmarineSwap(wallet, data) {
LNbits.api
.request(
@ -924,6 +490,40 @@
LNbits.utils.notifyApiError(error)
})
},
createAutoReverseSubmarineSwap(wallet, data) {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap/reverse/auto',
this.g.user.wallets[0].adminkey,
data
)
.then(res => {
this.autoReverseSubmarineSwaps.unshift(res.data)
this.resetAutoReverseSubmarineSwapDialog()
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
deleteAutoReverseSwap(swap_id) {
LNbits.api
.request(
'DELETE',
'/boltz/api/v1/swap/reverse/auto/' + swap_id,
this.g.user.wallets[0].adminkey
)
.then(res => {
let i = this.autoReverseSubmarineSwaps.findIndex(
swap => swap.id === swap_id
)
this.autoReverseSubmarineSwaps.splice(i, 1)
})
.catch(error => {
console.log(error)
LNbits.utils.notifyApiError(error)
})
},
getSubmarineSwap() {
LNbits.api
.request(
@ -952,6 +552,20 @@
LNbits.utils.notifyApiError(error)
})
},
getAutoReverseSubmarineSwap() {
LNbits.api
.request(
'GET',
'/boltz/api/v1/swap/reverse/auto?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.autoReverseSubmarineSwaps = response.data
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getMempool() {
LNbits.api
.request('GET', '/boltz/api/v1/swap/mempool')
@ -967,26 +581,7 @@
LNbits.api
.request('GET', '/boltz/api/v1/swap/boltz')
.then(res => {
this.boltzConfig = res
})
.catch(error => {
console.log('error', error)
LNbits.utils.notifyApiError(error)
})
},
checkSwaps() {
LNbits.api
.request(
'POST',
'/boltz/api/v1/swap/check',
this.g.user.wallets[0].adminkey
)
.then(res => {
this.allStatusDialog.data = {
swaps: _.where(res.data, {reverse: false}),
reverse_swaps: _.where(res.data, {reverse: true})
}
this.allStatusDialog.show = true
this.boltzConfig = res.data
})
.catch(error => {
console.log('error', error)
@ -999,6 +594,7 @@
this.getBoltzConfig()
this.getSubmarineSwap()
this.getReverseSubmarineSwap()
this.getAutoReverseSubmarineSwap()
}
})
</script>

View File

@ -1,10 +1,25 @@
import asyncio
import calendar
import datetime
from typing import Awaitable
import httpx
from loguru import logger
from boltz_client.boltz import BoltzClient, BoltzConfig
from lnbits.core.services import fee_reserve, get_wallet
from lnbits.core.services import fee_reserve, get_wallet, pay_invoice
from lnbits.settings import settings
from .models import ReverseSubmarineSwap
def create_boltz_client() -> BoltzClient:
config = BoltzConfig(
network=settings.boltz_network,
api_url=settings.boltz_url,
mempool_url=f"{settings.boltz_mempool_space_url}/api",
mempool_ws_url=f"{settings.boltz_mempool_space_url_ws}/api/v1/ws",
referral_id="lnbits",
)
return BoltzClient(config)
async def check_balance(data) -> bool:
@ -23,22 +38,50 @@ def get_timestamp():
return calendar.timegm(date.utctimetuple())
def req_wrap(funcname, *args, **kwargs):
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:
func = getattr(httpx, funcname)
except AttributeError:
logger.error('httpx function not found "%s"' % funcname)
else:
res = func(*args, timeout=30, **kwargs)
res.raise_for_status()
return res
except httpx.RequestError as exc:
msg = f"Unreachable: {exc.request.url!r}."
logger.error(msg)
raise
except httpx.HTTPStatusError as exc:
msg = f"HTTP Status Error: {exc.response.status_code} while requesting {exc.request.url!r}."
logger.error(msg)
logger.error(exc.response.json()["error"])
raise
awaited = await awaitable
await update_swap_status(swap_id, "complete")
return awaited
except asyncio.exceptions.CancelledError:
"""lnbits process was exited, do nothing and handle it in startup script"""
except:
wstask.cancel()
await update_swap_status(swap_id, "failed")
return asyncio.create_task(_pay_invoice(awaitable))

View File

@ -1,11 +1,10 @@
from urllib.parse import urlparse
from fastapi import Request
from fastapi.params import Depends
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import Payment, User
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import boltz_ext, boltz_renderer
@ -16,7 +15,6 @@ templates = Jinja2Templates(directory="templates")
@boltz_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
root_url = urlparse(str(request.url)).netloc
wallet_ids = [wallet.id for wallet in user.wallets]
return boltz_renderer().TemplateResponse(
"boltz/index.html",
{"request": request, "user": user.dict(), "root_url": root_url},

View File

@ -1,34 +1,23 @@
from datetime import datetime
from http import HTTPStatus
from typing import List
import httpx
from fastapi import status
from fastapi.encoders import jsonable_encoder
from fastapi.param_functions import Body
from fastapi.params import Depends, Query
from loguru import logger
from pydantic import BaseModel
from fastapi import Depends, Query, status
from starlette.exceptions import HTTPException
from starlette.requests import Request
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import settings
from . import boltz_ext
from .boltz import (
create_refund_tx,
create_reverse_swap,
create_swap,
get_boltz_pairs,
get_swap_status,
)
from .crud import (
create_auto_reverse_submarine_swap,
create_reverse_submarine_swap,
create_submarine_swap,
get_pending_reverse_submarine_swaps,
get_pending_submarine_swaps,
delete_auto_reverse_submarine_swap,
get_auto_reverse_submarine_swap_by_wallet,
get_auto_reverse_submarine_swaps,
get_reverse_submarine_swap,
get_reverse_submarine_swaps,
get_submarine_swap,
@ -36,12 +25,14 @@ from .crud import (
update_swap_status,
)
from .models import (
AutoReverseSubmarineSwap,
CreateAutoReverseSubmarineSwap,
CreateReverseSubmarineSwap,
CreateSubmarineSwap,
ReverseSubmarineSwap,
SubmarineSwap,
)
from .utils import check_balance
from .utils import check_balance, create_boltz_client, execute_reverse_swap
@boltz_ext.get(
@ -76,17 +67,8 @@ async def api_submarineswap(
):
wallet_ids = [g.wallet.id]
if all_wallets:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
for swap in await get_pending_submarine_swaps(wallet_ids):
swap_status = get_swap_status(swap)
if swap_status.hit_timeout:
if not swap_status.has_lockup:
logger.warning(
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
)
await update_swap_status(swap.id, "timeout")
user = await get_user(g.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [swap.dict() for swap in await get_submarine_swaps(wallet_ids)]
@ -109,35 +91,29 @@ async def api_submarineswap(
},
},
)
async def api_submarineswap_refund(
swap_id: str,
g: WalletTypeInfo = Depends(require_admin_key),
):
if swap_id == None:
async def api_submarineswap_refund(swap_id: str):
if not swap_id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="swap_id missing"
)
swap = await get_submarine_swap(swap_id)
if swap == None:
if not swap:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
)
if swap.status != "pending":
raise HTTPException(
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="swap is not pending."
)
try:
await create_refund_tx(swap)
except httpx.RequestError as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Unreachable: {exc.request.url!r}.",
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,
)
except Exception as exc:
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
await update_swap_status(swap.id, "refunded")
return swap
@ -153,37 +129,43 @@ async def api_submarineswap_refund(
""",
response_description="create swap",
response_model=SubmarineSwap,
dependencies=[Depends(require_admin_key)],
responses={
405: {"description": "not allowed method, insufficient balance"},
405: {
"description": "auto reverse swap is active, a swap would immediatly be swapped out again."
},
500: {"description": "boltz error"},
},
)
async def api_submarineswap_create(
data: CreateSubmarineSwap,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
try:
swap_data = await create_swap(data)
except httpx.RequestError as exc:
async def api_submarineswap_create(data: CreateSubmarineSwap):
auto_swap = await get_auto_reverse_submarine_swap_by_wallet(data.wallet)
if auto_swap:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Unreachable: {exc.request.url!r}.",
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
detail="auto reverse swap is active, a swap would immediatly be swapped out again.",
)
except Exception as exc:
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
except httpx.HTTPStatusError as exc:
raise HTTPException(
status_code=exc.response.status_code, detail=exc.response.json()["error"]
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},
)
swap = await create_submarine_swap(swap_data)
return swap.dict()
refund_privkey_wif, swap = client.create_swap(payment_request)
new_swap = await create_submarine_swap(
data, swap, swap_id, refund_privkey_wif, payment_hash
)
return new_swap.dict() if new_swap else None
# REVERSE SWAP
@boltz_ext.get(
"/api/v1/swap/reverse",
name=f"boltz.get /swap/reverse",
summary="get a list of reverse swaps a swap",
summary="get a list of reverse swaps",
description="""
This endpoint gets a list of reverse swaps.
""",
@ -192,13 +174,14 @@ async def api_submarineswap_create(
response_model=List[ReverseSubmarineSwap],
)
async def api_reverse_submarineswap(
g: WalletTypeInfo = Depends(get_key_type), # type:ignore
g: WalletTypeInfo = Depends(get_key_type),
all_wallets: bool = Query(False),
):
wallet_ids = [g.wallet.id]
if all_wallets:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return [swap.dict() for swap in await get_reverse_submarine_swaps(wallet_ids)]
user = await get_user(g.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [swap for swap in await get_reverse_submarine_swaps(wallet_ids)]
@boltz_ext.post(
@ -211,6 +194,7 @@ async def api_reverse_submarineswap(
""",
response_description="create reverse swap",
response_model=ReverseSubmarineSwap,
dependencies=[Depends(require_admin_key)],
responses={
405: {"description": "not allowed method, insufficient balance"},
500: {"description": "boltz error"},
@ -218,30 +202,88 @@ async def api_reverse_submarineswap(
)
async def api_reverse_submarineswap_create(
data: CreateReverseSubmarineSwap,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
) -> ReverseSubmarineSwap:
if not await check_balance(data):
raise HTTPException(
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Insufficient balance."
)
try:
swap_data, task = await create_reverse_swap(data)
except httpx.RequestError as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Unreachable: {exc.request.url!r}.",
client = create_boltz_client()
claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
amount=data.amount
)
except httpx.HTTPStatusError as exc:
raise HTTPException(
status_code=exc.response.status_code, detail=exc.response.json()["error"]
new_swap = await create_reverse_submarine_swap(
data, claim_privkey_wif, preimage_hex, swap
)
except Exception as exc:
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
await execute_reverse_swap(client, new_swap)
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(
@ -252,65 +294,22 @@ async def api_reverse_submarineswap_create(
This endpoint attempts to get the status of the swap.
""",
response_description="status of swap json",
dependencies=[Depends(require_admin_key)],
responses={
404: {"description": "when swap_id is not found"},
},
)
async def api_swap_status(
swap_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
async def api_swap_status(swap_id: str):
swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap(
swap_id
)
if swap == None:
if not swap:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
)
try:
status = get_swap_status(swap)
except httpx.RequestError as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Unreachable: {exc.request.url!r}.",
)
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
)
return status
@boltz_ext.post(
"/api/v1/swap/check",
name=f"boltz.swap_check",
summary="list all pending swaps",
description="""
This endpoint gives you 2 lists of pending swaps and reverse swaps.
""",
response_description="list of pending swaps",
)
async def api_check_swaps(
g: WalletTypeInfo = Depends(require_admin_key),
all_wallets: bool = Query(False),
):
wallet_ids = [g.wallet.id]
if all_wallets:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
status = []
try:
for swap in await get_pending_submarine_swaps(wallet_ids):
status.append(get_swap_status(swap))
for reverseswap in await get_pending_reverse_submarine_swaps(wallet_ids):
status.append(get_swap_status(reverseswap))
except httpx.RequestError as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Unreachable: {exc.request.url!r}.",
)
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
)
client = create_boltz_client()
status = client.swap_status(swap.boltz_id)
return status
@ -325,14 +324,5 @@ async def api_check_swaps(
response_model=dict,
)
async def api_boltz_config():
try:
res = get_boltz_pairs()
except httpx.RequestError as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Unreachable: {exc.request.url!r}.",
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
return res["pairs"]["BTC/BTC"]
client = create_boltz_client()
return {"minimal": client.limit_minimal, "maximal": client.limit_maximal}

View File

@ -108,9 +108,8 @@
},
methods: {
showNotif: function (userMessage) {
var colour = this.colours[
Math.floor(Math.random() * this.colours.length)
]
var colour =
this.colours[Math.floor(Math.random() * this.colours.length)]
this.$q.notify({
color: colour,
icon: 'chat_bubble_outline',

View File

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

View File

@ -40,19 +40,6 @@ def get_text_item_dict(
elif font_size <= 40:
line_width = 25
# Get font sizes for Gerty mini
if gerty_type.lower() == "mini gerty":
if font_size <= 12:
font_size = 1
if font_size <= 15:
font_size = 1
elif font_size <= 20:
font_size = 2
elif font_size <= 40:
font_size = 2
else:
font_size = 5
# wrap the text
wrapper = textwrap.TextWrapper(width=line_width)
word_list = wrapper.wrap(text=text)
@ -118,7 +105,7 @@ async def get_mining_dashboard(gerty):
text = []
text.append(
get_text_item_dict(
text="Current mining hashrate", font_size=12, gerty_type=gerty.type
text="Current hashrate", font_size=12, gerty_type=gerty.type
)
)
text.append(
@ -152,7 +139,7 @@ async def get_mining_dashboard(gerty):
)
)
text.append(
get_text_item_dict(text=progress, font_size=60, gerty_type=gerty.type)
get_text_item_dict(text=progress, font_size=40, gerty_type=gerty.type)
)
areas.append(text)
@ -190,7 +177,7 @@ async def get_mining_dashboard(gerty):
text="{0}{1}%".format(
"+" if difficultyChange > 0 else "", round(difficultyChange, 2)
),
font_size=60,
font_size=40,
gerty_type=gerty.type,
)
)
@ -315,7 +302,7 @@ def get_next_update_time(sleep_time_seconds: int = 0, utc_offset: int = 0):
local_refresh_time = next_refresh_time + timedelta(hours=utc_offset)
return "{0} {1}".format(
"I'll wake up at" if gerty_should_sleep(utc_offset) else "Next update at",
local_refresh_time.strftime("%H:%M on %e %b %Y"),
local_refresh_time.strftime("%H:%M"),
)
@ -459,7 +446,7 @@ async def get_screen_data(screen_num: int, screens_list: list, gerty):
response = await client.get(url)
text.append(
get_text_item_dict(
text=url,
text=make_url_readable(url),
font_size=20,
gerty_type=gerty.type,
)
@ -475,7 +462,7 @@ async def get_screen_data(screen_num: int, screens_list: list, gerty):
text = []
text.append(
get_text_item_dict(
text=url,
text=make_url_readable(url),
font_size=20,
gerty_type=gerty.type,
)
@ -496,6 +483,13 @@ async def get_screen_data(screen_num: int, screens_list: list, gerty):
areas.append(await get_onchain_stat(screen_slug, gerty))
elif screen_slug == "onchain_block_height":
text = []
text.append(
get_text_item_dict(
text="Block Height",
font_size=20,
gerty_type=gerty.type,
)
)
text.append(
get_text_item_dict(
text=format_number(await get_mempool_info("tip_height", gerty)),
@ -697,7 +691,6 @@ async def get_onchain_stat(stat_slug: str, gerty):
or stat_slug == "onchain_difficulty_blocks_remaining"
or stat_slug == "onchain_difficulty_epoch_time_remaining"
):
async with httpx.AsyncClient() as client:
r = await get_mempool_info("difficulty_adjustment", gerty)
if stat_slug == "onchain_difficulty_epoch_progress":
stat = round(r["progressPercent"])
@ -764,18 +757,31 @@ async def get_onchain_stat(stat_slug: str, gerty):
async def get_onchain_dashboard(gerty):
areas = []
if isinstance(gerty.mempool_endpoint, str):
async with httpx.AsyncClient() as client:
text = []
stat = (format_number(await get_mempool_info("tip_height", gerty)),)
text.append(
get_text_item_dict(
text="Current block height", font_size=12, gerty_type=gerty.type
)
)
text.append(
get_text_item_dict(text=stat[0], font_size=40, gerty_type=gerty.type)
)
areas.append(text)
r = await get_mempool_info("difficulty_adjustment", gerty)
text = []
stat = round(r["progressPercent"])
text.append(
get_text_item_dict(
text="Progress through epoch", font_size=12, gerty_type=gerty.type
text="Progress through current epoch",
font_size=12,
gerty_type=gerty.type,
)
)
text.append(
get_text_item_dict(
text="{0}%".format(stat), font_size=60, gerty_type=gerty.type
text="{0}%".format(stat), font_size=40, gerty_type=gerty.type
)
)
areas.append(text)
@ -788,9 +794,7 @@ async def get_onchain_dashboard(gerty):
text="Date of next adjustment", font_size=12, gerty_type=gerty.type
)
)
text.append(
get_text_item_dict(text=dt, font_size=20, gerty_type=gerty.type)
)
text.append(get_text_item_dict(text=dt, font_size=20, gerty_type=gerty.type))
areas.append(text)
text = []
@ -803,23 +807,7 @@ async def get_onchain_dashboard(gerty):
text.append(
get_text_item_dict(
text="{0}".format(format_number(stat)),
font_size=60,
gerty_type=gerty.type,
)
)
areas.append(text)
text = []
stat = r["remainingTime"]
text.append(
get_text_item_dict(
text="Time until adjustment", font_size=12, gerty_type=gerty.type
)
)
text.append(
get_text_item_dict(
text=get_time_remaining(stat / 1000, 4),
font_size=20,
font_size=40,
gerty_type=gerty.type,
)
)
@ -944,3 +932,7 @@ async def get_mempool_stat(stat_slug: str, gerty):
)
)
return text
def make_url_readable(url: str):
return url.replace("https://", "").replace("http://", "").strip("/")

View File

@ -224,21 +224,17 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
this.gertyname = this.gerty[i].settings.name
}
if (this.gerty[i].screen.group == 'fun_satoshi_quotes') {
this.fun_satoshi_quotes['quote'] = this.gerty[
i
].screen.areas[0][0].value
this.fun_satoshi_quotes['date'] = this.gerty[
i
].screen.areas[0][1].value
this.fun_satoshi_quotes['quote'] =
this.gerty[i].screen.areas[0][0].value
this.fun_satoshi_quotes['date'] =
this.gerty[i].screen.areas[0][1].value
this.gertyname = this.gerty[i].settings.name
}
if (this.gerty[i].screen.group == 'fun_exchange_market_rate') {
this.fun_exchange_market_rate['unit'] = this.gerty[
i
].screen.areas[0][0].value
this.fun_exchange_market_rate['amount'] = this.gerty[
i
].screen.areas[0][1].value
this.fun_exchange_market_rate['unit'] =
this.gerty[i].screen.areas[0][0].value
this.fun_exchange_market_rate['amount'] =
this.gerty[i].screen.areas[0][1].value
this.gertyname = this.gerty[i].settings.name
}
}

View File

@ -59,17 +59,16 @@
>
<q-tooltip>Launch software Gerty</q-tooltip>
</q-btn>
<q-btn
unelevated
flat
dense
size="xs"
icon="code"
color="pink"
type="a"
:href="props.row.gertyJson"
target="_blank"
@click="openSettingsModal(props.row.gertyJson)"
icon="perm_data_setting"
color="primary"
>
<q-tooltip>View Gerty API</q-tooltip>
<q-tooltip> Gerty Settings </q-tooltip>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
@ -130,6 +129,35 @@
</q-card>
</div>
<q-dialog
v-model="settingsDialog.show"
deviceition="top"
@hide="closeFormDialog"
>
<q-card
style="width: 700px; max-width: 80vw"
class="q-pa-lg lnbits__dialog-card"
>
<div class="text-h6 text-center">Gerty API URL</div>
<center>
<q-btn
dense
outline
unelevated
color="primary"
size="md"
@click="copyText(settingsDialog.apiUrl, 'Link copied to clipboard!')"
>{% raw %}{{settingsDialog.apiUrl}}{% endraw %}<q-tooltip>
Click to Copy URL
</q-tooltip>
</q-btn>
</center>
<div class="text-subtitle2">
<small> </small>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendFormDataGerty" class="q-gutter-md">
@ -157,6 +185,13 @@
val="xs"
label="Fiat to BTC price"
></q-checkbox>
<q-checkbox
class="q-pl-md"
size="xs"
v-model="formDialog.data.display_preferences.onchain_block_height"
val="xs"
label="Block Height"
></q-checkbox>
<q-checkbox
class="q-pl-md"
size="xs"
@ -232,7 +267,8 @@
multiple
hide-dropdown-icon
new-value-mode="add-unique"
label="Urls to watch."
max-values="4"
label="URLs to watch."
>
<q-tooltip>Hit enter to add values</q-tooltip>
</q-select>
@ -300,7 +336,11 @@
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.gerty = ['/gerty/', obj.id].join('')
obj.gertyJson = ['/gerty/api/v1/gerty/pages/', obj.id, '/0'].join('')
obj.gertyJson = [
window.location.origin,
'/gerty/api/v1/gerty/pages/',
obj.id
].join('')
return obj
}
@ -514,6 +554,10 @@
rowsPerPage: 10
}
},
settingsDialog: {
show: false,
data: {}
},
formDialog: {
show: false,
data: {
@ -572,8 +616,8 @@
self.toggleStates.onchain
self.formDialog.data.display_preferences.onchain_difficulty_retarget_date =
self.toggleStates.onchain
self.formDialog.data.display_preferences.onchain_difficulty_blocks_remaining = !self
.toggleStates.onchain
self.formDialog.data.display_preferences.onchain_difficulty_blocks_remaining =
!self.toggleStates.onchain
self.formDialog.data.display_preferences.onchain_difficulty_epoch_time_remaining =
self.toggleStates.onchain
self.formDialog.data.display_preferences.onchain_block_height =
@ -610,6 +654,10 @@
})
})
},
openSettingsModal: function (apiUrl) {
this.settingsDialog.apiUrl = apiUrl
this.settingsDialog.show = true
},
updateformDialog: function (formId) {
var gerty = _.findWhere(this.gertys, {id: formId})
this.formDialog.data.id = gerty.id
@ -762,29 +810,6 @@
this.formDialog.data.display_preferences.url_checker = false
}
}
},
toggleStates: {
handler(toggleStatesValue) {
// Switch all the toggles in each section to the relevant state
for (const [toggleKey, toggleValue] of Object.entries(
toggleStatesValue
)) {
if (this.oldToggleStates[toggleKey] !== toggleValue) {
for (const [dpKey, dpValue] of Object.entries(
this.formDialog.data.display_preferences
)) {
if (dpKey.indexOf(toggleKey) === 0) {
this.formDialog.data.display_preferences[dpKey] = toggleValue
}
}
}
}
// This is a weird hack we have to use to get VueJS to persist the previous toggle state between
// watches. VueJS passes the old and new values by reference so when comparing objects they
// will have the same values unless we do this
this.oldToggleStates = JSON.parse(JSON.stringify(toggleStatesValue))
},
deep: true
}
}
})

View File

@ -77,7 +77,7 @@ async def delete_jukebox(juke_id: str):
#####################################PAYMENTS
async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment:
async def create_jukebox_payment(data: CreateJukeboxPayment) -> CreateJukeboxPayment:
await db.execute(
"""
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
@ -87,7 +87,7 @@ async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment:
)
jukebox_payment = await get_jukebox_payment(data.payment_hash)
assert jukebox_payment, "Newly created Jukebox Payment couldn't be retrieved"
return jukebox_payment
return data
async def update_jukebox_payment(

View File

@ -1,6 +1,7 @@
from fastapi.param_functions import Query
from typing import Optional
from fastapi import Query
from pydantic import BaseModel
from pydantic.main import BaseModel
class CreateJukeLinkData(BaseModel):
@ -21,13 +22,13 @@ class Jukebox(BaseModel):
user: str
title: str
wallet: str
inkey: str
inkey: Optional[str]
sp_user: str
sp_secret: str
sp_access_token: str
sp_refresh_token: str
sp_device: str
sp_playlists: str
sp_access_token: Optional[str]
sp_refresh_token: Optional[str]
sp_device: Optional[str]
sp_playlists: Optional[str]
price: int
profit: int

View File

@ -255,7 +255,8 @@ new Vue({
},
createJukebox() {
self = this
self.jukeboxDialog.data.sp_playlists = self.jukeboxDialog.data.sp_playlists.join()
self.jukeboxDialog.data.sp_playlists =
self.jukeboxDialog.data.sp_playlists.join()
self.updateDB()
self.jukeboxDialog.show = false
self.getJukeboxes()

View File

@ -31,6 +31,8 @@ async def connect_to_jukebox(request: Request, juke_id):
)
devices = await api_get_jukebox_device_check(juke_id)
deviceConnected = False
assert jukebox.sp_device
assert jukebox.sp_playlists
for device in devices["devices"]:
if device["id"] == jukebox.sp_device.split("-")[1]:
deviceConnected = True

View File

@ -114,6 +114,7 @@ async def api_get_jukebox_song(
tracks = []
async with httpx.AsyncClient() as client:
try:
assert jukebox.sp_access_token
r = await client.get(
"https://api.spotify.com/v1/playlists/" + sp_playlist + "/tracks",
timeout=40,
@ -194,6 +195,7 @@ async def api_get_jukebox_device_check(
if not jukebox:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
async with httpx.AsyncClient() as client:
assert jukebox.sp_access_token
rDevice = await client.get(
"https://api.spotify.com/v1/me/player/devices",
timeout=40,
@ -229,6 +231,7 @@ async def api_get_jukebox_invoice(juke_id, song_id):
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
try:
assert jukebox.sp_device
devices = await api_get_jukebox_device_check(juke_id)
deviceConnected = False
for device in devices["devices"]:
@ -291,6 +294,7 @@ async def api_get_jukebox_invoice_paid(
jukebox_payment = await get_jukebox_payment(pay_hash)
if jukebox_payment and jukebox_payment.paid:
async with httpx.AsyncClient() as client:
assert jukebox.sp_access_token
r = await client.get(
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
timeout=40,
@ -308,6 +312,7 @@ async def api_get_jukebox_invoice_paid(
if r.status_code == 204 or isPlaying == False:
async with httpx.AsyncClient() as client:
uri = ["spotify:track:" + song_id]
assert jukebox.sp_device
r = await client.put(
"https://api.spotify.com/v1/me/player/play?device_id="
+ jukebox.sp_device.split("-")[1],
@ -339,6 +344,8 @@ async def api_get_jukebox_invoice_paid(
)
elif r.status_code == 200:
async with httpx.AsyncClient() as client:
assert jukebox.sp_access_token
assert jukebox.sp_device
r = await client.post(
"https://api.spotify.com/v1/me/player/queue?uri=spotify%3Atrack%3A"
+ song_id
@ -399,6 +406,7 @@ async def api_get_jukebox_currently(
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
async with httpx.AsyncClient() as client:
try:
assert jukebox.sp_access_token
r = await client.get(
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
timeout=40,

View File

@ -807,11 +807,12 @@
updatedData
)
.then(function (response) {
self.lnurldeviceLinks = _.reject(self.lnurldeviceLinks, function (
obj
) {
self.lnurldeviceLinks = _.reject(
self.lnurldeviceLinks,
function (obj) {
return obj.id === updatedData.id
})
}
)
self.lnurldeviceLinks.push(maplnurldevice(response.data))
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()

View File

@ -1,9 +1,281 @@
<h1>Market</h1>
<h2>A movable market stand</h2>
Make a list of products to sell, point the list to an relay (or many), stack sats.
Market is a movable market stand, for anon transactions. You then give permission for an relay to list those products. Delivery addresses are sent through the Lightning Network.
<img src="https://i.imgur.com/P1tvBSG.png">
## Nostr Diagon Alley protocol (for resilient marketplaces)
#### Original protocol https://github.com/lnbits/Diagon-Alley
> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR!
In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked.
A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events.
#### For further information about NOSTR, see https://github.com/nostr-protocol/nostr
## Terms
* `merchant` - seller of products with NOSTR key-pair
* `customer` - buyer of products with NOSTR key-pair
* `product` - item for sale by the `merchant`
* `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls)
* `marketplace` - clientside software for searching `stalls` and purchasing `products`
## Diagon Alley Clients
### Merchant admin
Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`.
The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events.
### Marketplace
`Marketplace` software should be entirely clientside, either as a stand-alone app, or as a purely frontend webpage. A `customer` subscribes to different merchant NOSTR public keys, and those `merchants` `stalls` and `products` become listed and searchable. The marketplace client is like any other ecommerce site, with basket and checkout. `Marketplaces` may also wish to include a `customer` support area for direct message communication with `merchants`.
## `Merchant` publishing/updating products (event)
NIP-01 https://github.com/nostr-protocol/nips/blob/master/01.md uses the basic NOSTR event type.
The `merchant` event that publishes and updates product lists
The below json goes in `content` of NIP-01.
Data from newer events should replace data from older events.
`action` types (used to indicate changes):
* `update` element has changed
* `delete` element should be deleted
* `suspend` element is suspended
* `unsuspend` element is unsuspended
```
{
"name": <String, name of merchant>,
"description": <String, description of merchant>,
"currency": <Str, currency used>,
"action": <String, optional action>,
"shipping": [
{
"id": <String, UUID derived from stall ID>,
"zones": <String, CSV of countries/zones>,
"price": <int, cost>,
},
{
"id": <String, UUID derived from stall ID>,
"zones": <String, CSV of countries/zones>,
"price": <int, cost>,
},
{
"id": <String, UUID derived from stall ID>,
"zones": <String, CSV of countries/zones>,
"price": <int, cost>,
}
],
"stalls": [
{
"id": <UUID derived from merchant public-key>,
"name": <String, stall name>,
"description": <String, stall description>,
"categories": <String, CSV of voluntary categories>,
"shipping": <String, CSV of shipping ids>,
"action": <String, optional action>,
"products": [
{
"id": <String, UUID derived from stall ID>,
"name": <String, name of product>,
"description": <String, product description>,
"categories": <String, CSV of voluntary categories>,
"amount": <Int, number of units>,
"price": <Int, cost per unit>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional action>,
},
{
"id": <String, UUID derived from stall ID>,
"name": <String, name of product>,
"description": <String, product description>,
"categories": <String, CSV of voluntary categories>,
"amount": <Int, number of units>,
"price": <Int, cost per unit>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
},
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional action>,
},
]
},
{
"id": <UUID derived from merchant public_key>,
"name": <String, stall name>,
"description": <String, stall description>,
"categories": <String, CSV of voluntary categories>,
"shipping": <String, CSV of shipping ids>,
"action": <String, optional action>,
"products": [
{
"id": <String, UUID derived from stall ID>,
"name": <String, name of product>,
"categories": <String, CSV of voluntary categories>,
"amount": <Int, number of units>,
"price": <Int, cost per unit>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional action>,
}
]
}
]
}
```
As all elements are optional, an `update` `action` to a `product` `image`, may look as simple as:
```
{
"stalls": [
{
"id": <UUID derived from merchant public-key>,
"products": [
{
"id": <String, UUID derived from stall ID>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional action>,
},
]
}
]
}
```
## Checkout events
NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted
The below json goes in `content` of NIP-04.
### Step 1: `customer` order (event)
```
{
"id": <String, UUID derived from sum of product ids + timestamp>,
"name": <String, name of customer>,
"description": <String, description of customer>,
"address": <String, postal address>,
"message": <String, special request>,
"contact": [
"nostr": <String, NOSTR public key>,
"phone": <String, phone number>,
"email": <String, email address>
],
"items": [
{
"id": <String, product ID>,
"quantity": <String, stall name>,
"message": <String, special request>
},
{
"id": <String, product ID>,
"quantity": <String, stall name>,
"message": <String, special request>
},
{
"id": <String, product ID>,
"quantity": <String, stall name>,
"message": <String, special request>
}
}
```
Merchant should verify the sum of product ids + timestamp.
### Step 2: `merchant` request payment (event)
Sent back from the merchant for payment. Any payment option is valid that the merchant can check.
The below json goes in `content` of NIP-04.
`payment_options`/`type` include:
* `url` URL to a payment page, stripe, paypal, btcpayserver, etc
* `btc` onchain bitcoin address
* `ln` bitcoin lightning invoice
* `lnurl` bitcoin lnurl-pay
```
{
"id": <String, UUID derived from sum of product ids + timestamp>,
"message": <String, message to customer>,
"payment_options": [
{
"type": <String, option type>,
"link": <String, url, btc address, ln invoice, etc>
},
{
"type": <String, option type>,
"link": <String, url, btc address, ln invoice, etc>
},
{
"type": <String, option type>,
"link": <String, url, btc address, ln invoice, etc>
}
}
```
### Step 3: `merchant` verify payment/shipped (event)
Once payment has been received and processed.
The below json goes in `content` of NIP-04.
```
{
"id": <String, UUID derived from sum of product ids + timestamp>,
"message": <String, message to customer>,
"paid": <Bool, true/false has received payment>,
"shipped": <Bool, true/false has been shipped>,
}
```
## Customer support events
Customer support is handle over whatever communication method was specified. If communicationg via nostr, NIP-04 is used https://github.com/nostr-protocol/nips/blob/master/04.md.
## Additional
Standard data models can be found here <a href="models.json">here</a>
<h2>API endpoints</h2>
<code>curl -X GET http://YOUR-TOR-ADDRESS</code>

View File

@ -0,0 +1,227 @@
{
"shipping_zones": [
"Free (digital)",
"Worldwide",
"Europe",
"Australia",
"Austria",
"Belgium",
"Brazil",
"Canada",
"Denmark",
"Finland",
"France",
"Germany",
"Greece",
"Hong Kong",
"Hungary",
"Ireland",
"Indonesia",
"Israel",
"Italy",
"Japan",
"Kazakhstan",
"Korea",
"Luxembourg",
"Malaysia",
"Mexico",
"Netherlands",
"New Zealand",
"Norway",
"Poland",
"Portugal",
"Russia",
"Saudi Arabia",
"Singapore",
"Spain",
"Sweden",
"Switzerland",
"Thailand",
"Turkey",
"Ukraine",
"United Kingdom**",
"United States***",
"Vietnam",
"China"
],
"categories": [
"Fashion (clothing and accessories)",
"Health (and beauty)",
"Toys (and baby equipment)",
"Media (Books and CDs)",
"Groceries (Food and Drink)",
"Technology (Phones and Computers)",
"Home (furniture and accessories)",
"Gifts (flowers, cards, etc)",
"Adult",
"Other"
],
"currency": {
"BTC": "Bitcoin",
"SAT": "Bitcoin satoshis",
"AED": "United Arab Emirates Dirham",
"AFN": "Afghan Afghani",
"ALL": "Albanian Lek",
"AMD": "Armenian Dram",
"ANG": "Netherlands Antillean Gulden",
"AOA": "Angolan Kwanza",
"ARS": "Argentine Peso",
"AUD": "Australian Dollar",
"AWG": "Aruban Florin",
"AZN": "Azerbaijani Manat",
"BAM": "Bosnia and Herzegovina Convertible Mark",
"BBD": "Barbadian Dollar",
"BDT": "Bangladeshi Taka",
"BGN": "Bulgarian Lev",
"BHD": "Bahraini Dinar",
"BIF": "Burundian Franc",
"BMD": "Bermudian Dollar",
"BND": "Brunei Dollar",
"BOB": "Bolivian Boliviano",
"BRL": "Brazilian Real",
"BSD": "Bahamian Dollar",
"BTN": "Bhutanese Ngultrum",
"BWP": "Botswana Pula",
"BYN": "Belarusian Ruble",
"BYR": "Belarusian Ruble",
"BZD": "Belize Dollar",
"CAD": "Canadian Dollar",
"CDF": "Congolese Franc",
"CHF": "Swiss Franc",
"CLF": "Unidad de Fomento",
"CLP": "Chilean Peso",
"CNH": "Chinese Renminbi Yuan Offshore",
"CNY": "Chinese Renminbi Yuan",
"COP": "Colombian Peso",
"CRC": "Costa Rican Colón",
"CUC": "Cuban Convertible Peso",
"CVE": "Cape Verdean Escudo",
"CZK": "Czech Koruna",
"DJF": "Djiboutian Franc",
"DKK": "Danish Krone",
"DOP": "Dominican Peso",
"DZD": "Algerian Dinar",
"EGP": "Egyptian Pound",
"ERN": "Eritrean Nakfa",
"ETB": "Ethiopian Birr",
"EUR": "Euro",
"FJD": "Fijian Dollar",
"FKP": "Falkland Pound",
"GBP": "British Pound",
"GEL": "Georgian Lari",
"GGP": "Guernsey Pound",
"GHS": "Ghanaian Cedi",
"GIP": "Gibraltar Pound",
"GMD": "Gambian Dalasi",
"GNF": "Guinean Franc",
"GTQ": "Guatemalan Quetzal",
"GYD": "Guyanese Dollar",
"HKD": "Hong Kong Dollar",
"HNL": "Honduran Lempira",
"HRK": "Croatian Kuna",
"HTG": "Haitian Gourde",
"HUF": "Hungarian Forint",
"IDR": "Indonesian Rupiah",
"ILS": "Israeli New Sheqel",
"IMP": "Isle of Man Pound",
"INR": "Indian Rupee",
"IQD": "Iraqi Dinar",
"ISK": "Icelandic Króna",
"JEP": "Jersey Pound",
"JMD": "Jamaican Dollar",
"JOD": "Jordanian Dinar",
"JPY": "Japanese Yen",
"KES": "Kenyan Shilling",
"KGS": "Kyrgyzstani Som",
"KHR": "Cambodian Riel",
"KMF": "Comorian Franc",
"KRW": "South Korean Won",
"KWD": "Kuwaiti Dinar",
"KYD": "Cayman Islands Dollar",
"KZT": "Kazakhstani Tenge",
"LAK": "Lao Kip",
"LBP": "Lebanese Pound",
"LKR": "Sri Lankan Rupee",
"LRD": "Liberian Dollar",
"LSL": "Lesotho Loti",
"LYD": "Libyan Dinar",
"MAD": "Moroccan Dirham",
"MDL": "Moldovan Leu",
"MGA": "Malagasy Ariary",
"MKD": "Macedonian Denar",
"MMK": "Myanmar Kyat",
"MNT": "Mongolian Tögrög",
"MOP": "Macanese Pataca",
"MRO": "Mauritanian Ouguiya",
"MUR": "Mauritian Rupee",
"MVR": "Maldivian Rufiyaa",
"MWK": "Malawian Kwacha",
"MXN": "Mexican Peso",
"MYR": "Malaysian Ringgit",
"MZN": "Mozambican Metical",
"NAD": "Namibian Dollar",
"NGN": "Nigerian Naira",
"NIO": "Nicaraguan Córdoba",
"NOK": "Norwegian Krone",
"NPR": "Nepalese Rupee",
"NZD": "New Zealand Dollar",
"OMR": "Omani Rial",
"PAB": "Panamanian Balboa",
"PEN": "Peruvian Sol",
"PGK": "Papua New Guinean Kina",
"PHP": "Philippine Peso",
"PKR": "Pakistani Rupee",
"PLN": "Polish Złoty",
"PYG": "Paraguayan Guaraní",
"QAR": "Qatari Riyal",
"RON": "Romanian Leu",
"RSD": "Serbian Dinar",
"RUB": "Russian Ruble",
"RWF": "Rwandan Franc",
"SAR": "Saudi Riyal",
"SBD": "Solomon Islands Dollar",
"SCR": "Seychellois Rupee",
"SEK": "Swedish Krona",
"SGD": "Singapore Dollar",
"SHP": "Saint Helenian Pound",
"SLL": "Sierra Leonean Leone",
"SOS": "Somali Shilling",
"SRD": "Surinamese Dollar",
"SSP": "South Sudanese Pound",
"STD": "São Tomé and Príncipe Dobra",
"SVC": "Salvadoran Colón",
"SZL": "Swazi Lilangeni",
"THB": "Thai Baht",
"TJS": "Tajikistani Somoni",
"TMT": "Turkmenistani Manat",
"TND": "Tunisian Dinar",
"TOP": "Tongan Paʻanga",
"TRY": "Turkish Lira",
"TTD": "Trinidad and Tobago Dollar",
"TWD": "New Taiwan Dollar",
"TZS": "Tanzanian Shilling",
"UAH": "Ukrainian Hryvnia",
"UGX": "Ugandan Shilling",
"USD": "US Dollar",
"UYU": "Uruguayan Peso",
"UZS": "Uzbekistan Som",
"VEF": "Venezuelan Bolívar",
"VES": "Venezuelan Bolívar Soberano",
"VND": "Vietnamese Đồng",
"VUV": "Vanuatu Vatu",
"WST": "Samoan Tala",
"XAF": "Central African Cfa Franc",
"XAG": "Silver (Troy Ounce)",
"XAU": "Gold (Troy Ounce)",
"XCD": "East Caribbean Dollar",
"XDR": "Special Drawing Rights",
"XOF": "West African Cfa Franc",
"XPD": "Palladium",
"XPF": "Cfp Franc",
"XPT": "Platinum",
"YER": "Yemeni Rial",
"ZAR": "South African Rand",
"ZMW": "Zambian Kwacha",
"ZWL": "Zimbabwean Dollar"
}
}

View File

@ -803,9 +803,8 @@
self.productDialog.data = _.clone(link._data)
if (self.productDialog.data.categories) {
self.productDialog.data.categories = self.productDialog.data.categories.split(
','
)
self.productDialog.data.categories =
self.productDialog.data.categories.split(',')
}
if (self.productDialog.data.image.startsWith('data:')) {
self.productDialog.url = false

View File

@ -809,7 +809,8 @@
this.createTheme(wallet, data)
},
sendFormDataCharge: function () {
this.formDialogCharge.data.custom_css = this.formDialogCharge.data.custom_css?.id
this.formDialogCharge.data.custom_css =
this.formDialogCharge.data.custom_css?.id
const data = this.formDialogCharge.data
const wallet = this.g.user.wallets[0].inkey
data.amount = parseInt(data.amount)

View File

@ -574,11 +574,12 @@
.inkey
)
.then(function (response) {
self.emailaddresses = _.reject(self.emailaddresses, function (
obj
) {
self.emailaddresses = _.reject(
self.emailaddresses,
function (obj) {
return obj.id == emailaddressId
})
}
)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)

View File

@ -484,9 +484,8 @@
var link = _.findWhere(this.domains, {id: formId})
console.log(link.id)
this.domainDialog.data = _.clone(link)
this.domainDialog.data.allowed_record_types = link.allowed_record_types.split(
', '
)
this.domainDialog.data.allowed_record_types =
link.allowed_record_types.split(', ')
this.domainDialog.show = true
},
updateDomain: function (wallet, data) {

View File

@ -500,6 +500,7 @@
.post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, {
params: {
amount: this.sat,
memo: this.amountFormatted,
tipAmount: this.tipAmountSat
}
})

View File

@ -58,7 +58,7 @@ async def api_tpos_delete(
@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED)
async def api_tpos_create_invoice(
tpos_id: str, amount: int = Query(..., ge=1), tipAmount: int = 0
tpos_id: str, amount: int = Query(..., ge=1), memo: str = "", tipAmount: int = 0
) -> dict:
tpos = await get_tpos(tpos_id)
@ -75,7 +75,7 @@ async def api_tpos_create_invoice(
payment_hash, payment_request = await create_invoice(
wallet_id=tpos.wallet,
amount=amount,
memo=f"{tpos.name}",
memo=f"{memo} to {tpos.name}" if memo else f"{tpos.name}",
extra={
"tag": "tpos",
"tipAmount": tipAmount,

View File

@ -174,11 +174,12 @@ async function walletList(path) {
'/watchonly/api/v1/wallet/' + walletAccountId,
this.adminkey
)
this.walletAccounts = _.reject(this.walletAccounts, function (
obj
) {
this.walletAccounts = _.reject(
this.walletAccounts,
function (obj) {
return obj.id === walletAccountId
})
}
)
await this.refreshWalletAccounts()
} catch (error) {
this.$q.notify({

View File

@ -8,7 +8,7 @@ from typing import List, Optional
import httpx
from loguru import logger
from pydantic import BaseSettings, Field, validator
from pydantic import BaseSettings, Extra, Field, validator
def list_parse_fallback(v):
@ -33,6 +33,7 @@ class LNbitsSettings(BaseSettings):
env_file_encoding = "utf-8"
case_sensitive = False
json_loads = list_parse_fallback
extra = Extra.ignore
class UsersSettings(LNbitsSettings):

39
package-lock.json generated
View File

@ -1,31 +1,54 @@
{
"name": "lnbits-legend",
"name": "main",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"devDependencies": {
"prettier": "2.1.1"
"prettier": "2.8.3",
"pyright": "1.1.289"
}
},
"node_modules/prettier": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz",
"integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==",
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
"integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pyright": {
"version": "1.1.289",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
"dev": true,
"bin": {
"pyright": "index.js",
"pyright-langserver": "langserver.index.js"
},
"engines": {
"node": ">=12.0.0"
}
}
},
"dependencies": {
"prettier": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz",
"integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==",
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
"integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
"dev": true
},
"pyright": {
"version": "1.1.289",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
"dev": true
}
}

View File

@ -1,5 +1,6 @@
{
"devDependencies": {
"prettier": "2.1.1"
"prettier": "2.8.3",
"pyright": "1.1.289"
}
}

26
poetry.lock generated
View File

@ -175,6 +175,24 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "boltz-client"
version = "0.1.2"
description = "python boltz client"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "boltz_client-0.1.2-py3-none-any.whl", hash = "sha256:2fb0814c7c3ea88d039e71088648df27db0c036b777b0618bd30638dd76ebe90"},
{file = "boltz_client-0.1.2.tar.gz", hash = "sha256:b360c0ff26f2dea62af6457de4d8c46e434cd24b607ed3aa71494409b57e082b"},
]
[package.dependencies]
click = ">=8"
embit = ">=0.4"
httpx = ">=0.23"
websockets = ">=10"
[[package]]
name = "cashu"
version = "0.8.2"
@ -532,10 +550,10 @@ files = [
cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools-rust (>=0.11.4)"]
sdist = ["setuptools_rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
@ -1784,7 +1802,7 @@ mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"]
mysql = ["mysqlclient"]
oracle = ["cx-oracle"]
oracle = ["cx_oracle"]
postgresql = ["psycopg2"]
postgresql-pg8000 = ["pg8000 (<1.16.6)"]
postgresql-psycopg2binary = ["psycopg2-binary"]
@ -2094,4 +2112,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
content-hash = "9daf94dd600a7e23dcefcc8752fae1694e0084e56553dc578a63272776a8fe53"
content-hash = "b2d22a2a33b4c0a4491b5519b28772435c15747b407a150ffa591bcf6ccb56a6"

View File

@ -62,7 +62,8 @@ protobuf = "^4.21.6"
Cerberus = "^1.3.4"
async-timeout = "^4.0.2"
pyln-client = "0.11.1"
cashu = "0.8.2"
cashu = "^0.8.2"
boltz-client = "^0.1.2"
[tool.poetry.dev-dependencies]
@ -85,11 +86,18 @@ lnbits = "lnbits.server:main"
[tool.isort]
profile = "black"
[tool.pyright]
include = [
"lnbits"
]
exclude = [
"lnbits/wallets/lnd_grpc_files",
]
[tool.mypy]
files = "lnbits"
exclude = """(?x)(
^lnbits/extensions/boltz.
| ^lnbits/wallets/lnd_grpc_files.
^lnbits/wallets/lnd_grpc_files.
)"""
[[tool.mypy.overrides]]

View File

@ -7,6 +7,7 @@ attrs==22.2.0 ; python_version >= "3.7" and python_version < "4.0"
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
boltz-client==0.1.2 ; python_version >= "3.7" and python_version < "4.0"
cashu==0.8.2 ; python_version >= "3.7" and python_version < "4.0"
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
certifi==2022.12.7 ; python_version >= "3.7" and python_version < "4.0"

View File

@ -1,17 +1,6 @@
import asyncio
import json
import secrets
import pytest
import pytest_asyncio
from lnbits.core.crud import create_account, create_wallet, get_wallet
from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap
from lnbits.extensions.boltz.models import (
CreateReverseSubmarineSwap,
CreateSubmarineSwap,
)
from tests.mocks import WALLET
from lnbits.extensions.boltz.models import CreateReverseSubmarineSwap
@pytest_asyncio.fixture(scope="session")
@ -22,4 +11,4 @@ async def reverse_swap(from_wallet):
onchain_address="bcrt1q4vfyszl4p8cuvqh07fyhtxve5fxq8e2ux5gx43",
amount=20_000,
)
return await create_reverse_swap(data)
return data

View File

@ -1,7 +1,6 @@
import pytest
import pytest_asyncio
from tests.helpers import is_fake, is_regtest
from tests.helpers import is_fake
@pytest.mark.asyncio

View File

@ -1,31 +0,0 @@
import asyncio
import pytest
import pytest_asyncio
from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap
from lnbits.extensions.boltz.crud import (
create_reverse_submarine_swap,
create_submarine_swap,
get_reverse_submarine_swap,
get_submarine_swap,
)
from tests.extensions.boltz.conftest import reverse_swap
from tests.helpers import is_fake, is_regtest
@pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes in regtest")
async def test_create_reverse_swap(client, reverse_swap):
swap, wait_for_onchain = reverse_swap
assert swap.status == "pending"
assert swap.id is not None
assert swap.boltz_id is not None
assert swap.claim_privkey is not None
assert swap.onchain_address is not None
assert swap.lockup_address is not None
newswap = await create_reverse_submarine_swap(swap)
await wait_for_onchain
newswap = await get_reverse_submarine_swap(swap.id)
assert newswap is not None
assert newswap.status == "complete"