Merge remote-tracking branch 'origin/main' into add-satoshis-currency-option-nip5

This commit is contained in:
ben 2023-01-09 11:00:03 +00:00
commit cf03ae0a5f
12 changed files with 1155 additions and 0 deletions

View File

@ -0,0 +1,11 @@
# Deezy: Home for Lightning Liquidity
Swap lightning bitcoin for on-chain bitcoin to get inbound liquidity. Or get an on-chain deposit address for your lightning address.
* [Website](https://deezy.io)
* [Lightning Node](https://amboss.space/node/024bfaf0cabe7f874fd33ebf7c6f4e5385971fc504ef3f492432e9e3ec77e1b5cf)
* [Documentation](https://docs.deezy.io)
* [Discord](https://discord.gg/nEBbrUAvPy)
# Usage
This extension lets you swap lightning btc for on-chain btc and vice versa.
* Swap Lightning -> BTC to get inbound liquidity
* Swap BTC -> Lightning to generate an on-chain deposit address for your lightning address

View File

@ -0,0 +1,25 @@
from fastapi import APIRouter
from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_deezy")
deezy_ext: APIRouter = APIRouter(prefix="/deezy", tags=["deezy"])
deezy_static_files = [
{
"path": "/deezy/static",
"app": StaticFiles(directory="lnbits/extensions/deezy/static"),
"name": "deezy_static",
}
]
def deezy_renderer():
return template_renderer(["lnbits/extensions/deezy/templates"])
from .views import * # noqa
from .views_api import * # noqa

View File

@ -0,0 +1,6 @@
{
"name": "Deezy",
"short_description": "LN to onchain, onchain to LN swaps",
"tile": "/deezy/static/deezy.png",
"contributors": ["Uthpala"]
}

View File

@ -0,0 +1,115 @@
from http import HTTPStatus
from typing import List, Optional
from . import db
from .models import BtcToLnSwap, LnToBtcSwap, Token, UpdateLnToBtcSwap
async def get_ln_to_btc() -> List[LnToBtcSwap]:
rows = await db.fetchall(
f"SELECT * FROM deezy.ln_to_btc_swap ORDER BY created_at DESC",
)
return [LnToBtcSwap(**row) for row in rows]
async def get_btc_to_ln() -> List[BtcToLnSwap]:
rows = await db.fetchall(
f"SELECT * FROM deezy.btc_to_ln_swap ORDER BY created_at DESC",
)
return [BtcToLnSwap(**row) for row in rows]
async def get_token() -> Optional[Token]:
row = await db.fetchone(
f"SELECT * FROM deezy.token ORDER BY created_at DESC",
)
return Token(**row) if row else None
async def save_token(
data: Token,
) -> Token:
await db.execute(
"""
INSERT INTO deezy.token (
deezy_token
)
VALUES (?)
""",
(data.deezy_token,),
)
return data
async def save_ln_to_btc(
data: LnToBtcSwap,
) -> LnToBtcSwap:
return await db.execute(
"""
INSERT INTO deezy.ln_to_btc_swap (
amount_sats,
on_chain_address,
on_chain_sats_per_vbyte,
bolt11_invoice,
fee_sats,
txid,
tx_hex
)
VALUES (?,?,?,?,?,?,?)
""",
(
data.amount_sats,
data.on_chain_address,
data.on_chain_sats_per_vbyte,
data.bolt11_invoice,
data.fee_sats,
data.txid,
data.tx_hex,
),
)
async def update_ln_to_btc(data: UpdateLnToBtcSwap) -> str:
await db.execute(
"""
UPDATE deezy.ln_to_btc_swap
SET txid = ?, tx_hex = ?
WHERE bolt11_invoice = ?
""",
(data.txid, data.tx_hex, data.bolt11_invoice),
)
return data.txid
async def save_btc_to_ln(
data: BtcToLnSwap,
) -> BtcToLnSwap:
return await db.execute(
"""
INSERT INTO deezy.btc_to_ln_swap (
ln_address,
on_chain_address,
secret_access_key,
commitment,
signature
)
VALUES (?,?,?,?,?)
""",
(
data.ln_address,
data.on_chain_address,
data.secret_access_key,
data.commitment,
data.signature,
),
)

View File

@ -0,0 +1,37 @@
async def m001_initial(db):
await db.execute(
f"""
CREATE TABLE deezy.ln_to_btc_swap (
id TEXT PRIMARY KEY,
amount_sats {db.big_int} NOT NULL,
on_chain_address TEXT NOT NULL,
on_chain_sats_per_vbyte INT NOT NULL,
bolt11_invoice TEXT NOT NULL,
fee_sats {db.big_int} NOT NULL,
txid TEXT NULL,
tx_hex TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
"""
)
await db.execute(
f"""
CREATE TABLE deezy.btc_to_ln_swap (
id TEXT PRIMARY KEY,
ln_address TEXT NOT NULL,
on_chain_address TEXT NOT NULL,
secret_access_key TEXT NOT NULL,
commitment TEXT NOT NULL,
signature TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
"""
)
await db.execute(
f"""
CREATE TABLE deezy.token (
deezy_token TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
"""
)

View File

@ -0,0 +1,34 @@
from typing import Optional
from pydantic.main import BaseModel
from sqlalchemy.engine import base # type: ignore
class Token(BaseModel):
deezy_token: str
class LnToBtcSwap(BaseModel):
amount_sats: int
on_chain_address: str
on_chain_sats_per_vbyte: int
bolt11_invoice: str
fee_sats: int
txid: str = ""
tx_hex: str = ""
created_at: str = ""
class UpdateLnToBtcSwap(BaseModel):
txid: str
tx_hex: str
bolt11_invoice: str
class BtcToLnSwap(BaseModel):
ln_address: str
on_chain_address: str
secret_access_key: str
commitment: str
signature: str
created_at: str = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,253 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="About Deezy"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<img
alt=""
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTMwLjA5IDEzNi43MyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fS5jbHMtMntmaWxsLXJ1bGU6ZXZlbm9kZDtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ze2ZpbGw6I2ZmYzkyYjt9PC9zdHlsZT48bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudCIgeDE9IjUxLjY5IiB5MT0iMzEuNjciIHgyPSIxODAuMjMiIHkyPSIxMDUuMTIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmZmYyMWYiLz48c3RvcCBvZmZzZXQ9IjAuMjkiIHN0b3AtY29sb3I9IiNmZmNkMmQiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmNzkyMzMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTYxLjg5LDBoNTcuNTVDMTMzLjksMCwxNDUsMS40NCwxNTIuOTIsNC4zM2MxNC4yMSw1LjA1LDIzLjYsMTQuMTgsMjguNjYsMjcuNjRMMTUyLjY4LDQ2LjRsLS4yMy0uNDhjLTIuMTgtNi43NC01LjA2LTExLjU0LTguNDMtMTQuOUEyNS40MywyNS40MywwLDAsMCwxMzIsMjUuNDlsLS4yNC0yLjg5LTMuMTMsMi4xNmE1NC4xMSw1NC4xMSwwLDAsMC05LjE2LS40OEg5MC43OVY1MUw2MS44OSw3MC42OFptMTI1LDU0LjgxQTEyNC43NiwxMjQuNzYsMCwwLDEsMTg3LjYsNjhhMTA4LjM4LDEwOC4zOCwwLDAsMS01LjMsMzQuNjJjLTMuMzcsMTEuMy05LjM5LDE5LjQ3LTE3LjU4LDI0Ljc2YTQ2LjE4LDQ2LjE4LDAsMCwxLTE3LjA5LDYuNDljLTYsMS4yLTE1LjQxLDEuNjgtMjguMTksMS42OEg2MS44OVY5OS4yOWwyOC45LTE0LjQzdjI2LjY5aDExLjU2bC4yNCwyLjE2LDMuMzctMi4xNmgxMy40OGMxMi43OCwwLDIxLjQ0LTIuODksMjYuMjYtOC40MiwzLjEzLTMuNiw1LjU0LTguNDEsNy4yMi0xNC45YTU0LjI4LDU0LjI4LDAsMCwwLDIuNDEtMTEuM1oiLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMCAxMjIuMTMgMTI1LjcxIDM1LjU4IDEyOC44NSA2Ni41OSAyMzEuOTIgMTQuNjcgMTA4LjM3IDEwMC45NyAxMDQuNzYgNjkuNzEgMCAxMjIuMTMiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yNjYuNjksMjguNjhoMTN2ODRoLTEzVjEwNHEtNy4zMiwxMC4yLTIxLDEwLjJhMjguMTQsMjguMTQsMCwwLDEtMjEuMTItOS4xOCwzMS4yMSwzMS4yMSwwLDAsMS04Ljc2LTIyLjM4LDMxLjE1LDMxLjE1LDAsMCwxLDguNzYtMjIuNDQsMjguMjMsMjguMjMsMCwwLDEsMjEuMTItOS4xMnExMy42OCwwLDIxLDEwLjA4Wk0yMzQuMTcsOTYuNDJhMTkuNTcsMTkuNTcsMCwwLDAsMjcuMTIsMCwxOC43NCwxOC43NCwwLDAsMCw1LjQtMTMuNzQsMTguNzQsMTguNzQsMCwwLDAtNS40LTEzLjc0LDE5LjU3LDE5LjU3LDAsMCwwLTI3LjEyLDAsMTguNzQsMTguNzQsMCwwLDAtNS40LDEzLjc0QTE4Ljc0LDE4Ljc0LDAsMCwwLDIzNC4xNyw5Ni40MloiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0zMDIsODguMmExNi40OCwxNi40OCwwLDAsMCw2LjYsMTAuNSwyMS4yMiwyMS4yMiwwLDAsMCwxMi42LDMuNjZxMTAuMzIsMCwxNS40OC03LjQ0bDEwLjY4LDYuMjRxLTguODgsMTMuMDgtMjYuMjgsMTMuMDgtMTQuNjQsMC0yMy42NC04Ljk0dC05LTIyLjYycTAtMTMuNDQsOC44OC0yMi41dDIyLjgtOS4wNnExMy4yLDAsMjEuNjYsOS4yNGEzMiwzMiwwLDAsMSw4LjQ2LDIyLjQ0LDQwLjA5LDQwLjA5LDAsMCwxLS40OCw1LjRabS0uMTItMTAuNTZoMzUuMjhxLTEuMzItNy4zMi02LjA2LTExQTE3LjQ1LDE3LjQ1LDAsMCwwLDMyMCw2Mi44OGExOC4yMywxOC4yMywwLDAsMC0xMiw0QTE3Ljg2LDE3Ljg2LDAsMCwwLDMwMS44NSw3Ny42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0zNjguNDUsODguMmExNi40OCwxNi40OCwwLDAsMCw2LjYsMTAuNSwyMS4yMiwyMS4yMiwwLDAsMCwxMi42LDMuNjZxMTAuMzIsMCwxNS40OC03LjQ0bDEwLjY4LDYuMjRxLTguODgsMTMuMDgtMjYuMjgsMTMuMDgtMTQuNjQsMC0yMy42NC04Ljk0dC05LTIyLjYycTAtMTMuNDQsOC44OC0yMi41dDIyLjgtOS4wNnExMy4yLDAsMjEuNjYsOS4yNGEzMiwzMiwwLDAsMSw4LjQ2LDIyLjQ0LDQwLjA5LDQwLjA5LDAsMCwxLS40OCw1LjRabS0uMTItMTAuNTZoMzUuMjhxLTEuMzItNy4zMi02LjA2LTExYTE3LjQ1LDE3LjQ1LDAsMCwwLTExLjEtMy43MiwxOC4yMywxOC4yMywwLDAsMC0xMiw0QTE3Ljg2LDE3Ljg2LDAsMCwwLDM2OC4zMyw3Ny42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik00MzcuNTgsMTAwLjQ0aDI5LjE2djEyLjI0SDQxOS45M1YxMDRMNDQ4LDY0LjkySDQyMS4xM1Y1Mi42OGg0NC4zOXY4LjYzWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTUxNi4yOSw1Mi42OGgxMy44bC0yMyw2MS45MnEtOC42NCwyMy4yOC0yOS4yOCwyMi4wOFYxMjQuNTZxNi4xMi4zNiw5Ljg0LTIuNTh0Ni4xMi05LjE4bC42LTEuMkw0NjguODksNTIuNjhoMTQuMTZsMTcuODksNDMuNTVaIi8+PC9nPjwvZz48L3N2Zz4="
height="40"
class="d-inline-block align-top my-2"
/>
<h5 class="text-subtitle1 q-my-none">
Deezy.io: Do onchain to offchain and vice-versa swaps
</h5>
<p>
Link :
<a class="text-light-blue" target="_blank" href="https://deezy.io/">
https://deezy.io/
</a>
</p>
<p>
<a class="text-light-blue" target="_blank" href="https://docs.deezy.io/"
>API DOCS</a
>
</p>
<p>
<small
>Created by,
<a
class="text-light-blue"
target="_blank"
href="https://twitter.com/Uthpala_419"
>Uthpala</a
></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item
group="swap-ln-to-btc"
dense
expand-separator
label="Swap (LIGHTNING TO BTC)"
:content-inset-level="0.5"
>
<q-expansion-item group="ln-to-btc" dense expand-separator label="GET Info">
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Get the current info about the swap service for converting LN btc to
on-chain BTC.
</h5>
<code class="text-light-blue">
<span class="text-white">GET (mainnet)</span>
https://api.deezy.io/v1/swap/info
</code>
<br />
<code class="text-light-blue">
<span class="text-white">GET (testnet)</span>
https://api-testnet.deezy.io/v1/swap/info
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"liquidity_fee_ppm": 2000,
"on_chain_bytes_estimate": 300,
"max_swap_amount_sats": 100000000,
"min_swap_amount_sats": 100000,
"available": true
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="ln-to-btc"
dense
expand-separator
label="POST New (LN to BTC) Swap"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Initiate a new swap to send lightning btc in exchange for on-chain
btc
</h5>
<code class="text-light-blue">
<span class="text-white">POST (mainnet)</span>
https://api.deezy.io/v1/swap
</code>
<br />
<code class="text-light-blue">
<span class="text-white">POST (testnet)</span>
https://api-testnet.deezy.io/v1/swap
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Payload</h6>
<pre>
{
"amount_sats": 500000,
"on_chain_address": "tb1qrcdhlm0m...",
"on_chain_sats_per_vbyte": 2
}
</pre>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"bolt11_invoice": "lntb603u1p3vmxj7p...",
"fee_sats": 600
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="ln-to-btc"
dense
expand-separator
label="GET Lookup (LN to BTC) Swap"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Lookup the on-chain transaction information for an existing swap
</h5>
<code class="text-light-blue">
<span class="text-white">GET (mainnet)</span>
https://api.deezy.io/v1/swap/lookup
</code>
<br />
<code class="text-light-blue">
<span class="text-white">GET (testnet)</span>
https://api-testnet.deezy.io/v1/swap/lookup
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Query Parameter</h6>
<pre>
"bolt11_invoice": "lntb603u1p3vmxj7pp54...",
</pre>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"on_chain_txid": "string",
"tx_hex": "string"
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
<q-expansion-item
group="swap-btc-to-ln"
dense
expand-separator
label="Swap (BTC TO LIGHTNING)"
:content-inset-level="0.5"
>
<q-expansion-item
group="btc-to-ln"
dense
expand-separator
label="POST New On-Chain Deposit Address"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Generate an on-chain deposit address for your lnurl or lightning
address.
</h5>
<code class="text-light-blue">
<span class="text-white">POST (mainnet)</span>
https://api.deezy.io/v1/source
</code>
<br />
<code class="text-light-blue">
<span class="text-white">POST (testnet)</span>
https://api-testnet.deezy.io/v1/source
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Payload</h6>
<pre>
{
"lnurl_or_lnaddress": "LNURL1DP68GURN8GHJ...",
"secret_access_key": "b3c6056d2845867fa7..",
"webhook_url": "https://your.website.com/dee.."
}
</pre>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"address": "bc1qkceyc5...",
"secret_access_key": "b3c6056d28458...",
"commitment": "for any satoshis sent to bc1..",
"signature": "d69j6aj1ssz5egmsr..",
"webhook_url": "https://your.website.com/deez.."
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="btc-to-ln"
dense
expand-separator
label="GET Lookup (BTC to LN) Swaps"
>
<q-card>
<q-card-section>
<h5 class="text-caption q-mt-sm q-mb-none">
Lookup (BTC to LN) swaps
</h5>
<code class="text-light-blue">
<span class="text-white">GET (mainnet)</span>
https://api.deezy.io/v1/source/lookup
</code>
<br />
<code class="text-light-blue">
<span class="text-white">GET (testnet)</span>
https://api-testnet.deezy.io/v1/source/lookup
</code>
<h6 class="text-caption q-mt-sm q-mb-none">Response</h6>
<pre>
{
"swaps": [
{
"lnurl_or_lnaddress": "string",
"deposit_address": "string",
"utxo_key": "string",
"deposit_amount_sats": 0,
"target_payout_amount_sats": 0,
"paid_amount_sats": 0,
"deezy_fee_sats": 0,
"status": "string"
}
],
"total_sent_sats": 0,
"total_received_sats": 0,
"total_pending_payout_sats": 0,
"total_deezy_fees_sats": 0
}
</pre>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
</q-expansion-item>

View File

@ -0,0 +1,588 @@
{% 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>
<h5 class="text-subtitle1 q-mt-none q-mb-md">Deezy</h5>
<p class="text-subtitle2 q-mt-none q-mb-md">
An access token is required to use the swap service. Email
support@deezy.io or contact @dannydeezy on telegram to get one.
</p>
<div>
<div class="flex justify-between items-center">
<span>Deezy token </span>
<q-btn
type="button"
@click="showDeezyTokenForm = !showDeezyTokenForm"
>Add or Update token</q-btn
>
</div>
<p v-if="storedDeezyToken" v-text="storedDeezyToken"></p>
</div>
<q-form
v-if="showDeezyTokenForm"
@submit="storeDeezyToken"
class="q-gutter-md q-mt-lg"
>
<q-input
filled
dense
emit-value
:placeholder="storedDeezyToken"
v-model.trim="deezyToken"
label="Deezy Token"
type="text"
></q-input>
<q-btn color="grey" type="submit" label="Store Deezy Token"></q-btn>
</q-form>
<q-separator class="q-my-lg"></q-separator>
<q-card>
<q-card-section>
<q-btn
label="SWAP (LIGHTNING -> BTC)"
unelevated
color="primary"
@click="showLnToBtcForm"
:disabled="!storedDeezyToken"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send lightning btc and receive on-chain btc
</q-tooltip>
</q-btn>
<q-btn
label="SWAP (BTC -> LIGHTNING)"
unelevated
color="primary"
@click="swapBtcToLn.show = true; swapLnToBtc.show = false;"
:disabled="!storedDeezyToken"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
Send on-chain btc and receive via lightning
</q-tooltip>
</q-btn>
</q-card-section>
</q-card>
<div
v-show="swapLnToBtc.show"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<h6 class="q-mt-none">LIGHTNING BTC -> BTC</h6>
<q-form @submit="sendLnToBtc" class="q-gutter-md">
<q-input
filled
dense
emit-value
v-model.trim="swapLnToBtc.data.amount"
label="Amount (sats)"
type="number"
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="swapLnToBtc.data.on_chain_address"
type="string"
label="Onchain address to receive funds"
></q-input>
<q-input
filled
dense
emit-value
v-model.trim="swapLnToBtc.data.on_chain_sats_per_vbyte"
label="On chain fee rate (sats/vbyte)"
min="1"
type="number"
:hint="swapLnToBtc.suggested_fees && `Economy Fee - ${swapLnToBtc.suggested_fees?.economyFee} | Half an hour fee - ${swapLnToBtc.suggested_fees?.halfHourFee} | Fastest fee - ${swapLnToBtc.suggested_fees?.fastestFee}`"
>
</q-input>
<q-btn
unelevated
color="primary"
type="submit"
label="Create Swap"
></q-btn>
<q-btn flat color="grey" class="q-ml-auto" @click="resetSwapLnToBtc"
>Cancel</q-btn
>
</q-form>
<q-dialog v-model="swapLnToBtc.showInvoice" persistent>
<q-card flat bordered class="my-card">
<q-card-section>
<div class="flex justify-between">
<div class="text-h6">Pay invoice to complete swap</div>
<q-btn flat v-close-popup>
<q-icon name="close" />
</q-btn>
</div>
</q-card-section>
<q-card-section class="q-pt-none">
<qrcode
:value="swapLnToBtc.response"
:options="{width: 360}"
class="rounded-borders"
></qrcode>
</q-card-section>
<q-card-section>
<q-btn
outline
@click="copyLnInvoice"
label="Copy"
color="primary"
></q-btn>
<q-input
v-model="swapLnToBtc.response"
type="textarea"
readonly
@click="$event.target.select()"
/>
</q-card-section>
</q-card>
</q-dialog>
</div>
<div
v-show="swapBtcToLn.show"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<h6 class="q-mt-none">BTC -> LIGHTNING BTC</h6>
<q-form @submit="sendBtcToLn" class="q-gutter-md">
<q-input
filled
dense
emit-value
v-model.trim="swapBtcToLn.data.lnurl_or_lnaddress"
label="Lnurl or Lightning Address"
type="string"
></q-input>
<q-btn
unelevated
color="primary"
type="submit"
label="Generate Onchain Address"
></q-btn>
<q-btn flat color="grey" class="q-ml-auto" @click="resetSwapBtcToLn"
>Cancel</q-btn
>
</q-form>
<q-dialog v-model="swapBtcToLn.showDetails" persistent>
<q-card flat bordered class="my-card">
<q-card-section>
<div class="flex justify-between">
<div class="text-h6">Onchain Address</div>
<q-btn flat v-close-popup>
<q-icon name="close" />
</q-btn>
</div>
</q-card-section>
<q-card-section>
<q-input
v-model="swapBtcToLn.response.address"
type="textarea"
readonly
@click="$event.target.select()"
/>
</q-card-section>
<q-card-section>
<q-btn
outline
@click="copyBtcToLnBtcAddress"
label="Copy Address"
color="primary"
></q-btn>
</q-card-section>
<q-card-section>
<q-input
v-model="swapBtcToLn.response.commitment"
type="textarea"
readonly
@click="$event.target.select()"
/>
</q-card-section>
</q-card>
</q-dialog>
</div>
</q-card-section>
</q-card>
{% raw %}
<q-dialog v-model="swapLnToBtc.invoicePaid">
<q-card class="bg-teal text-white" style="width: 400px">
<q-card-section>
<div class="text-h6">Success Bitcoin is on its way</div>
</q-card-section>
<q-card-section class="q-pt-none">
Onchain tx id {{ swapLnToBtc.onchainTxId }}
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn flat label="OK" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
{% endraw %}
</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 "deezy/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<div class="q-pa-md full-width">
<q-table
title="Swaps Lightning -> BTC"
:data="rowsLnToBtc"
:columns="columnsLnToBtc"
row-key="name"
/>
</div>
<div class="q-pa-md full-width">
<q-table
title="Swaps BTC -> Lightning"
:data="rowsBtcToLn"
:columns="columnsBtcToLn"
row-key="name"
/>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
columnsLnToBtc: [
{
name: 'amount_sats',
label: 'Amount Sats',
align: 'left',
field: 'amount_sats',
sortable: true
},
{
name: 'on_chain_address',
align: 'left',
label: 'On chain address',
field: 'on_chain_address'
},
{
name: 'on_chain_sats_per_vbyte',
align: 'left',
label: 'Onchin sats per vbyte',
field: 'on_chain_sats_per_vbyte',
sortable: true
},
{
name: 'fee_sats',
label: 'Fee sats',
align: 'left',
field: 'fee_sats'
},
{name: 'txid', label: 'Tx Id', align: 'left', field: 'txid'},
{name: 'tx_hex', label: 'Tx Hex', align: 'left', field: 'tx_hex'},
{
name: 'created_at',
label: 'Created at',
align: 'left',
field: 'created_at',
sortable: true,
sort: true
}
],
rowsLnToBtc: [],
columnsBtcToLn: [
{
name: 'ln_address',
align: 'left',
label: 'Ln Address or Invoice',
field: 'ln_address'
},
{
name: 'on_chain_address',
align: 'left',
label: 'Onchain Address',
field: 'on_chain_address'
},
{
name: 'secret_access_key',
align: 'left',
label: 'Secret Access Key',
field: 'secret_access_key'
},
{
name: 'commitment',
align: 'left',
label: 'Commitment',
field: 'commitment'
},
{
name: 'signature',
align: 'left',
label: 'Signature',
field: 'signature'
},
{
name: 'created_at',
label: 'Created at',
field: 'created_at',
align: 'left',
sortable: true,
sort: true
}
],
rowsBtcToLn: [],
showDeezyTokenForm: false,
storedDeezyToken: null,
deezyToken: null,
lightning_btc: '',
tools: [],
swapLnToBtc: {
show: false,
showInvoice: false,
data: {
on_chain_sats_per_vbyte: 1
},
suggested_fees: null,
response: null,
invoicePaid: false,
onchainTxId: null
},
swapBtcToLn: {
show: false,
showDetails: false,
data: {},
response: {}
}
}
},
created: async function () {
this.getToken()
this.getLnToBtc()
this.getBtcToLn()
},
methods: {
updateLnToBtc(payload) {
var self = this
return axios
.post('/deezy/api/v1/update-ln-to-btc', {
...payload
})
.then(function (response) {
console.log('btc to ln is update', response)
})
.catch(function (error) {
console.log(error)
})
},
getToken() {
var self = this
axios({
method: 'GET',
url: '/deezy/api/v1/token'
}).then(function (response) {
self.storedDeezyToken = response.data.deezy_token
if (!self.storeDeezyToken) {
showDeezyTokenForm = true
}
})
},
getLnToBtc() {
var self = this
axios.get('/deezy/api/v1/ln-to-btc').then(function (response) {
if (response.data.length) {
self.rowsLnToBtc = response.data
}
})
},
getBtcToLn() {
var self = this
axios.get('/deezy/api/v1/btc-to-ln').then(function (response) {
if (response.data.length) {
self.rowsBtcToLn = response.data
}
})
},
showLnToBtcForm() {
if (!this.swapLnToBtc.show) {
this.getSuggestedOnChainFees()
}
this.swapLnToBtc.show = true
this.swapBtcToLn.show = false
},
getSuggestedOnChainFees() {
axios
.get('https://mempool.space/api/v1/fees/recommended')
.then(result => {
this.swapLnToBtc.suggested_fees = result.data
})
},
checkIfInvoiceIsPaid() {
if (this.swapLnToBtc.response && !this.swapLnToBtc.invoicePaid) {
var self = this
let interval = setInterval(() => {
axios
.get(
`https://api.deezy.io/v1/swap/lookup?bolt11_invoice=${self.swapLnToBtc.response}`
)
.then(async function (response) {
if (response.data.on_chain_txid) {
self.swapLnToBtc = {
...self.swapLnToBtc,
invoicePaid: true,
onchainTxId: response.data.on_chain_txid
}
self
.updateLnToBtc({
txid: response.data.on_chain_txid,
tx_hex: response.data.tx_hex,
bolt11_invoice: self.swapLnToBtc.response
})
.then(() => {
self.getLnToBtc()
})
clearInterval(interval)
}
})
}, 4000)
}
},
copyLnInvoice() {
Quasar.utils.copyToClipboard(this.swapLnToBtc.response)
},
copyBtcToLnBtcAddress() {
Quasar.utils.copyToClipboard(this.swapBtcToLn.response.address)
},
sendLnToBtc() {
var self = this
axios
.post(
'https://api.deezy.io/v1/swap',
{
amount_sats: parseInt(self.swapLnToBtc.data.amount),
on_chain_address: self.swapLnToBtc.data.on_chain_address,
on_chain_sats_per_vbyte: parseInt(
self.swapLnToBtc.data.on_chain_sats_per_vbyte
)
},
{
headers: {
'x-api-token': self.storedDeezyToken
}
}
)
.then(function (response) {
self.swapLnToBtc = {
...self.swapLnToBtc,
showInvoice: true,
response: response.data.bolt11_invoice
}
const payload = {
amount_sats: parseInt(self.swapLnToBtc.data.amount),
on_chain_address: self.swapLnToBtc.data.on_chain_address,
on_chain_sats_per_vbyte:
self.swapLnToBtc.data.on_chain_sats_per_vbyte,
bolt11_invoice: response.data.bolt11_invoice,
fee_sats: response.data.fee_sats
}
self.storeLnToBtc(payload)
self.checkIfInvoiceIsPaid()
})
.catch(function (error) {
console.log(error)
})
},
sendBtcToLn() {
var self = this
axios
.post(
'https://api.deezy.io/v1/source',
{
lnurl_or_lnaddress: self.swapBtcToLn.data.lnurl_or_lnaddress
},
{
headers: {
'x-api-token': self.storedDeezyToken
}
}
)
.then(function (response) {
self.swapBtcToLn = {
...self.swapBtcToLn,
response: response.data,
showDetails: true
}
const payload = {
ln_address: self.swapBtcToLn.data.lnurl_or_lnaddress,
on_chain_address: response.data.address,
secret_access_key: response.data.secret_access_key,
commitment: response.data.commitment,
signature: response.data.signature
}
self.storeBtcToLn(payload)
})
.catch(function (error) {
console.log(error)
})
},
storeBtcToLn(payload) {
var self = this
axios
.post('/deezy/api/v1/store-btc-to-ln', {
...payload
})
.then(function (response) {
console.log('btc to ln is stored', response)
})
.catch(function (error) {
console.log(error)
})
},
storeLnToBtc(payload) {
var self = this
axios
.post('/deezy/api/v1/store-ln-to-btc', {
...payload
})
.then(function (response) {
console.log('ln to btc is stored', response)
})
.catch(function (error) {
console.log(error)
})
},
storeDeezyToken() {
var self = this
axios
.post('/deezy/api/v1/store-token', {
deezy_token: self.deezyToken
})
.then(function (response) {
self.storedDeezyToken = response.data
self.showDeezyTokenForm = false
})
.catch(function (error) {
console.log(error)
})
},
resetSwapBtcToLn() {
this.swapBtcToLn = {
...this.swapBtcToLn,
data: {}
}
},
resetSwapLnToBtc() {
this.swapLnToBtc = {
...this.swapLnToBtc,
data: {}
}
}
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,21 @@
from fastapi import FastAPI, Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import deezy_ext, deezy_renderer
templates = Jinja2Templates(directory="templates")
@deezy_ext.get("/", response_class=HTMLResponse)
async def index(
request: Request,
user: User = Depends(check_user_exists), # type: ignore
):
return deezy_renderer().TemplateResponse(
"deezy/index.html", {"request": request, "user": user.dict()}
)

View File

@ -0,0 +1,65 @@
# views_api.py is for you API endpoints that could be hit by another service
# add your dependencies here
# import httpx
# (use httpx just like requests, except instead of response.ok there's only the
# response.is_error that is its inverse)
from . import deezy_ext
from .crud import (
get_btc_to_ln,
get_ln_to_btc,
get_token,
save_btc_to_ln,
save_ln_to_btc,
save_token,
update_ln_to_btc,
)
from .models import BtcToLnSwap, LnToBtcSwap, Token, UpdateLnToBtcSwap
@deezy_ext.get("/api/v1/token")
async def api_deezy_get_token():
rows = await get_token()
return rows
@deezy_ext.get("/api/v1/ln-to-btc")
async def api_deezy_get_ln_to_btc():
rows = await get_ln_to_btc()
return rows
@deezy_ext.get("/api/v1/btc-to-ln")
async def api_deezy_get_btc_to_ln():
rows = await get_btc_to_ln()
return rows
@deezy_ext.post("/api/v1/store-token")
async def api_deezy_save_toke(data: Token):
await save_token(data)
return data.deezy_token
@deezy_ext.post("/api/v1/store-ln-to-btc")
async def api_deezy_save_ln_to_btc(data: LnToBtcSwap):
response = await save_ln_to_btc(data)
return response
@deezy_ext.post("/api/v1/update-ln-to-btc")
async def api_deezy_update_ln_to_btc(data: UpdateLnToBtcSwap):
response = await update_ln_to_btc(data)
return response
@deezy_ext.post("/api/v1/store-btc-to-ln")
async def api_deezy_save_btc_to_ln(data: BtcToLnSwap):
response = await save_btc_to_ln(data)
return response

Binary file not shown.