Merge remote-tracking branch 'origin/main' into universalwebsocket

This commit is contained in:
ben
2022-11-29 11:10:34 +00:00
14 changed files with 644 additions and 109 deletions

View File

@@ -1,6 +1,7 @@
import asyncio
import datetime
import os
import re
import time
from contextlib import asynccontextmanager
from typing import Optional
@@ -73,18 +74,39 @@ class Connection(Compat):
query = query.replace("?", "%s")
return query
def rewrite_values(self, values):
# strip html
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
def cleanhtml(raw_html):
if isinstance(raw_html, str):
cleantext = re.sub(CLEANR, "", raw_html)
return cleantext
else:
return raw_html
# tuple to list and back to tuple
values = tuple([cleanhtml(l) for l in list(values)])
return values
async def fetchall(self, query: str, values: tuple = ()) -> list:
result = await self.conn.execute(self.rewrite_query(query), values)
result = await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
)
return await result.fetchall()
async def fetchone(self, query: str, values: tuple = ()):
result = await self.conn.execute(self.rewrite_query(query), values)
result = await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
)
row = await result.fetchone()
await result.close()
return row
async def execute(self, query: str, values: tuple = ()):
return await self.conn.execute(self.rewrite_query(query), values)
return await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
)
class Database(Compat):

View File

@@ -2,7 +2,5 @@
"name": "SatsPay Server",
"short_description": "Create onchain and LN charges",
"icon": "payment",
"contributors": [
"arcbtc"
]
"contributors": ["arcbtc"]
}

View File

@@ -10,7 +10,7 @@ from lnbits.helpers import urlsafe_short_hash
from ..watchonly.crud import get_config, get_fresh_address
from . import db
from .helpers import fetch_onchain_balance
from .models import Charges, CreateCharge
from .models import Charges, CreateCharge, SatsPayThemes
###############CHARGES##########################
@@ -53,9 +53,10 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
time,
amount,
balance,
extra
extra,
custom_css
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
charge_id,
@@ -73,6 +74,7 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
data.amount,
0,
data.extra,
data.custom_css,
),
)
return await get_charge(charge_id)
@@ -121,3 +123,101 @@ async def check_address_balance(charge_id: str) -> Optional[Charges]:
if invoice_status["paid"]:
return await update_charge(charge_id=charge_id, balance=charge.amount)
return await get_charge(charge_id)
################## SETTINGS ###################
async def save_theme(data: SatsPayThemes, css_id: str = None):
# insert or update
if css_id:
await db.execute(
"""
UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
""",
(data.custom_css, data.title, css_id),
)
else:
css_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satspay.themes (
css_id,
title,
user,
custom_css
)
VALUES (?, ?, ?, ?)
""",
(
css_id,
data.title,
data.user,
data.custom_css,
),
)
return await get_theme(css_id)
async def get_theme(css_id: str) -> SatsPayThemes:
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
return SatsPayThemes.from_row(row) if row else None
async def get_themes(user_id: str) -> List[SatsPayThemes]:
rows = await db.fetchall(
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "timestamp" DESC """,
(user_id,),
)
return await get_config(row.user)
################## SETTINGS ###################
async def save_theme(data: SatsPayThemes, css_id: str = None):
# insert or update
if css_id:
await db.execute(
"""
UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
""",
(data.custom_css, data.title, css_id),
)
else:
css_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satspay.themes (
css_id,
title,
"user",
custom_css
)
VALUES (?, ?, ?, ?)
""",
(
css_id,
data.title,
data.user,
data.custom_css,
),
)
return await get_theme(css_id)
async def get_theme(css_id: str) -> SatsPayThemes:
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
return SatsPayThemes.from_row(row) if row else None
async def get_themes(user_id: str) -> List[SatsPayThemes]:
rows = await db.fetchall(
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "title" DESC """,
(user_id,),
)
return [SatsPayThemes.from_row(row) for row in rows]
async def delete_theme(theme_id: str) -> None:
await db.execute("DELETE FROM satspay.themes WHERE css_id = ?", (theme_id,))

View File

