diff --git a/lnbits/extensions/deezy/README.md b/lnbits/extensions/deezy/README.md new file mode 100644 index 00000000..c8c0678a --- /dev/null +++ b/lnbits/extensions/deezy/README.md @@ -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 \ No newline at end of file diff --git a/lnbits/extensions/deezy/__init__.py b/lnbits/extensions/deezy/__init__.py new file mode 100644 index 00000000..05d1c9a7 --- /dev/null +++ b/lnbits/extensions/deezy/__init__.py @@ -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 diff --git a/lnbits/extensions/deezy/config.json b/lnbits/extensions/deezy/config.json new file mode 100644 index 00000000..4f945a79 --- /dev/null +++ b/lnbits/extensions/deezy/config.json @@ -0,0 +1,6 @@ +{ + "name": "Deezy", + "short_description": "LN to onchain, onchain to LN swaps", + "tile": "/deezy/static/deezy.png", + "contributors": ["Uthpala"] +} diff --git a/lnbits/extensions/deezy/crud.py b/lnbits/extensions/deezy/crud.py new file mode 100644 index 00000000..75549349 --- /dev/null +++ b/lnbits/extensions/deezy/crud.py @@ -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, + ), + ) diff --git a/lnbits/extensions/deezy/migrations.py b/lnbits/extensions/deezy/migrations.py new file mode 100644 index 00000000..67455d6b --- /dev/null +++ b/lnbits/extensions/deezy/migrations.py @@ -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 + ); + """ + ) diff --git a/lnbits/extensions/deezy/models.py b/lnbits/extensions/deezy/models.py new file mode 100644 index 00000000..e69db355 --- /dev/null +++ b/lnbits/extensions/deezy/models.py @@ -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 = "" diff --git a/lnbits/extensions/deezy/static/deezy.png b/lnbits/extensions/deezy/static/deezy.png new file mode 100644 index 00000000..cb526705 Binary files /dev/null and b/lnbits/extensions/deezy/static/deezy.png differ diff --git a/lnbits/extensions/deezy/templates/deezy/_api_docs.html b/lnbits/extensions/deezy/templates/deezy/_api_docs.html new file mode 100644 index 00000000..4a4e9e30 --- /dev/null +++ b/lnbits/extensions/deezy/templates/deezy/_api_docs.html @@ -0,0 +1,253 @@ + + + + +
+ Deezy.io: Do onchain to offchain and vice-versa swaps +
+

+ Link : + + https://deezy.io/ + +

+

+ API DOCS +

+

+ Created by, + Uthpala +

+
+
+
+ + + + + +
+ Get the current info about the swap service for converting LN btc to + on-chain BTC. +
+ + GET (mainnet) + https://api.deezy.io/v1/swap/info + +
+ + GET (testnet) + https://api-testnet.deezy.io/v1/swap/info + +
Response
+
+            {
+              "liquidity_fee_ppm": 2000,
+              "on_chain_bytes_estimate": 300,
+              "max_swap_amount_sats": 100000000,
+              "min_swap_amount_sats": 100000,
+              "available": true
+            }
+          
+
+
+
+ + + +
+ Initiate a new swap to send lightning btc in exchange for on-chain + btc +
+ + POST (mainnet) + https://api.deezy.io/v1/swap + +
+ + POST (testnet) + https://api-testnet.deezy.io/v1/swap + +
Payload
+
+            {
+              "amount_sats": 500000,
+              "on_chain_address": "tb1qrcdhlm0m...",
+              "on_chain_sats_per_vbyte": 2
+            }
+          
+
Response
+
+            {
+              "bolt11_invoice": "lntb603u1p3vmxj7p...",
+              "fee_sats": 600
+            }
+          
+
+
+
+ + + +
+ Lookup the on-chain transaction information for an existing swap +
+ + GET (mainnet) + https://api.deezy.io/v1/swap/lookup + +
+ + GET (testnet) + https://api-testnet.deezy.io/v1/swap/lookup + +
Query Parameter
+
+            "bolt11_invoice": "lntb603u1p3vmxj7pp54...",
+          
+
Response
+
+            {
+              "on_chain_txid": "string",
+              "tx_hex": "string"
+            }
+          
+
+
+
+
+ + + + +
+ Generate an on-chain deposit address for your lnurl or lightning + address. +
+ + POST (mainnet) + https://api.deezy.io/v1/source + +
+ + POST (testnet) + https://api-testnet.deezy.io/v1/source + +
Payload
+
+            {
+              "lnurl_or_lnaddress": "LNURL1DP68GURN8GHJ...",
+              "secret_access_key": "b3c6056d2845867fa7..",
+              "webhook_url": "https://your.website.com/dee.."
+            }
+          
+
Response
+
+            {
+              "address": "bc1qkceyc5...",
+              "secret_access_key": "b3c6056d28458...",
+              "commitment": "for any satoshis sent to bc1..",
+              "signature": "d69j6aj1ssz5egmsr..",
+              "webhook_url": "https://your.website.com/deez.."
+            }
+          
+
+
+
+ + + +
+ Lookup (BTC to LN) swaps +
+ + GET (mainnet) + https://api.deezy.io/v1/source/lookup + +
+ + GET (testnet) + https://api-testnet.deezy.io/v1/source/lookup + +
Response
+
+            {
+              "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
+            }
+          
+
+
+
+
+
diff --git a/lnbits/extensions/deezy/templates/deezy/index.html b/lnbits/extensions/deezy/templates/deezy/index.html new file mode 100644 index 00000000..858d3255 --- /dev/null +++ b/lnbits/extensions/deezy/templates/deezy/index.html @@ -0,0 +1,588 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + +
Deezy
+

+ An access token is required to use the swap service. Email + support@deezy.io or contact @dannydeezy on telegram to get one. +

+
+
+ Deezy token + Add or Update token +
+

+
+ + + + + + + + + + Send lightning btc and receive on-chain btc + + + + + Send on-chain btc and receive via lightning + + + + +
+
LIGHTNING BTC -> BTC
+ + + + + + + Cancel + + + + +
+
Pay invoice to complete swap
+ + + +
+
+ + + + + + + +
+
+
+
+
BTC -> LIGHTNING BTC
+ + + + Cancel + + + + +
+
Onchain Address
+ + + +
+
+ + + + + + + + + +
+
+
+
+
+ {% raw %} + + + +
Success Bitcoin is on its way
+
+ + + Onchain tx id {{ swapLnToBtc.onchainTxId }} + + + + + +
+
+ {% endraw %} +
+
+ + +
{{SITE_TITLE}} Boltz extension
+
+ + + {% include "deezy/_api_docs.html" %} + +
+
+
+ +
+
+ +
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/deezy/views.py b/lnbits/extensions/deezy/views.py new file mode 100644 index 00000000..131c03b2 --- /dev/null +++ b/lnbits/extensions/deezy/views.py @@ -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()} + ) diff --git a/lnbits/extensions/deezy/views_api.py b/lnbits/extensions/deezy/views_api.py new file mode 100644 index 00000000..1006edeb --- /dev/null +++ b/lnbits/extensions/deezy/views_api.py @@ -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 diff --git a/tests/data/mock_data.zip b/tests/data/mock_data.zip index 4070bee7..d5169e12 100644 Binary files a/tests/data/mock_data.zip and b/tests/data/mock_data.zip differ