diff --git a/lnbits/extensions/amilk/README.md b/lnbits/extensions/amilk/README.md new file mode 100644 index 000000000..277294592 --- /dev/null +++ b/lnbits/extensions/amilk/README.md @@ -0,0 +1,11 @@ +
curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"
diff --git a/lnbits/extensions/amilk/__init__.py b/lnbits/extensions/amilk/__init__.py
new file mode 100644
index 000000000..0cdd8727f
--- /dev/null
+++ b/lnbits/extensions/amilk/__init__.py
@@ -0,0 +1,12 @@
+from quart import Blueprint
+from lnbits.db import Database
+
+db = Database("ext_amilk")
+
+amilk_ext: Blueprint = Blueprint(
+ "amilk", __name__, static_folder="static", template_folder="templates"
+)
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/amilk/config.json b/lnbits/extensions/amilk/config.json
new file mode 100644
index 000000000..09faf8af8
--- /dev/null
+++ b/lnbits/extensions/amilk/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "AMilk",
+ "short_description": "Assistant Faucet Milker",
+ "icon": "room_service",
+ "contributors": ["arcbtc"]
+}
diff --git a/lnbits/extensions/amilk/crud.py b/lnbits/extensions/amilk/crud.py
new file mode 100644
index 000000000..859d2fa84
--- /dev/null
+++ b/lnbits/extensions/amilk/crud.py
@@ -0,0 +1,42 @@
+from base64 import urlsafe_b64encode
+from uuid import uuid4
+from typing import List, Optional, Union
+
+from . import db
+from .models import AMilk
+
+
+async def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) -> AMilk:
+ amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
+ await db.execute(
+ """
+ INSERT INTO amilk.amilks (id, wallet, lnurl, atime, amount)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (amilk_id, wallet_id, lnurl, atime, amount),
+ )
+
+ amilk = await get_amilk(amilk_id)
+ assert amilk, "Newly created amilk_id couldn't be retrieved"
+ return amilk
+
+
+async def get_amilk(amilk_id: str) -> Optional[AMilk]:
+ row = await db.fetchone("SELECT * FROM amilk.amilks WHERE id = ?", (amilk_id,))
+ return AMilk(**row) if row else None
+
+
+async def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM amilk.amilks WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ return [AMilk(**row) for row in rows]
+
+
+async def delete_amilk(amilk_id: str) -> None:
+ await db.execute("DELETE FROM amilk.amilks WHERE id = ?", (amilk_id,))
diff --git a/lnbits/extensions/amilk/migrations.py b/lnbits/extensions/amilk/migrations.py
new file mode 100644
index 000000000..596a86335
--- /dev/null
+++ b/lnbits/extensions/amilk/migrations.py
@@ -0,0 +1,15 @@
+async def m001_initial(db):
+ """
+ Initial amilks table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE amilk.amilks (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ lnurl TEXT NOT NULL,
+ atime INTEGER NOT NULL,
+ amount INTEGER NOT NULL
+ );
+ """
+ )
diff --git a/lnbits/extensions/amilk/models.py b/lnbits/extensions/amilk/models.py
new file mode 100644
index 000000000..647cc530e
--- /dev/null
+++ b/lnbits/extensions/amilk/models.py
@@ -0,0 +1,9 @@
+from pydantic import BaseModel
+
+
+class AMilk(BaseModel):
+ id: str
+ wallet: str
+ lnurl: str
+ atime: int
+ amount: int
diff --git a/lnbits/extensions/amilk/templates/amilk/_api_docs.html b/lnbits/extensions/amilk/templates/amilk/_api_docs.html
new file mode 100644
index 000000000..f1c27a1ba
--- /dev/null
+++ b/lnbits/extensions/amilk/templates/amilk/_api_docs.html
@@ -0,0 +1,24 @@
+
+ Milking faucets with software, known as "assmilking", seems at first to
+ be black-hat, although in fact there might be some unexplored use cases.
+ An LNURL withdraw gives someone the right to pull funds, which can be
+ done over time. An LNURL withdraw could be used outside of just faucets,
+ to provide money streaming and repeat payments.
Paste or scan an
+ LNURL withdraw, enter the amount for the AMilk to pull and the frequency
+ for it to be pulled.
+
+ Created by, Ben Arc
+
+ This extension allows you to connect a Bleskomat ATM to an lnbits + wallet. It will work with both the + open-source DIY Bleskomat ATM project + as well as the + commercial Bleskomat ATM. +
++ Since the Bleskomat ATMs are designed to be offline, a cryptographic + signing scheme is used to verify that the URL was generated by an + authorized device. When one of your customers inserts fiat money into + the device, a signed URL (lnurl-withdraw) is created and displayed as a + QR code. Your customer scans the QR code with their lnurl-supporting + mobile app, their mobile app communicates with the web API of lnbits to + verify the signature, the fiat currency amount is converted to sats, the + customer accepts the withdrawal, and finally lnbits will pay the + customer from your lnbits wallet. +
+curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"
diff --git a/lnbits/extensions/captcha/__init__.py b/lnbits/extensions/captcha/__init__.py
new file mode 100644
index 000000000..f25dccce2
--- /dev/null
+++ b/lnbits/extensions/captcha/__init__.py
@@ -0,0 +1,12 @@
+from quart import Blueprint
+from lnbits.db import Database
+
+db = Database("ext_captcha")
+
+captcha_ext: Blueprint = Blueprint(
+ "captcha", __name__, static_folder="static", template_folder="templates"
+)
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/captcha/config.json b/lnbits/extensions/captcha/config.json
new file mode 100644
index 000000000..4ef7c43fb
--- /dev/null
+++ b/lnbits/extensions/captcha/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Captcha",
+ "short_description": "Create captcha to stop spam",
+ "icon": "block",
+ "contributors": ["pseudozach"]
+}
diff --git a/lnbits/extensions/captcha/crud.py b/lnbits/extensions/captcha/crud.py
new file mode 100644
index 000000000..43a0374e1
--- /dev/null
+++ b/lnbits/extensions/captcha/crud.py
@@ -0,0 +1,53 @@
+from typing import List, Optional, Union
+
+from lnbits.helpers import urlsafe_short_hash
+
+from . import db
+from .models import Captcha
+
+
+async def create_captcha(
+ *,
+ wallet_id: str,
+ url: str,
+ memo: str,
+ description: Optional[str] = None,
+ amount: int = 0,
+ remembers: bool = True,
+) -> Captcha:
+ captcha_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO captcha.captchas (id, wallet, url, memo, description, amount, remembers)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ (captcha_id, wallet_id, url, memo, description, amount, int(remembers)),
+ )
+
+ captcha = await get_captcha(captcha_id)
+ assert captcha, "Newly created captcha couldn't be retrieved"
+ return captcha
+
+
+async def get_captcha(captcha_id: str) -> Optional[Captcha]:
+ row = await db.fetchone(
+ "SELECT * FROM captcha.captchas WHERE id = ?", (captcha_id,)
+ )
+
+ return Captcha.from_row(row) if row else None
+
+
+async def get_captchas(wallet_ids: Union[str, List[str]]) -> List[Captcha]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM captcha.captchas WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ return [Captcha.from_row(row) for row in rows]
+
+
+async def delete_captcha(captcha_id: str) -> None:
+ await db.execute("DELETE FROM captcha.captchas WHERE id = ?", (captcha_id,))
diff --git a/lnbits/extensions/captcha/migrations.py b/lnbits/extensions/captcha/migrations.py
new file mode 100644
index 000000000..744fc5067
--- /dev/null
+++ b/lnbits/extensions/captcha/migrations.py
@@ -0,0 +1,63 @@
+async def m001_initial(db):
+ """
+ Initial captchas table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE captcha.captchas (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ secret TEXT NOT NULL,
+ url TEXT NOT NULL,
+ memo TEXT NOT NULL,
+ amount INTEGER NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
+
+
+async def m002_redux(db):
+ """
+ Creates an improved captchas table and migrates the existing data.
+ """
+ await db.execute("ALTER TABLE captcha.captchas RENAME TO captchas_old")
+ await db.execute(
+ """
+ CREATE TABLE captcha.captchas (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ url TEXT NOT NULL,
+ memo TEXT NOT NULL,
+ description TEXT NULL,
+ amount INTEGER DEFAULT 0,
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """,
+ remembers INTEGER DEFAULT 0,
+ extras TEXT NULL
+ );
+ """
+ )
+
+ for row in [
+ list(row) for row in await db.fetchall("SELECT * FROM captcha.captchas_old")
+ ]:
+ await db.execute(
+ """
+ INSERT INTO captcha.captchas (
+ id,
+ wallet,
+ url,
+ memo,
+ amount,
+ time
+ )
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (row[0], row[1], row[3], row[4], row[5], row[6]),
+ )
+
+ await db.execute("DROP TABLE captcha.captchas_old")
diff --git a/lnbits/extensions/captcha/models.py b/lnbits/extensions/captcha/models.py
new file mode 100644
index 000000000..2b98a91e4
--- /dev/null
+++ b/lnbits/extensions/captcha/models.py
@@ -0,0 +1,24 @@
+import json
+
+from sqlite3 import Row
+from pydantic import BaseModel
+from typing import Optional
+
+
+class Captcha(BaseModel):
+ id: str
+ wallet: str
+ url: str
+ memo: str
+ description: str
+ amount: int
+ time: int
+ remembers: bool
+ extras: Optional[dict]
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Captcha":
+ data = dict(row)
+ data["remembers"] = bool(data["remembers"])
+ data["extras"] = json.loads(data["extras"]) if data["extras"] else None
+ return cls(**data)
diff --git a/lnbits/extensions/captcha/static/js/captcha.js b/lnbits/extensions/captcha/static/js/captcha.js
new file mode 100644
index 000000000..1da24f572
--- /dev/null
+++ b/lnbits/extensions/captcha/static/js/captcha.js
@@ -0,0 +1,82 @@
+var ciframeLoaded = !1,
+ captchaStyleAdded = !1
+
+function ccreateIframeElement(t = {}) {
+ const e = document.createElement('iframe')
+ // e.style.marginLeft = "25px",
+ ;(e.style.border = 'none'),
+ (e.style.width = '100%'),
+ (e.style.height = '100%'),
+ (e.scrolling = 'no'),
+ (e.id = 'captcha-iframe')
+ t.dest, t.amount, t.currency, t.label, t.opReturn
+ var captchaid = document
+ .getElementById('captchascript')
+ .getAttribute('data-captchaid')
+ var lnbhostsrc = document.getElementById('captchascript').getAttribute('src')
+ var lnbhost = lnbhostsrc.split('/captcha/static/js/captcha.js')[0]
+ return (e.src = lnbhost + '/captcha/' + captchaid), e
+}
+document.addEventListener('DOMContentLoaded', function () {
+ if (captchaStyleAdded) console.log('Captcha already added!')
+ else {
+ console.log('Adding captcha'), (captchaStyleAdded = !0)
+ var t = document.createElement('style')
+ t.innerHTML =
+ "\t/*Button*/\t\t.button-captcha-filled\t\t\t{\t\t\tdisplay: flex;\t\t\talign-items: center;\t\t\tjustify-content: center;\t\t\twidth: 120px;\t\t\tmin-width: 30px;\t\t\theight: 40px;\t\t\tline-height: 2.5;\t\t\ttext-align: center;\t\t\tcursor: pointer;\t\t\t/* Rectangle 2: */\t\t\tbackground: #FF7979;\t\t\tbox-shadow: 0 2px 4px 0 rgba(0,0,0,0.20);\t\t\tborder-radius: 20px;\t\t\t/* Sign up: */\t\t\tfont-family: 'Avenir-Heavy', Futura, Helvetica, Arial;\t\t\tfont-size: 16px;\t\t\tcolor: #FFFFFF;\t\t}\t\t.button-captcha-filled:hover\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #FF7979;\t\t\tbox-shadow: 0 0 4px 0 rgba(0,0,0,0.20);\t\t}\t\t.button-captcha-filled:active\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #FF7979;\t\t\t/*Move it down a little bit*/\t\t\tposition: relative;\t\t\ttop: 1px;\t\t}\t\t.button-captcha-filled-dark\t\t\t{\t\t\tdisplay: flex;\t\t\talign-items: center;\t\t\tjustify-content: center;\t\t\twidth: 120px;\t\t\tmin-width: 30px;\t\t\theight: 40px;\t\t\tline-height: 2.5;\t\t\ttext-align: center;\t\t\tcursor: pointer;\t\t\t/* Rectangle 2: */\t\t\tbackground: #161C38;\t\t\tbox-shadow: 0 0px 4px 0 rgba(0,0,0,0.20);\t\t\tborder-radius: 20px;\t\t\t/* Sign up: */\t\t\tfont-family: 'Avenir-Heavy', Futura, Helvetica, Arial;\t\t\tfont-size: 16px;\t\t\tcolor: #FFFFFF;\t\t}\t\t.button-captcha-filled-dark:hover\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #161C38;\t\t\tbox-shadow: 0 0px 4px 0 rgba(0,0,0,0.20);\t\t}\t\t.button-captcha-filled-dark:active\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #161C38;\t\t\t/*Move it down a little bit*/\t\t\tposition: relative;\t\t\ttop: 1px;\t\t}\t\t.modal-captcha-container {\t\t position: fixed;\t\t z-index: 1000;\t\t text-align: left;/*Si no añado esto, a veces hereda el text-align:center del body, y entonces el popup queda movido a la derecha, por center + margin left que aplico*/\t\t left: 0;\t\t top: 0;\t\t width: 100%;\t\t height: 100%;\t\t background-color: rgba(0, 0, 0, 0.5);\t\t opacity: 0;\t\t visibility: hidden;\t\t transform: scale(1.1);\t\t transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;\t\t}\t\t.modal-captcha-content {\t\t position: absolute;\t\t top: 50%;\t\t left: 50%;\t\t transform: translate(-50%, -50%);\t\t background-color: white;\t\t width: 100%;\t\t height: 100%;\t\t border-radius: 0.5rem;\t\t /*Rounded shadowed borders*/\t\t\tbox-shadow: 2px 2px 4px 0 rgba(0,0,0,0.15);\t\t\tborder-radius: 5px;\t\t}\t\t.close-button-captcha {\t\t float: right;\t\t width: 1.5rem;\t\t line-height: 1.5rem;\t\t text-align: center;\t\t cursor: pointer;\t\t margin-right:20px;\t\t margin-top:10px;\t\t border-radius: 0.25rem;\t\t background-color: lightgray;\t\t}\t\t.close-button-captcha:hover {\t\t background-color: darkgray;\t\t}\t\t.show-modal-captcha {\t\t opacity: 1;\t\t visibility: visible;\t\t transform: scale(1.0);\t\t transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;\t\t}\t\t/* Mobile */\t\t@media screen and (min-device-width: 160px) and ( max-width: 1077px ) /*No tendria ni por que poner un min-device, porq abarca todo lo humano...*/\t\t{\t\t}"
+ var e = document.querySelector('script')
+ e.parentNode.insertBefore(t, e)
+ var i = document.getElementById('captchacheckbox'),
+ n = i.dataset,
+ o = 'true' === n.dark
+ var a = document.createElement('div')
+ ;(a.className += ' modal-captcha-container'),
+ (a.innerHTML =
+ '\t\tGET /captcha/api/v1/captchas
+ {"X-Api-Key": <invoice_key>}
[<captcha_object>, ...]
+ curl -X GET {{ request.url_root }}captcha/api/v1/captchas -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ POST /captcha/api/v1/captchas
+ {"X-Api-Key": <admin_key>}
{"amount": <integer>, "description": <string>, "memo":
+ <string>, "remembers": <boolean>, "url":
+ <string>}
+ {"amount": <integer>, "description": <string>, "id":
+ <string>, "memo": <string>, "remembers": <boolean>,
+ "time": <int>, "url": <string>, "wallet":
+ <string>}
+ curl -X POST {{ request.url_root }}captcha/api/v1/captchas -d
+ '{"url": <string>, "memo": <string>, "description":
+ <string>, "amount": <integer>, "remembers":
+ <boolean>}' -H "Content-type: application/json" -H "X-Api-Key:
+ {{ g.user.wallets[0].adminkey }}"
+
+ POST
+ /captcha/api/v1/captchas/<captcha_id>/invoice
+ {"amount": <integer>}
+ {"payment_hash": <string>, "payment_request":
+ <string>}
+ curl -X POST {{ request.url_root
+ }}captcha/api/v1/captchas/<captcha_id>/invoice -d '{"amount":
+ <integer>}' -H "Content-type: application/json"
+
+ POST
+ /captcha/api/v1/captchas/<captcha_id>/check_invoice
+ {"payment_hash": <string>}
+ {"paid": false}
{"paid": true, "url": <string>, "remembers":
+ <boolean>}
+ curl -X POST {{ request.url_root
+ }}captcha/api/v1/captchas/<captcha_id>/check_invoice -d
+ '{"payment_hash": <string>}' -H "Content-type: application/json"
+
+ DELETE
+ /captcha/api/v1/captchas/<captcha_id>
+ {"X-Api-Key": <admin_key>}
+ curl -X DELETE {{ request.url_root
+ }}captcha/api/v1/captchas/<captcha_id> -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ {{ captcha.description }}
+ {% endif %} +
+ Captcha accepted. You are probably human.
+
+
+ {{ qrCodeDialog.data.snippet }}
+
+ + Copy the snippet above and paste into your website/form. The checkbox + can be in checked state only after user pays. +
+
+ ID: {{ qrCodeDialog.data.id }}
+ Amount: {{ qrCodeDialog.data.amount }}
+
+
curl -X GET http://YOUR-TOR-ADDRESS
diff --git a/lnbits/extensions/diagonalley/__init__.py b/lnbits/extensions/diagonalley/__init__.py
new file mode 100644
index 000000000..ac907f5c7
--- /dev/null
+++ b/lnbits/extensions/diagonalley/__init__.py
@@ -0,0 +1,10 @@
+from quart import Blueprint
+
+
+diagonalley_ext: Blueprint = Blueprint(
+ "diagonalley", __name__, static_folder="static", template_folder="templates"
+)
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/diagonalley/config.json.example b/lnbits/extensions/diagonalley/config.json.example
new file mode 100644
index 000000000..057d0f234
--- /dev/null
+++ b/lnbits/extensions/diagonalley/config.json.example
@@ -0,0 +1,6 @@
+{
+ "name": "Diagon Alley",
+ "short_description": "Movable anonymous market stand",
+ "icon": "add_shopping_cart",
+ "contributors": ["benarc"]
+}
diff --git a/lnbits/extensions/diagonalley/crud.py b/lnbits/extensions/diagonalley/crud.py
new file mode 100644
index 000000000..971cd449d
--- /dev/null
+++ b/lnbits/extensions/diagonalley/crud.py
@@ -0,0 +1,308 @@
+from base64 import urlsafe_b64encode
+from uuid import uuid4
+from typing import List, Optional, Union
+import httpx
+from lnbits.db import open_ext_db
+from lnbits.settings import WALLET
+from .models import Products, Orders, Indexers
+import re
+
+regex = re.compile(
+ r"^(?:http|ftp)s?://" # http:// or https://
+ r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"
+ r"localhost|"
+ r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
+ r"(?::\d+)?"
+ r"(?:/?|[/?]\S+)$",
+ re.IGNORECASE,
+)
+
+###Products
+
+
+def create_diagonalleys_product(
+ *,
+ wallet_id: str,
+ product: str,
+ categories: str,
+ description: str,
+ image: str,
+ price: int,
+ quantity: int,
+) -> Products:
+ with open_ext_db("diagonalley") as db:
+ product_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
+ db.execute(
+ """
+ INSERT INTO diagonalley.products (id, wallet, product, categories, description, image, price, quantity)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ product_id,
+ wallet_id,
+ product,
+ categories,
+ description,
+ image,
+ price,
+ quantity,
+ ),
+ )
+
+ return get_diagonalleys_product(product_id)
+
+
+def update_diagonalleys_product(product_id: str, **kwargs) -> Optional[Indexers]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+
+ with open_ext_db("diagonalley") as db:
+ db.execute(
+ f"UPDATE diagonalley.products SET {q} WHERE id = ?",
+ (*kwargs.values(), product_id),
+ )
+ row = db.fetchone(
+ "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,)
+ )
+
+ return get_diagonalleys_indexer(product_id)
+
+
+def get_diagonalleys_product(product_id: str) -> Optional[Products]:
+ with open_ext_db("diagonalley") as db:
+ row = db.fetchone(
+ "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,)
+ )
+
+ return Products(**row) if row else None
+
+
+def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Products]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ with open_ext_db("diagonalley") as db:
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = db.fetchall(
+ f"SELECT * FROM diagonalley.products WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ return [Products(**row) for row in rows]
+
+
+def delete_diagonalleys_product(product_id: str) -> None:
+ with open_ext_db("diagonalley") as db:
+ db.execute("DELETE FROM diagonalley.products WHERE id = ?", (product_id,))
+
+
+###Indexers
+
+
+def create_diagonalleys_indexer(
+ wallet_id: str,
+ shopname: str,
+ indexeraddress: str,
+ shippingzone1: str,
+ shippingzone2: str,
+ zone1cost: int,
+ zone2cost: int,
+ email: str,
+) -> Indexers:
+ with open_ext_db("diagonalley") as db:
+ indexer_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
+ db.execute(
+ """
+ INSERT INTO diagonalley.indexers (id, wallet, shopname, indexeraddress, online, rating, shippingzone1, shippingzone2, zone1cost, zone2cost, email)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ indexer_id,
+ wallet_id,
+ shopname,
+ indexeraddress,
+ False,
+ 0,
+ shippingzone1,
+ shippingzone2,
+ zone1cost,
+ zone2cost,
+ email,
+ ),
+ )
+ return get_diagonalleys_indexer(indexer_id)
+
+
+def update_diagonalleys_indexer(indexer_id: str, **kwargs) -> Optional[Indexers]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+
+ with open_ext_db("diagonalley") as db:
+ db.execute(
+ f"UPDATE diagonalley.indexers SET {q} WHERE id = ?",
+ (*kwargs.values(), indexer_id),
+ )
+ row = db.fetchone(
+ "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,)
+ )
+
+ return get_diagonalleys_indexer(indexer_id)
+
+
+def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]:
+ with open_ext_db("diagonalley") as db:
+ roww = db.fetchone(
+ "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,)
+ )
+ try:
+ x = httpx.get(roww["indexeraddress"] + "/" + roww["ratingkey"])
+ if x.status_code == 200:
+ print(x)
+ print("poo")
+ with open_ext_db("diagonalley") as db:
+ db.execute(
+ "UPDATE diagonalley.indexers SET online = ? WHERE id = ?",
+ (
+ True,
+ indexer_id,
+ ),
+ )
+ else:
+ with open_ext_db("diagonalley") as db:
+ db.execute(
+ "UPDATE diagonalley.indexers SET online = ? WHERE id = ?",
+ (
+ False,
+ indexer_id,
+ ),
+ )
+ except:
+ print("An exception occurred")
+ with open_ext_db("diagonalley") as db:
+ row = db.fetchone(
+ "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,)
+ )
+ return Indexers(**row) if row else None
+
+
+def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexers]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ with open_ext_db("diagonalley") as db:
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = db.fetchall(
+ f"SELECT * FROM diagonalley.indexers WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+
+ for r in rows:
+ try:
+ x = httpx.get(r["indexeraddress"] + "/" + r["ratingkey"])
+ if x.status_code == 200:
+ with open_ext_db("diagonalley") as db:
+ db.execute(
+ "UPDATE diagonalley.indexers SET online = ? WHERE id = ?",
+ (
+ True,
+ r["id"],
+ ),
+ )
+ else:
+ with open_ext_db("diagonalley") as db:
+ db.execute(
+ "UPDATE diagonalley.indexers SET online = ? WHERE id = ?",
+ (
+ False,
+ r["id"],
+ ),
+ )
+ except:
+ print("An exception occurred")
+ with open_ext_db("diagonalley") as db:
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = db.fetchall(
+ f"SELECT * FROM diagonalley.indexers WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+ return [Indexers(**row) for row in rows]
+
+
+def delete_diagonalleys_indexer(indexer_id: str) -> None:
+ with open_ext_db("diagonalley") as db:
+ db.execute("DELETE FROM diagonalley.indexers WHERE id = ?", (indexer_id,))
+
+
+###Orders
+
+
+def create_diagonalleys_order(
+ *,
+ productid: str,
+ wallet: str,
+ product: str,
+ quantity: int,
+ shippingzone: str,
+ address: str,
+ email: str,
+ invoiceid: str,
+ paid: bool,
+ shipped: bool,
+) -> Indexers:
+ with open_ext_db("diagonalley") as db:
+ order_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
+ db.execute(
+ """
+ INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ order_id,
+ productid,
+ wallet,
+ product,
+ quantity,
+ shippingzone,
+ address,
+ email,
+ invoiceid,
+ False,
+ False,
+ ),
+ )
+
+ return get_diagonalleys_order(order_id)
+
+
+def get_diagonalleys_order(order_id: str) -> Optional[Orders]:
+ with open_ext_db("diagonalley") as db:
+ row = db.fetchone("SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,))
+
+ return Orders(**row) if row else None
+
+
+def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ with open_ext_db("diagonalley") as db:
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = db.fetchall(
+ f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+ for r in rows:
+ PAID = (await WALLET.get_invoice_status(r["invoiceid"])).paid
+ if PAID:
+ with open_ext_db("diagonalley") as db:
+ db.execute(
+ "UPDATE diagonalley.orders SET paid = ? WHERE id = ?",
+ (
+ True,
+ r["id"],
+ ),
+ )
+ rows = db.fetchall(
+ f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})",
+ (*wallet_ids,),
+ )
+ return [Orders(**row) for row in rows]
+
+
+def delete_diagonalleys_order(order_id: str) -> None:
+ with open_ext_db("diagonalley") as db:
+ db.execute("DELETE FROM diagonalley.orders WHERE id = ?", (order_id,))
diff --git a/lnbits/extensions/diagonalley/migrations.py b/lnbits/extensions/diagonalley/migrations.py
new file mode 100644
index 000000000..9f2b787f9
--- /dev/null
+++ b/lnbits/extensions/diagonalley/migrations.py
@@ -0,0 +1,60 @@
+async def m001_initial(db):
+ """
+ Initial products table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE diagonalley.products (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ product TEXT NOT NULL,
+ categories TEXT NOT NULL,
+ description TEXT NOT NULL,
+ image TEXT NOT NULL,
+ price INTEGER NOT NULL,
+ quantity INTEGER NOT NULL
+ );
+ """
+ )
+
+ """
+ Initial indexers table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE diagonalley.indexers (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ shopname TEXT NOT NULL,
+ indexeraddress TEXT NOT NULL,
+ online BOOLEAN NOT NULL,
+ rating INTEGER NOT NULL,
+ shippingzone1 TEXT NOT NULL,
+ shippingzone2 TEXT NOT NULL,
+ zone1cost INTEGER NOT NULL,
+ zone2cost INTEGER NOT NULL,
+ email TEXT NOT NULL
+ );
+ """
+ )
+
+ """
+ Initial orders table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE diagonalley.orders (
+ id TEXT PRIMARY KEY,
+ productid TEXT NOT NULL,
+ wallet TEXT NOT NULL,
+ product TEXT NOT NULL,
+ quantity INTEGER NOT NULL,
+ shippingzone INTEGER NOT NULL,
+ address TEXT NOT NULL,
+ email TEXT NOT NULL,
+ invoiceid TEXT NOT NULL,
+ paid BOOLEAN NOT NULL,
+ shipped BOOLEAN NOT NULL
+ );
+ """
+ )
diff --git a/lnbits/extensions/diagonalley/models.py b/lnbits/extensions/diagonalley/models.py
new file mode 100644
index 000000000..ab1c592de
--- /dev/null
+++ b/lnbits/extensions/diagonalley/models.py
@@ -0,0 +1,41 @@
+from typing import NamedTuple
+from sqlite3 import Row
+from pydantic import BaseModel
+
+class Indexers(BaseModel):
+ id: str
+ wallet: str
+ shopname: str
+ indexeraddress: str
+ online: bool
+ rating: str
+ shippingzone1: str
+ shippingzone2: str
+ zone1cost: int
+ zone2cost: int
+ email: str
+
+
+class Products(BaseModel):
+ id: str
+ wallet: str
+ product: str
+ categories: str
+ description: str
+ image: str
+ price: int
+ quantity: int
+
+
+class Orders(BaseModel):
+ id: str
+ productid: str
+ wallet: str
+ product: str
+ quantity: int
+ shippingzone: int
+ address: str
+ email: str
+ invoiceid: str
+ paid: bool
+ shipped: bool
diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html b/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html
new file mode 100644
index 000000000..585e8d7c8
--- /dev/null
+++ b/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html
@@ -0,0 +1,122 @@
+
+ Make a list of products to sell, point your list of products at a public
+ indexer. Buyers browse your products on the indexer, and pay you
+ directly. Ratings are managed by the indexer. Your stall can be listed
+ in multiple indexers, even over TOR, if you wish to be anonymous.
+ More information on the
+ Diagon Alley Protocol
+
+ Created by, Ben Arc
+
GET
+ /api/v1/diagonalley/stall/products/<indexer_id>
+ Product JSON list
+ curl -X GET {{ request.url_root
+ }}diagonalley/api/v1/diagonalley/stall/products/<indexer_id>
+ POST
+ /api/v1/diagonalley/stall/order/<indexer_id>
+ {"id": <string>, "address": <string>, "shippingzone":
+ <integer>, "email": <string>, "quantity":
+ <integer>}
+ {"checking_id": <string>,"payment_request":
+ <string>}
+ curl -X POST {{ request.url_root
+ }}diagonalley/api/v1/diagonalley/stall/order/<indexer_id> -d
+ '{"id": <product_id&>, "email": <customer_email>,
+ "address": <customer_address>, "quantity": 2, "shippingzone":
+ 1}' -H "Content-type: application/json"
+
+ GET
+ /diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id>
+ {"shipped": <boolean>}
+ curl -X GET {{ request.url_root
+ }}diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id>
+ -H "Content-type: application/json"
+
+ Events alows you to make a wave of tickets for an event, each ticket is
+ in the form of a unqiue QRcode, which the user presents at registration.
+ Events comes with a shareable ticket scanner, which can be used to
+ register attendees.
+
+ Created by, Ben Arc
+
+
You'll be redirected in a few moments...
+curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"
diff --git a/lnbits/extensions/example/__init__.py b/lnbits/extensions/example/__init__.py
new file mode 100644
index 000000000..e16e0372f
--- /dev/null
+++ b/lnbits/extensions/example/__init__.py
@@ -0,0 +1,12 @@
+from quart import Blueprint
+from lnbits.db import Database
+
+db = Database("ext_example")
+
+example_ext: Blueprint = Blueprint(
+ "example", __name__, static_folder="static", template_folder="templates"
+)
+
+
+from .views_api import * # noqa
+from .views import * # noqa
diff --git a/lnbits/extensions/example/config.json b/lnbits/extensions/example/config.json
new file mode 100644
index 000000000..55389373b
--- /dev/null
+++ b/lnbits/extensions/example/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Build your own!",
+ "short_description": "Join us, make an extension",
+ "icon": "info",
+ "contributors": ["github_username"]
+}
diff --git a/lnbits/extensions/example/migrations.py b/lnbits/extensions/example/migrations.py
new file mode 100644
index 000000000..99d7c362d
--- /dev/null
+++ b/lnbits/extensions/example/migrations.py
@@ -0,0 +1,10 @@
+# async def m001_initial(db):
+# await db.execute(
+# f"""
+# CREATE TABLE example.example (
+# id TEXT PRIMARY KEY,
+# wallet TEXT NOT NULL,
+# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
+# );
+# """
+# )
diff --git a/lnbits/extensions/example/models.py b/lnbits/extensions/example/models.py
new file mode 100644
index 000000000..be5232339
--- /dev/null
+++ b/lnbits/extensions/example/models.py
@@ -0,0 +1,11 @@
+# from sqlite3 import Row
+# from typing import NamedTuple
+
+
+# class Example(NamedTuple):
+# id: str
+# wallet: str
+#
+# @classmethod
+# def from_row(cls, row: Row) -> "Example":
+# return cls(**dict(row))
diff --git a/lnbits/extensions/example/templates/example/index.html b/lnbits/extensions/example/templates/example/index.html
new file mode 100644
index 000000000..d732ef376
--- /dev/null
+++ b/lnbits/extensions/example/templates/example/index.html
@@ -0,0 +1,59 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
++ A magical "g" is always available, with info about the user, wallets and + extensions: +
+{% raw %}{{ g }}{% endraw %}
+ + Hivemind is a Bitcoin sidechain + project for a peer-to-peer oracle protocol that absorbs accurate data into + a blockchain so that Bitcoin users can speculate in prediction markets. +
++ These markets have the potential to revolutionize the emergence of + diffusion of knowledge in society and fix all sorts of problems in the + world. +
++ This extension will become fully operative when the + BIP300 soft-fork gets activated and + Bitcoin Hivemind is launched. +
+GET /jukebox/api/v1/jukebox
+ {"X-Api-Key": <admin_key>}
[<jukebox_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ GET
+ /jukebox/api/v1/jukebox/<juke_id>
+ {"X-Api-Key": <admin_key>}
<jukebox_object>
+ curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+ POST/PUT
+ /jukebox/api/v1/jukebox/
+ {"X-Api-Key": <admin_key>}
<jukbox_object>
+ curl -X POST {{ request.url_root }}api/v1/jukebox/ -d '{"user":
+ <string, user_id>, "title": <string>,
+ "wallet":<string>, "sp_user": <string,
+ spotify_user_account>, "sp_secret": <string,
+ spotify_user_secret>, "sp_access_token": <string,
+ not_required>, "sp_refresh_token": <string, not_required>,
+ "sp_device": <string, spotify_user_secret>, "sp_playlists":
+ <string, not_required>, "price": <integer, not_required>}'
+ -H "Content-type: application/json" -H "X-Api-Key:
+ {{g.user.wallets[0].adminkey }}"
+
+ DELETE
+ /jukebox/api/v1/jukebox/<juke_id>
+ {"X-Api-Key": <admin_key>}
<jukebox_object>
+ curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+ Currently playing
+Pick a song
+Add tracks, profit.
+GET
+ /livestream/api/v1/livestream
+ {"X-Api-Key": <invoice_key>}
[<livestream_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ PUT
+ /livestream/api/v1/livestream/track/<track_id>
+ {"X-Api-Key": <invoice_key>}
curl -X PUT {{ request.url_root
+ }}api/v1/livestream/track/<track_id> -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ PUT
+ /livestream/api/v1/livestream/fee/<fee_pct>
+ {"X-Api-Key": <invoice_key>}
curl -X PUT {{ request.url_root
+ }}api/v1/livestream/fee/<fee_pct> -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ POST
+ /livestream/api/v1/livestream/tracks
+ {"X-Api-Key": <invoice_key>}
{"name": <string>, "download_url": <string>,
+ "price_msat": <integer>, "producer_id": <integer>,
+ "producer_name": <string>}
+ curl -X POST {{ request.url_root }}api/v1/livestream/tracks -d
+ '{"name": <string>, "download_url": <string>,
+ "price_msat": <integer>, "producer_id": <integer>,
+ "producer_name": <string>}' -H "Content-type: application/json"
+ -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+ DELETE
+ /livestream/api/v1/livestream/tracks/<track_id>
+ {"X-Api-Key": <invoice_key>}
+ curl -X DELETE {{ request.url_root
+ }}api/v1/livestream/tracks/<track_id> -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ + Standalone QR Code for this track +
+ +Add a wallet / Import wallet
on BlueWallet or
+ Settings / Add a new node
on Zeus.
+ + LndHub is a protocol invented by + BlueWallet that allows mobile + wallets to query payments and balances, generate invoices and make + payments from accounts that exist on a server. The protocol is a + collection of HTTP endpoints exposed through the internet. +
++ For a wallet that supports it, reading a QR code that contains the URL + along with secret access credentials should enable access. Currently it + is supported by Zeus and + BlueWallet. +
+
+ Charge people per word for contacting you. Possible applications incude,
+ paid support ticketing, PAYG language services, contact spam
+ protection.
+
+ Created by, Ben Arc
+
{% raw %}{{amountWords}}{% endraw %}
+GET /lnurlp/api/v1/links
+ {"X-Api-Key": <invoice_key>}
[<pay_link_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/links -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ GET /lnurlp/api/v1/links/<pay_id>
+ {"X-Api-Key": <invoice_key>}
{"lnurl": <string>}
+ curl -X GET {{ request.url_root }}api/v1/links/<pay_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ POST /lnurlp/api/v1/links
+ {"X-Api-Key": <admin_key>}
{"description": <string> "amount": <integer> "max": <integer> "min": <integer> "comment_chars": <integer>}
+ {"lnurl": <string>}
+ curl -X POST {{ request.url_root }}api/v1/links -d '{"description":
+ <string>, "amount": <integer>, "max": <integer>, "min": <integer>, "comment_chars": <integer>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+ PUT
+ /lnurlp/api/v1/links/<pay_id>
+ {"X-Api-Key": <admin_key>}
{"description": <string>, "amount": <integer>}
+ {"lnurl": <string>}
+ curl -X PUT {{ request.url_root }}api/v1/links/<pay_id> -d
+ '{"description": <string>, "amount": <integer>}' -H
+ "Content-type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ DELETE
+ /lnurlp/api/v1/links/<pay_id>
+ {"X-Api-Key": <admin_key>}
+ curl -X DELETE {{ request.url_root }}api/v1/links/<pay_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+
+ WARNING: LNURL must be used over https or TOR
+ LNURL is a range of lightning-network standards that allow us to use
+ lightning-network differently. An LNURL-pay is a link that wallets use
+ to fetch an invoice from a server on-demand. The link or QR code is
+ fixed, but each time it is read by a compatible wallet a new QR code is
+ issued by the service. It can be used to activate machines without them
+ having to maintain an electronic screen to generate and show invoices
+ locally, or to sell any predefined good or service automatically.
+
+ Exploring LNURL and finding use cases, is really helping inform + lightning protocol development, rather than the protocol dictating how + lightning-network should be engaged with. +
+ Check + Awesome LNURL + for further information. +Use an LNURL compatible bitcoin wallet to pay.
+
+ ID: {{ qrCodeDialog.data.id }}
+ Amount: {{ qrCodeDialog.data.amount }}
+ {{ qrCodeDialog.data.currency }} price: {{
+ fiatRates[qrCodeDialog.data.currency] ?
+ fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}
+ Accepts comments: {{ qrCodeDialog.data.comments }}
+ Dispatches webhook to: {{ qrCodeDialog.data.webhook
+ }}
+ On success: {{ qrCodeDialog.data.success }}
+
+ Note that if you restart your device, your device will generate a + new url. If anyone is using your old one for wallets, lnurls, + etc., whatever they are doing will stop working. +
+ Created by + Supertestnet. ++ The confirmation codes are words from a predefined sequential word list. + Each new payment bumps the words sequence by 1. So you can check the + confirmation codes manually by just looking at them. +
+
+ For example, if your wordlist is
+ [apple, banana, coconut]
the first purchase will be
+ apple
, the second banana
and so on. When it
+ gets to the end it starts from the beginning again.
+
Powered by LNURL-pay.
+POST
+ {"X-Api-Key": <invoice_key>}
curl -X GET {{ request.url_root
+ }}/offlineshop/api/v1/offlineshop/items -H "Content-Type:
+ application/json" -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -d
+ '{"name": <string>, "description": <string>, "image":
+ <data-uri string>, "price": <integer>, "unit": <"sat"
+ or "USD">}'
+
+ GET
+ {"X-Api-Key": <invoice_key>}
{"id": <integer>, "wallet": <string>, "wordlist":
+ <string>, "items": [{"id": <integer>, "name":
+ <string>, "description": <string>, "image":
+ <string>, "enabled": <boolean>, "price": <integer>,
+ "unit": <string>, "lnurl": <string>}, ...]}<
+ curl -X GET {{ request.url_root }}/offlineshop/api/v1/offlineshop -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ PUT
+ {"X-Api-Key": <invoice_key>}
curl -X GET {{ request.url_root
+ }}/offlineshop/api/v1/offlineshop/items/<item_id> -H
+ "Content-Type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}" -d '{"name": <string>,
+ "description": <string>, "image": <data-uri string>,
+ "price": <integer>, "unit": <"sat" or "USD">}'
+
+ DELETE
+ {"X-Api-Key": <invoice_key>}
curl -X GET {{ request.url_root
+ }}/offlineshop/api/v1/offlineshop/items/<item_id> -H "X-Api-Key:
+ {{ g.user.wallets[0].inkey }}"
+
+ + Setting this option disables the confirmation code message that + appears in the consumer wallet after a purchase is paid for. It's ok + if the consumer is to be trusted when they claim to have paid. +
+ +GET /paywall/api/v1/paywalls
+ {"X-Api-Key": <invoice_key>}
[<paywall_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/paywalls -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ POST /paywall/api/v1/paywalls
+ {"X-Api-Key": <admin_key>}
{"amount": <integer>, "description": <string>, "memo":
+ <string>, "remembers": <boolean>, "url":
+ <string>}
+ {"amount": <integer>, "description": <string>, "id":
+ <string>, "memo": <string>, "remembers": <boolean>,
+ "time": <int>, "url": <string>, "wallet":
+ <string>}
+ curl -X POST {{ request.url_root }}api/v1/paywalls -d '{"url":
+ <string>, "memo": <string>, "description": <string>,
+ "amount": <integer>, "remembers": <boolean>}' -H
+ "Content-type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ POST
+ /paywall/api/v1/paywalls/<paywall_id>/invoice
+ {"amount": <integer>}
+ {"payment_hash": <string>, "payment_request":
+ <string>}
+ curl -X POST {{ request.url_root
+ }}api/v1/paywalls/<paywall_id>/invoice -d '{"amount":
+ <integer>}' -H "Content-type: application/json"
+
+ POST
+ /paywall/api/v1/paywalls/<paywall_id>/check_invoice
+ {"payment_hash": <string>}
+ {"paid": false}
{"paid": true, "url": <string>, "remembers":
+ <boolean>}
+ curl -X POST {{ request.url_root
+ }}api/v1/paywalls/<paywall_id>/check_invoice -d
+ '{"payment_hash": <string>}' -H "Content-type: application/json"
+
+ DELETE
+ /paywall/api/v1/paywalls/<paywall_id>
+ {"X-Api-Key": <admin_key>}
+ curl -X DELETE {{ request.url_root
+ }}api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ {{ paywall.description }}
+ {% endif %} +
+ You can access the URL behind this paywall:
+ {% raw %}{{ redirectUrl }}{% endraw %}
+
+ SatsPayServer, create Onchain/LN charges.
WARNING: If using with the
+ WatchOnly extension, we highly reccomend using a fresh extended public Key
+ specifically for SatsPayServer!
+
+ Created by, Ben Arc
+
POST /satspay/api/v1/charge
+ {"X-Api-Key": <admin_key>}
[<charge_object>, ...]
+ curl -X POST {{ request.url_root }}api/v1/charge -d
+ '{"onchainwallet": <string, watchonly_wallet_id>,
+ "description": <string>, "webhook":<string>, "time":
+ <integer>, "amount": <integer>, "lnbitswallet":
+ <string, lnbits_wallet_id>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
+
+ PUT
+ /satspay/api/v1/charge/<charge_id>
+ {"X-Api-Key": <admin_key>}
[<charge_object>, ...]
+ curl -X POST {{ request.url_root }}api/v1/charge/<charge_id>
+ -d '{"onchainwallet": <string, watchonly_wallet_id>,
+ "description": <string>, "webhook":<string>, "time":
+ <integer>, "amount": <integer>, "lnbitswallet":
+ <string, lnbits_wallet_id>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
+
+ GET
+ /satspay/api/v1/charge/<charge_id>
+ {"X-Api-Key": <invoice_key>}
[<charge_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/charge/<charge_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ GET /satspay/api/v1/charges
+ {"X-Api-Key": <invoice_key>}
[<charge_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/charges -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ DELETE
+ /satspay/api/v1/charge/<charge_id>
+ {"X-Api-Key": <admin_key>}
+ curl -X DELETE {{ request.url_root
+ }}api/v1/charge/<charge_id> -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ GET
+ /satspay/api/v1/charges/balance/<charge_id>
+ [<charge_object>, ...]
+ curl -X GET {{ request.url_root
+ }}api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ + Add some wallets to the list of "Target Wallets", each with an + associated percent. After saving, every time any payment + arrives at the "Source Wallet" that payment will be split with the + target wallets according to their percent. +
+This is valid for every payment, doesn't matter how it was created.
+Target wallets can be any wallet from this same LNbits instance.
++ To remove a wallet from the targets list, just erase its fields and + save. To remove all, click "Clear" then save. +
+GET
+ /splitpayments/api/v1/targets
+ {"X-Api-Key": <admin_key>}
[{"wallet": <wallet id>, "alias": <chosen name for this
+ wallet>, "percent": <number between 1 and 100>}, ...]
+ curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ PUT
+ /splitpayments/api/v1/targets
+ {"X-Api-Key": <admin_key>}
curl -X PUT {{ request.url_root }}api/v1/splitpayments/targets -H
+ "X-Api-Key: {{ g.user.wallets[0].adminkey }}" -H 'Content-Type:
+ application/json' -d '{"targets": [{"wallet": <wallet id or invoice
+ key>, "alias": <name to identify this>, "percent": <number
+ between 1 and 100>}, ...]}'
+
+
+ Accept Bitcoin donations on Twitch, and integrate them into your alerts.
+ Present your viewers with a simple donation page, and add those donations
+ to Streamlabs to play alerts on your stream!
+ For detailed setup instructions, check out
+ this guide!
+
+ Created by, Fitti
+
+ Charge people for using your subdomain name...
+
+ More details
+
+
+ Created by, Kris
+
+ Cost per day: {{ domain_cost }} sats
+ {% raw %} Total cost: {{amountSats}} sats {% endraw %}
+
+ To help developers use LNbits to manage their users, the User Manager
+ extension allows the creation and management of users and wallets.
+
For example, a games developer may be developing a game that needs
+ each user to have their own wallet, LNbits can be included in the
+ develpoers stack as the user and wallet manager.
+
+ Created by, Ben Arc
+
GET
+ /usermanager/api/v1/users
+ JSON list of users
+ curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ GET
+ /usermanager/api/v1/users/<user_id>
+ JSON list of users
+ curl -X GET {{ request.url_root }}usermanager/api/v1/users/<user_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ GET
+ /usermanager/api/v1/wallets/<user_id>
+ {"X-Api-Key": <string>}
+ JSON wallet data
+ curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/<user_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ GET
+ /usermanager/api/v1/wallets<wallet_id>
+ {"X-Api-Key": <string>}
+ JSON a wallets transactions
+ curl -X GET {{ request.url_root }}usermanager/api/v1/wallets<wallet_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ POST
+ /usermanager/api/v1/users
+ {"X-Api-Key": <string>, "Content-type":
+ "application/json"}
+ {"admin_id": <string>, "user_name": <string>,
+ "wallet_name": <string>,"email": <Optional string>
+ ,"password": <Optional string>}
+ {"id": <string>, "name": <string>, "admin":
+ <string>, "email": <string>, "password":
+ <string>}
+ curl -X POST {{ request.url_root }}usermanager/api/v1/users -d '{"admin_id": "{{
+ g.user.id }}", "wallet_name": <string>, "user_name":
+ <string>, "email": <Optional string>, "password": <
+ Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
+ "Content-type: application/json"
+
+ POST
+ /usermanager/api/v1/wallets
+ {"X-Api-Key": <string>, "Content-type":
+ "application/json"}
+ {"user_id": <string>, "wallet_name": <string>,
+ "admin_id": <string>}
+ {"id": <string>, "admin": <string>, "name":
+ <string>, "user": <string>, "adminkey": <string>,
+ "inkey": <string>}
+ curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d '{"user_id":
+ <string>, "wallet_name": <string>, "admin_id": "{{
+ g.user.id }}"}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
+ "Content-type: application/json"
+
+ DELETE
+ /usermanager/api/v1/users/<user_id>
+ {"X-Api-Key": <string>}
+ curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/<user_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ DELETE
+ /usermanager/api/v1/wallets/<wallet_id>
+ {"X-Api-Key": <string>}
+ curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/<wallet_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ POST
+ /usermanager/api/v1/extensions
+ {"X-Api-Key": <string>}
+ curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d '{"userid":
+ <string>, "extension": <string>, "active":
+ <integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
+ "Content-type: application/json"
+
+
+ Watch Only extension uses mempool.space
+ For use with "account Extended Public Key"
+ https://iancoleman.io/bip39/
+
+
Created by,
+ Ben Arc (using,
+ Embit)
+
GET /watchonly/api/v1/wallet
+ {"X-Api-Key": <invoice_key>}
[<wallets_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/wallet -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ GET
+ /watchonly/api/v1/wallet/<wallet_id>
+ {"X-Api-Key": <invoice_key>}
[<wallet_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/wallet/<wallet_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ POST /watchonly/api/v1/wallet
+ {"X-Api-Key": <admin_key>}
[<wallet_object>, ...]
+ curl -X POST {{ request.url_root }}api/v1/wallet -d '{"title":
+ <string>, "masterpub": <string>}' -H "Content-type:
+ application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+ DELETE
+ /watchonly/api/v1/wallet/<wallet_id>
+ {"X-Api-Key": <admin_key>}
+ curl -X DELETE {{ request.url_root
+ }}api/v1/wallet/<wallet_id> -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ GET
+ /watchonly/api/v1/addresses/<wallet_id>
+ {"X-Api-Key": <invoice_key>}
[<address_object>, ...]
+ curl -X GET {{ request.url_root
+ }}api/v1/addresses/<wallet_id> -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ GET
+ /watchonly/api/v1/address/<wallet_id>
+ {"X-Api-Key": <invoice_key>}
[<address_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/address/<wallet_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ GET /watchonly/api/v1/mempool
+ {"X-Api-Key": <admin_key>}
[<mempool_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/mempool -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ POST
+ /watchonly/api/v1/mempool
+ {"X-Api-Key": <admin_key>}
[<mempool_object>, ...]
+ curl -X PUT {{ request.url_root }}api/v1/mempool -d '{"endpoint":
+ <string>}' -H "Content-type: application/json" -H "X-Api-Key:
+ {{ g.user.wallets[0].adminkey }}"
+
+
+ Current:
+ {{ currentaddress }}
+
+
GET /withdraw/api/v1/links
+ {"X-Api-Key": <invoice_key>}
[<withdraw_link_object>, ...]
+ curl -X GET {{ request.url_root }}api/v1/links -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ GET
+ /withdraw/api/v1/links/<withdraw_id>
+ {"X-Api-Key": <invoice_key>}
{"lnurl": <string>}
+ curl -X GET {{ request.url_root }}api/v1/links/<withdraw_id> -H
+ "X-Api-Key: {{ g.user.wallets[0].inkey }}"
+
+ POST /withdraw/api/v1/links
+ {"X-Api-Key": <admin_key>}
{"title": <string>, "min_withdrawable": <integer>,
+ "max_withdrawable": <integer>, "uses": <integer>,
+ "wait_time": <integer>, "is_unique": <boolean>}
+ {"lnurl": <string>}
+ curl -X POST {{ request.url_root }}api/v1/links -d '{"title":
+ <string>, "min_withdrawable": <integer>,
+ "max_withdrawable": <integer>, "uses": <integer>,
+ "wait_time": <integer>, "is_unique": <boolean>}' -H
+ "Content-type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ PUT
+ /withdraw/api/v1/links/<withdraw_id>
+ {"X-Api-Key": <admin_key>}
{"title": <string>, "min_withdrawable": <integer>,
+ "max_withdrawable": <integer>, "uses": <integer>,
+ "wait_time": <integer>, "is_unique": <boolean>}
+ {"lnurl": <string>}
+ curl -X PUT {{ request.url_root }}api/v1/links/<withdraw_id> -d
+ '{"title": <string>, "min_withdrawable": <integer>,
+ "max_withdrawable": <integer>, "uses": <integer>,
+ "wait_time": <integer>, "is_unique": <boolean>}' -H
+ "Content-type: application/json" -H "X-Api-Key: {{
+ g.user.wallets[0].adminkey }}"
+
+ DELETE
+ /withdraw/api/v1/links/<withdraw_id>
+ {"X-Api-Key": <admin_key>}
+ curl -X DELETE {{ request.url_root }}api/v1/links/<withdraw_id>
+ -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
+
+ GET
+ /withdraw/api/v1/links/<the_hash>/<lnurl_id>
+ {"X-Api-Key": <invoice_key>}
{"status": <bool>}
+ curl -X GET {{ request.url_root
+ }}api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{
+ g.user.wallets[0].inkey }}"
+
+ GET
+ /withdraw/img/<lnurl_id>
+ curl -X GET {{ request.url_root }}/withdraw/img/<lnurl_id>"
+
+
+ WARNING: LNURL must be used over https or TOR
+ LNURL is a range of lightning-network standards that allow us to use
+ lightning-network differently. An LNURL withdraw is the permission for
+ someone to pull a certain amount of funds from a lightning wallet. In
+ this extension time is also added - an amount can be withdraw over a
+ period of time. A typical use case for an LNURL withdraw is a faucet,
+ although it is a very powerful technology, with much further reaching
+ implications. For example, an LNURL withdraw could be minted to pay for
+ a subscription service.
+
+ Exploring LNURL and finding use cases, is really helping inform + lightning protocol development, rather than the protocol dictating how + lightning-network should be engaged with. +
+ Check + Awesome LNURL + for further information. ++ Use a LNURL compatible bitcoin wallet to claim the sats. +
+
+ ID: {{ qrCodeDialog.data.id }}
+ Unique: {{ qrCodeDialog.data.is_unique }}
+ (QR code will change after each withdrawal)
+ Max. withdrawable: {{
+ qrCodeDialog.data.max_withdrawable }} sat
+ Wait time: {{ qrCodeDialog.data.wait_time }} seconds
+ Withdraws: {{ qrCodeDialog.data.used }} / {{
+ qrCodeDialog.data.uses }}
+
+ |
+ {% endfor %}
+