@@ -19,10 +19,12 @@ def public_charge(charge: Charges):
"time_elapsed": charge.time_elapsed,
"time_left": charge.time_left,
"paid": charge.paid,
"custom_css": charge.custom_css,
}
if charge.paid:
c["completelink"] = charge.completelink
c["completelinktext"] = charge.completelinktext
return c

View File

@@ -37,3 +37,28 @@ async def m002_add_charge_extra_data(db):
ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}';
"""
)
async def m003_add_themes_table(db):
"""
Themes table
"""
await db.execute(
"""
CREATE TABLE satspay.themes (
css_id TEXT NOT NULL PRIMARY KEY,
"user" TEXT,
title TEXT,
custom_css TEXT
);
"""
)
async def m004_add_custom_css_to_charges(db):
"""
Add custom css option column to the 'charges' table
"""
await db.execute("ALTER TABLE satspay.charges ADD COLUMN custom_css TEXT;")

View File

@@ -14,6 +14,7 @@ class CreateCharge(BaseModel):
webhook: str = Query(None)
completelink: str = Query(None)
completelinktext: str = Query(None)
custom_css: Optional[str]
time: int = Query(..., ge=1)
amount: int = Query(..., ge=1)
extra: str = "{}"
@@ -38,6 +39,7 @@ class Charges(BaseModel):
completelink: Optional[str]
completelinktext: Optional[str] = "Back to Merchant"
extra: str = "{}"
custom_css: Optional[str]
time: int
amount: int
balance: int
@@ -72,3 +74,14 @@ class Charges(BaseModel):
def must_call_webhook(self):
return self.webhook and self.paid and self.config.webhook_success == False
class SatsPayThemes(BaseModel):
css_id: str = Query(None)
title: str = Query(None)
custom_css: str = Query(None)
user: Optional[str]
@classmethod
def from_row(cls, row: Row) -> "SatsPayThemes":
return cls(**dict(row))

View File

@@ -26,5 +26,10 @@ const mapCharge = (obj, oldObj = {}) => {
return charge
}
const mapCSS = (obj, oldObj = {}) => {
const theme = _.clone(obj)
return theme
}
const minutesToTime = min =>
min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : ''

View File

@@ -5,7 +5,13 @@
WatchOnly extension, we highly reccomend using a fresh extended public Key
specifically for SatsPayServer!<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
Created by, <a href="https://github.com/benarc">Ben Arc</a>,
<a
target="_blank"
style="color: unset"
href="https://github.com/motorina0"
>motorina0</a
></small
>
</p>
<br />

View File

@@ -174,7 +174,7 @@
<div class="col text-center">
<q-btn
outline
v-if="charge.webhook"
v-if="charge.completelink"
type="a"
:href="charge.completelink"
:label="charge.completelinktext"
@@ -297,7 +297,17 @@
</div>
<div class="col-lg- 4 col-md-3 col-sm-1"></div>
</div>
{% endblock %} {% block styles %}
<link
href="/satspay/css/{{ charge_data.custom_css }}"
rel="stylesheet"
type="text/css"
/>
<style>
header button.q-btn-dropdown {
display: none;
}
</style>
{% endblock %} {% block scripts %}
<script src="https://mempool.space/mempool.js"></script>
@@ -438,6 +448,10 @@
}
},
created: async function () {
// Remove a user defined theme
if (this.charge.custom_css) {
document.body.setAttribute('data-theme', '')
}
if (this.charge.payment_request) this.payInvoice()
else this.payOnchain()

View File

@@ -8,6 +8,26 @@
<q-btn unelevated color="primary" @click="formDialogCharge.show = true"
>New charge
</q-btn>
<q-btn
v-if="admin == 'True'"
unelevated
color="primary"
@click="getThemes();formDialogThemes.show = true"
>New CSS Theme
</q-btn>
<q-btn
v-else
disable
unelevated
color="primary"
@click="getThemes();formDialogThemes.show = true"
>New CSS Theme
<q-tooltip
>For security reason, custom css is only available to server
admins.</q-tooltip
></q-btn
>
</q-card-section>
</q-card>
@@ -259,6 +279,63 @@
</q-table>
</q-card-section>
</q-card>
<q-card v-if="admin == 'True'">
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Themes</h5>
</div>
</div>
<q-table
dense
flat
:data="themeLinks"
row-key="id"
:columns="customCSSTable.columns"
:pagination.sync="customCSSTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateformDialog(props.row.css_id)"
icon="edit"
color="light-blue"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTheme(props.row.css_id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
@@ -303,32 +380,6 @@
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.webhook"
type="url"
label="Webhook (URL to send transaction data to once paid)"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelink"
type="url"
label="Completed button URL"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelinktext"
type="text"
label="Completed button text (ie 'Back to merchant')"
>
</q-input>
<div class="row">
<div class="col">
<div v-if="walletLinks.length > 0">
@@ -377,6 +428,52 @@
label="Wallet *"
>
</q-select>
<q-toggle
v-model="showAdvanced"
label="Show advanced options"
></q-toggle>
<div v-if="showAdvanced" class="row">
<div class="col">
<q-input
filled
dense
v-model.trim="formDialogCharge.data.webhook"
type="url"
label="Webhook (URL to send transaction data to once paid)"
class="q-mt-lg"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelink"
type="url"
label="Completed button URL"
class="q-mt-lg"
>
</q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelinktext"
type="text"
label="Completed button text (ie 'Back to merchant')"
class="q-mt-lg"
>
</q-input>
<q-select
filled
dense
emit-value
v-model="formDialogCharge.data.custom_css"
:options="themeOptions"
label="Custom CSS theme (optional)"
class="q-mt-lg"
>
</q-select>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
unelevated
@@ -394,6 +491,43 @@
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="formDialogThemes.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDataThemes" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialogThemes.data.title"
type="text"
label="*Title"
></q-input>
<q-input
filled
dense
v-model.trim="formDialogThemes.data.custom_css"
type="textarea"
label="Custom CSS"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialogThemes.data.css_id"
unelevated
color="primary"
type="submit"
>Update CSS theme</q-btn
>
<q-btn v-else unelevated color="primary" type="submit"
>Save CSS theme</q-btn
>
<q-btn @click="cancelThemes" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<!-- lnbits/static/vendor
@@ -410,16 +544,21 @@
mixins: [windowMixin],
data: function () {
return {
settings: {},
filter: '',
admin: '{{ admin }}',
balance: null,
walletLinks: [],
chargeLinks: [],
onchainwallet: null,
themeLinks: [],
themeOptions: [],
onchainwallet: '',
rescanning: false,
mempool: {
endpoint: '',
network: 'Mainnet'
},
showAdvanced: false,
chargesTable: {
columns: [
@@ -494,7 +633,25 @@
rowsPerPage: 10
}
},
customCSSTable: {
columns: [
{
name: 'css_id',
align: 'left',
label: 'ID',
field: 'css_id'
},
{
name: 'title',
align: 'left',
label: 'Title',
field: 'title'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialogCharge: {
show: false,
data: {
@@ -502,13 +659,24 @@
onchainwallet: '',
lnbits: false,
description: '',
custom_css: '',
time: null,
amount: null
}
},
formDialogThemes: {
show: false,
data: {
custom_css: ''
}
}
}
},
methods: {
cancelThemes: function (data) {
this.formDialogCharge.data.custom_css = ''
this.formDialogThemes.show = false
},
cancelCharge: function (data) {
this.formDialogCharge.data.description = ''
this.formDialogCharge.data.onchain = false
@@ -517,6 +685,7 @@
this.formDialogCharge.data.time = null
this.formDialogCharge.data.amount = null
this.formDialogCharge.data.webhook = ''
this.formDialogCharge.data.custom_css = ''
this.formDialogCharge.data.completelink = ''
this.formDialogCharge.show = false
},
@@ -580,9 +749,39 @@
LNbits.utils.notifyApiError(error)
}
},
sendFormDataCharge: function () {
getThemes: async function () {
try {
const {data} = await LNbits.api.request(
'GET',
'/satspay/api/v1/themes',
this.g.user.wallets[0].inkey
)
console.log(data)
this.themeLinks = data.map(c =>
mapCSS(
c,
this.themeLinks.find(old => old.css_id === c.css_id)
)
)
this.themeOptions = data.map(w => ({
id: w.css_id,
label: w.title + ' - ' + w.css_id
}))
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
sendFormDataThemes: function () {
const wallet = this.g.user.wallets[0].inkey
const data = this.formDialogThemes.data
this.createTheme(wallet, data)
},
sendFormDataCharge: function () {
this.formDialogCharge.data.custom_css = this.formDialogCharge.data.custom_css?.id
const data = this.formDialogCharge.data
const wallet = this.g.user.wallets[0].inkey
data.amount = parseInt(data.amount)
data.time = parseInt(data.time)
data.lnbitswallet = data.lnbits ? data.lnbitswallet : null
@@ -651,6 +850,68 @@
this.rescanning = false
}
},
updateformDialog: function (themeId) {
const theme = _.findWhere(this.themeLinks, {css_id: themeId})
console.log(theme.css_id)
this.formDialogThemes.data.css_id = theme.css_id
this.formDialogThemes.data.title = theme.title
this.formDialogThemes.data.custom_css = theme.custom_css
this.formDialogThemes.show = true
},
createTheme: async function (wallet, data) {
console.log(data.css_id)
try {
if (data.css_id) {
const resp = await LNbits.api.request(
'POST',
'/satspay/api/v1/themes/' + data.css_id,
wallet,
data
)
this.themeLinks = _.reject(this.themeLinks, function (obj) {
return obj.css_id === data.css_id
})
this.themeLinks.unshift(mapCSS(resp.data))
} else {
const resp = await LNbits.api.request(
'POST',
'/satspay/api/v1/themes',
wallet,
data
)
this.themeLinks.unshift(mapCSS(resp.data))
}
this.formDialogThemes.show = false
this.formDialogThemes.data = {
title: '',
custom_css: ''
}
} catch (error) {
console.log('cun')
LNbits.utils.notifyApiError(error)
}
},
deleteTheme: function (themeId) {
const theme = _.findWhere(this.themeLinks, {id: themeId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this theme?')
.onOk(async () => {
try {
const response = await LNbits.api.request(
'DELETE',
'/satspay/api/v1/themes/' + themeId,
this.g.user.wallets[0].adminkey
)
this.themeLinks = _.reject(this.themeLinks, function (obj) {
return obj.css_id === themeId
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
})
},
createCharge: async function (wallet, data) {
try {
const resp = await LNbits.api.request(
@@ -703,6 +964,9 @@
}
},
created: async function () {
if (this.admin == 'True') {
await this.getThemes()
}
await this.getCharges()
await this.getWalletConfig()
await this.getWalletLinks()

View File

@@ -1,7 +1,10 @@
import json
from http import HTTPStatus
from fastapi import Response
from fastapi.param_functions import Depends
from fastapi.templating import Jinja2Templates
from loguru import logger
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
@@ -9,17 +12,21 @@ from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.extensions.satspay.helpers import public_charge
from lnbits.settings import LNBITS_ADMIN_USERS
from . import satspay_ext, satspay_renderer
from .crud import get_charge
from .crud import get_charge, get_theme
templates = Jinja2Templates(directory="templates")
@satspay_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
admin = False
if LNBITS_ADMIN_USERS and user.id in LNBITS_ADMIN_USERS:
admin = True
return satspay_renderer().TemplateResponse(
"satspay/index.html", {"request": request, "user": user.dict()}
"satspay/index.html", {"request": request, "user": user.dict(), "admin": admin}
)
@@ -40,3 +47,11 @@ async def display(request: Request, charge_id: str):
"network": charge.config.network,
},
)
@satspay_ext.get("/css/{css_id}")
async def display(css_id: str, response: Response):
theme = await get_theme(css_id)
if theme:
return Response(content=theme.custom_css, media_type="text/css")
return None

View File

@@ -1,9 +1,12 @@
import json
from http import HTTPStatus
import httpx
from fastapi.params import Depends
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
@@ -11,17 +14,22 @@ from lnbits.decorators import (
require_invoice_key,
)
from lnbits.extensions.satspay import satspay_ext
from lnbits.settings import LNBITS_ADMIN_EXTENSIONS, LNBITS_ADMIN_USERS
from .crud import (
check_address_balance,
create_charge,
delete_charge,
delete_theme,
get_charge,
get_charges,
get_theme,
get_themes,
save_theme,
update_charge,
)
from .helpers import call_webhook, public_charge
from .models import CreateCharge
from .models import CreateCharge, SatsPayThemes
#############################CHARGES##########################
@@ -126,3 +134,49 @@ async def api_charge_balance(charge_id):
await update_charge(charge_id=charge.id, extra=json.dumps(extra))
return {**public_charge(charge)}
#############################THEMES##########################
@satspay_ext.post("/api/v1/themes")
@satspay_ext.post("/api/v1/themes/{css_id}")
async def api_themes_save(
data: SatsPayThemes,
wallet: WalletTypeInfo = Depends(require_invoice_key),
css_id: str = None,
):
if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Only server admins can create themes.",
)
if css_id:
theme = await save_theme(css_id=css_id, data=data)
else:
data.user = wallet.wallet.user
theme = await save_theme(data=data)
return theme
@satspay_ext.get("/api/v1/themes")
async def api_themes_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
try:
return await get_themes(wallet.wallet.user)
except HTTPException:
logger.error("Error loading satspay themes")
logger.error(HTTPException)
return ""
@satspay_ext.delete("/api/v1/themes/{theme_id}")
async def api_charge_delete(theme_id, wallet: WalletTypeInfo = Depends(get_key_type)):
theme = await get_theme(theme_id)
if not theme:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Theme does not exist."
)
await delete_theme(theme_id)
return "", HTTPStatus.NO_CONTENT

View File

@@ -7,7 +7,7 @@ $colors in $themes {
[data-theme='#{$theme}'] .q-drawer--dark,
body[data-theme='#{$theme}'].body--dark,
[data-theme='#{$theme}'] .q-menu--dark {
background: $color !important;
background: $color;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
@@ -19,7 +19,7 @@ $colors in $themes {
@if $name=='info' {
[data-theme='#{$theme}'] .q-card--dark,
[data-theme='#{$theme}'] .q-stepper--dark {
background: $color !important;
background: $color;
}
}
}
@@ -27,37 +27,37 @@ $colors in $themes {
@each $name,
$color in $colors {
.bg-#{$name} {
background: $color !important;
background: $color;
}
.text-#{$name} {
color: $color !important;
color: $color;
}
}
}
}
[data-theme='freedom'] .q-drawer--dark {
background: #0a0a0a !important;
background: #0a0a0a;
}
[data-theme='freedom'] .q-header {
background: #0a0a0a !important;
background: #0a0a0a;
}
[data-theme='salvador'] .q-drawer--dark {
background: #242424 !important;
background: #242424;
}
[data-theme='salvador'] .q-header {
background: #0f47af !important;
background: #0f47af;
}
[data-theme='flamingo'] .q-drawer--dark {
background: #e75480 !important;
background: #e75480;
}
[data-theme='flamingo'] .q-header {
background: #e75480 !important;
background: #e75480;
}
[v-cloak] {

View File

@@ -1,55 +1,72 @@
aiofiles==0.8.0
anyio==3.6.1
asyncio==3.4.3
attrs==21.4.0
bech32==1.2.0
bitstring==3.1.9
cerberus==1.3.4
certifi==2022.6.15
cffi==1.15.0
click==8.1.3
ecdsa==0.18.0
embit==0.5.0
environs==9.5.0
fastapi==0.79.0
h11==0.12.0
httpcore==0.15.0
httptools==0.4.0
httpx==0.23.0
idna==3.3
jinja2==3.0.1
lnurl==0.3.6
loguru==0.6.0
markupsafe==2.1.1
marshmallow==3.17.0
outcome==1.2.0
packaging==21.3
psycopg2-binary==2.9.3
pycparser==2.21
pycryptodomex==3.15.0
pydantic==1.9.1
pyngrok==5.1.0
pyparsing==3.0.9
pypng==0.20220715.0
pyqrcode==1.2.1
pyscss==1.4.0
python-dotenv==0.20.0
pyyaml==6.0
represent==1.6.0.post0
rfc3986==1.5.0
secp256k1==0.14.0
shortuuid==1.0.9
six==1.16.0
sniffio==1.2.0
sqlalchemy-aio==0.17.0
sqlalchemy==1.3.23
sse-starlette==0.10.3
starlette==0.19.1
typing-extensions==4.3.0
uvicorn==0.18.2
uvloop==0.16.0
watchfiles==0.16.0
websockets==10.3
websocket-client==1.3.3
async-timeout==4.0.2
setuptools==65.4.0
aiofiles==0.8.0 ; python_version >= "3.7" and python_version < "4.0"
anyio==3.6.1 ; python_version >= "3.7" and python_version < "4.0"
asgiref==3.4.1 ; python_version >= "3.7" and python_version < "4.0"
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
async-timeout==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
attrs==21.2.0 ; python_version >= "3.7" and python_version < "4.0"
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
certifi==2021.5.30 ; python_version >= "3.7" and python_version < "4.0"
cffi==1.15.0 ; python_version >= "3.7" and python_version < "4.0"
charset-normalizer==2.0.6 ; python_version >= "3.7" and python_version < "4.0"
click==8.0.1 ; python_version >= "3.7" and python_version < "4.0"
coincurve==17.0.0 ; python_version >= "3.7" and python_version < "4.0"
colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
ecdsa==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
embit==0.4.9 ; python_version >= "3.7" and python_version < "4.0"
enum34==1.1.10 ; python_version >= "3.7" and python_version < "4.0"
environs==9.3.3 ; python_version >= "3.7" and python_version < "4.0"
fastapi==0.78.0 ; python_version >= "3.7" and python_version < "4.0"
grpcio==1.49.1 ; python_version >= "3.7" and python_version < "4.0"
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
httptools==0.4.0 ; python_version >= "3.7" and python_version < "4.0"
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
idna==3.2 ; python_version >= "3.7" and python_version < "4.0"
importlib-metadata==4.8.1 ; python_version >= "3.7" and python_version < "4.0"
jinja2==3.0.1 ; python_version >= "3.7" and python_version < "4.0"
lnurl==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
loguru==0.5.3 ; python_version >= "3.7" and python_version < "4.0"
markupsafe==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
marshmallow==3.17.0 ; python_version >= "3.7" and python_version < "4.0"
outcome==1.1.0 ; python_version >= "3.7" and python_version < "4.0"
packaging==21.3 ; python_version >= "3.7" and python_version < "4.0"
pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
protobuf==4.21.7 ; python_version >= "3.7" and python_version < "4.0"
psycopg2-binary==2.9.1 ; python_version >= "3.7" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
pycryptodomex==3.14.1 ; python_version >= "3.7" and python_version < "4.0"
pydantic==1.8.2 ; python_version >= "3.7" and python_version < "4.0"
pyln-bolt7==1.0.246 ; python_version >= "3.7" and python_version < "4.0"
pyln-client==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
pyln-proto==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0"
pypng==0.0.21 ; python_version >= "3.7" and python_version < "4.0"
pyqrcode==1.2.1 ; python_version >= "3.7" and python_version < "4.0"
pyscss==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
python-dotenv==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0"
represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
rfc3986==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
setuptools==65.4.1 ; python_version >= "3.7" and python_version < "4.0"
shortuuid==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
sniffio==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
sqlalchemy==1.3.23 ; python_version >= "3.7" and python_version < "4.0"
sse-starlette==0.6.2 ; python_version >= "3.7" and python_version < "4.0"
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
typing-extensions==3.10.0.2 ; python_version >= "3.7" and python_version < "4.0"
uvicorn==0.18.1 ; python_version >= "3.7" and python_version < "4.0"
uvloop==0.16.0 ; python_version >= "3.7" and python_version < "4.0"
watchgod==0.7 ; python_version >= "3.7" and python_version < "4.0"
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
websockets==10.0 ; python_version >= "3.7" and python_version < "4.0"
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
zipp==3.5.0 ; python_version >= "3.7" and python_version < "4.0"