Merge branch 'lnbits:main' into main

This commit is contained in:
blackcoffeexbt 2022-10-06 11:10:30 +01:00 committed by GitHub
commit 886a7e4ddc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 516 additions and 217 deletions

View file

@ -9,9 +9,20 @@ on:
jobs: jobs:
checks: checks:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install packages - name: Install packages
run: poetry install run: poetry install
- name: Check black - name: Check black

View file

@ -22,14 +22,18 @@ jobs:
--health-retries 5 --health-retries 5
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install poetry install

View file

@ -7,14 +7,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install poetry install

View file

@ -7,14 +7,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Setup Regtest - name: Setup Regtest
run: | run: |
docker build -t lnbitsdocker/lnbits-legend . docker build -t lnbitsdocker/lnbits-legend .
@ -46,14 +50,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Setup Regtest - name: Setup Regtest
run: | run: |
docker build -t lnbitsdocker/lnbits-legend . docker build -t lnbitsdocker/lnbits-legend .
@ -86,14 +94,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Setup Regtest - name: Setup Regtest
run: | run: |
docker build -t lnbitsdocker/lnbits-legend . docker build -t lnbitsdocker/lnbits-legend .

View file

@ -7,7 +7,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@ -29,14 +30,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies - name: Install dependencies
env: env:
VIRTUAL_ENV: ./venv VIRTUAL_ENV: ./venv
@ -64,14 +69,18 @@ jobs:
--health-retries 5 --health-retries 5
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: ["3.9"]
poetry-version: ["1.2.1"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: abatilo/actions-poetry@v2.1.3 - name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install poetry install

View file

@ -292,6 +292,43 @@ Save the file and run the following commands:
sudo systemctl enable lnbits.service sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service sudo systemctl start lnbits.service
``` ```
## Reverse proxy with automatic https using Caddy
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
Point your domain at the IP of the server you're running LNbits on, by making an `A` record.
Install Caddy on the server
https://caddyserver.com/docs/install#debian-ubuntu-raspbian
```
sudo caddy stop
```
Create a Caddyfile
```
sudo nano Caddyfile
```
Assuming your LNbits is running on port `5000` add:
```
yourdomain.com {
handle /api/v1/payments/sse* {
reverse_proxy 0.0.0.0:5000 {
header_up X-Forwarded-Host yourdomain.com
transport http {
keepalive off
compression off
}
}
}
reverse_proxy 0.0.0.0:5000 {
header_up X-Forwarded-Host yourdomain.com
}
}
```
Save and exit `CTRL + x`
```
sudo caddy start
```
## Running behind an apache2 reverse proxy over https ## Running behind an apache2 reverse proxy over https
Install apache2 and enable apache2 mods Install apache2 and enable apache2 mods

View file

@ -34,7 +34,6 @@ from .tasks import (
check_pending_payments, check_pending_payments,
internal_invoice_listener, internal_invoice_listener,
invoice_listener, invoice_listener,
run_deferred_async,
webhook_handler, webhook_handler,
) )
@ -185,7 +184,7 @@ def register_async_tasks(app):
loop.create_task(catch_everything_and_restart(invoice_listener)) loop.create_task(catch_everything_and_restart(invoice_listener))
loop.create_task(catch_everything_and_restart(internal_invoice_listener)) loop.create_task(catch_everything_and_restart(internal_invoice_listener))
await register_task_listeners() await register_task_listeners()
await run_deferred_async() # await run_deferred_async() # calle: doesn't do anyting?
@app.on_event("shutdown") @app.on_event("shutdown")
async def stop_listeners(): async def stop_listeners():

View file

@ -186,9 +186,9 @@ async def pay_invoice(
) )
# notify receiver asynchronously # notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue from lnbits.tasks import internal_invoice_queue
logger.debug(f"enqueuing internal invoice {internal_checking_id}")
await internal_invoice_queue.put(internal_checking_id) await internal_invoice_queue.put(internal_checking_id)
else: else:
logger.debug(f"backend: sending payment {temp_id}") logger.debug(f"backend: sending payment {temp_id}")

View file

@ -3,7 +3,11 @@ const CACHE_VERSION = 1
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-` const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => { const getApiKey = request => {
return request.headers.get('X-Api-Key') || 'none' let api_key = request.headers.get('X-Api-Key')
if (!api_key || api_key == 'undefined') {
api_key = 'no_api_key'
}
return api_key
} }
// on activation we clean up the previously registered service workers // on activation we clean up the previously registered service workers
@ -26,8 +30,10 @@ self.addEventListener('activate', evt =>
// If no response is found, it populates the runtime cache with the response // If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page. // from the network before returning it to the page.
self.addEventListener('fetch', event => { self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if ( if (
!event.request.url.startsWith(
self.location.origin + '/api/v1/payments/sse'
) &&
event.request.url.startsWith(self.location.origin) && event.request.url.startsWith(self.location.origin) &&
event.request.method == 'GET' event.request.method == 'GET'
) { ) {

View file

@ -1,30 +1,43 @@
import asyncio import asyncio
from typing import List from typing import Dict
import httpx import httpx
from loguru import logger from loguru import logger
from lnbits.tasks import register_invoice_listener from lnbits.helpers import get_current_extension_name
from lnbits.tasks import SseListenersDict, register_invoice_listener
from . import db from . import db
from .crud import get_balance_notify from .crud import get_balance_notify
from .models import Payment from .models import Payment
api_invoice_listeners: List[asyncio.Queue] = [] api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
"api_invoice_listeners"
)
async def register_task_listeners(): async def register_task_listeners():
"""
Registers an invoice listener queue for the core tasks.
Incoming payaments in this queue will eventually trigger the signals sent to all other extensions
and fulfill other core tasks such as dispatching webhooks.
"""
invoice_paid_queue = asyncio.Queue(5) invoice_paid_queue = asyncio.Queue(5)
register_invoice_listener(invoice_paid_queue) # we register invoice_paid_queue to receive all incoming invoices
register_invoice_listener(invoice_paid_queue, "core/tasks.py")
# register a worker that will react to invoices
asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue)) asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue))
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
"""
This worker dispatches events to all extensions, dispatches webhooks and balance notifys.
"""
while True: while True:
payment = await invoice_paid_queue.get() payment = await invoice_paid_queue.get()
logger.debug("received invoice paid event") logger.trace("received invoice paid event")
# send information to sse channel # send information to sse channel
await dispatch_invoice_listener(payment) await dispatch_api_invoice_listeners(payment)
# dispatch webhook # dispatch webhook
if payment.webhook and not payment.webhook_status: if payment.webhook and not payment.webhook_status:
@ -41,16 +54,23 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
pass pass
async def dispatch_invoice_listener(payment: Payment): async def dispatch_api_invoice_listeners(payment: Payment):
for send_channel in api_invoice_listeners: """
Emits events to invoice listener subscribed from the API.
"""
for chan_name, send_channel in api_invoice_listeners.items():
try: try:
logger.debug(f"sending invoice paid event to {chan_name}")
send_channel.put_nowait(payment) send_channel.put_nowait(payment)
except asyncio.QueueFull: except asyncio.QueueFull:
logger.debug("removing sse listener", send_channel) logger.error(f"removing sse listener {send_channel}:{chan_name}")
api_invoice_listeners.remove(send_channel) api_invoice_listeners.pop(chan_name)
async def dispatch_webhook(payment: Payment): async def dispatch_webhook(payment: Payment):
"""
Dispatches the webhook to the webhook url.
"""
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
data = payment.dict() data = payment.dict()
try: try:

View file

@ -3,11 +3,13 @@ import binascii
import hashlib import hashlib
import json import json
import time import time
import uuid
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO from io import BytesIO
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import async_timeout
import httpx import httpx
import pyqrcode import pyqrcode
from fastapi import Depends, Header, Query, Request from fastapi import Depends, Header, Query, Request
@ -16,7 +18,7 @@ from fastapi.params import Body
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.fields import Field from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse from sse_starlette.sse import EventSourceResponse, ServerSentEvent
from starlette.responses import HTMLResponse, StreamingResponse from starlette.responses import HTMLResponse, StreamingResponse
from lnbits import bolt11, lnurl from lnbits import bolt11, lnurl
@ -366,37 +368,48 @@ async def api_payments_pay_lnurl(
} }
async def subscribe(request: Request, wallet: Wallet): async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
"""
Subscribe to new invoices for a wallet. Can be wrapped in EventSourceResponse.
Listenes invoming payments for a wallet and yields jsons with payment details.
"""
this_wallet_id = wallet.id this_wallet_id = wallet.id
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0) payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
logger.debug("adding sse listener", payment_queue) uid = f"{this_wallet_id}_{str(uuid.uuid4())[:8]}"
api_invoice_listeners.append(payment_queue) logger.debug(f"adding sse listener for wallet: {uid}")
api_invoice_listeners[uid] = payment_queue
send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0) send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0)
async def payment_received() -> None: async def payment_received() -> None:
while True: while True:
payment: Payment = await payment_queue.get() try:
if payment.wallet_id == this_wallet_id: async with async_timeout.timeout(1):
logger.debug("payment received", payment) payment: Payment = await payment_queue.get()
await send_queue.put(("payment-received", payment)) if payment.wallet_id == this_wallet_id:
logger.debug("sse listener: payment receieved", payment)
await send_queue.put(("payment-received", payment))
except asyncio.TimeoutError:
pass
asyncio.create_task(payment_received()) task = asyncio.create_task(payment_received())
try: try:
while True: while True:
if await request.is_disconnected():
await request.close()
break
typ, data = await send_queue.get() typ, data = await send_queue.get()
if data: if data:
jdata = json.dumps(dict(data.dict(), pending=False)) jdata = json.dumps(dict(data.dict(), pending=False))
# yield dict(id=1, event="this", data="1234")
# await asyncio.sleep(2)
yield dict(data=jdata, event=typ) yield dict(data=jdata, event=typ)
# yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8")) except asyncio.CancelledError as e:
except asyncio.CancelledError: logger.debug(f"CancelledError on listener {uid}: {e}")
api_invoice_listeners.pop(uid)
task.cancel()
return return
@ -405,7 +418,9 @@ async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type) request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
return EventSourceResponse( return EventSourceResponse(
subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream" subscribe_wallet_invoices(request, wallet.wallet),
ping=20,
media_type="text/event-stream",
) )

View file

@ -46,8 +46,8 @@ async def api_public_payment_longpolling(payment_hash):
payment_queue = asyncio.Queue(0) payment_queue = asyncio.Queue(0)
logger.debug("adding standalone invoice listener", payment_hash, payment_queue) logger.debug(f"adding standalone invoice listener for hash: {payment_hash}")
api_invoice_listeners.append(payment_queue) api_invoice_listeners[payment_hash] = payment_queue
response = None response = None

View file

@ -5,6 +5,7 @@ import httpx
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import create_refund, get_hit from .crud import create_refund, get_hit
@ -12,7 +13,7 @@ from .crud import create_refund, get_hit
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -34,8 +34,8 @@ from .models import (
from .utils import check_balance, get_timestamp, req_wrap from .utils import check_balance, get_timestamp, req_wrap
net = NETWORKS[BOLTZ_NETWORK] net = NETWORKS[BOLTZ_NETWORK]
logger.debug(f"BOLTZ_URL: {BOLTZ_URL}") logger.trace(f"BOLTZ_URL: {BOLTZ_URL}")
logger.debug(f"Bitcoin Network: {net['name']}") logger.trace(f"Bitcoin Network: {net['name']}")
async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap: async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:

View file

@ -11,8 +11,8 @@ from lnbits.settings import BOLTZ_MEMPOOL_SPACE_URL, BOLTZ_MEMPOOL_SPACE_URL_WS
from .utils import req_wrap from .utils import req_wrap
logger.debug(f"BOLTZ_MEMPOOL_SPACE_URL: {BOLTZ_MEMPOOL_SPACE_URL}") logger.trace(f"BOLTZ_MEMPOOL_SPACE_URL: {BOLTZ_MEMPOOL_SPACE_URL}")
logger.debug(f"BOLTZ_MEMPOOL_SPACE_URL_WS: {BOLTZ_MEMPOOL_SPACE_URL_WS}") logger.trace(f"BOLTZ_MEMPOOL_SPACE_URL_WS: {BOLTZ_MEMPOOL_SPACE_URL_WS}")
websocket_url = f"{BOLTZ_MEMPOOL_SPACE_URL_WS}/api/v1/ws" websocket_url = f"{BOLTZ_MEMPOOL_SPACE_URL_WS}/api/v1/ws"

View file

@ -5,6 +5,7 @@ from loguru import logger
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.core.services import check_transaction_status from lnbits.core.services import check_transaction_status
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .boltz import ( from .boltz import (
@ -127,7 +128,7 @@ async def check_for_pending_swaps():
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -7,6 +7,7 @@ from starlette.exceptions import HTTPException
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_copilot from .crud import get_copilot
@ -15,7 +16,7 @@ from .views import updater
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import update_jukebox_payment from .crud import update_jukebox_payment
@ -8,7 +9,7 @@ from .crud import update_jukebox_payment
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -6,7 +6,7 @@ from loguru import logger
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.crud import create_payment from lnbits.core.crud import create_payment
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
from lnbits.tasks import internal_invoice_listener, register_invoice_listener from lnbits.tasks import internal_invoice_listener, register_invoice_listener
from .crud import get_livestream_by_track, get_producer, get_track from .crud import get_livestream_by_track, get_producer, get_track
@ -14,7 +14,7 @@ from .crud import get_livestream_by_track, get_producer, get_track
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -3,6 +3,7 @@ import asyncio
import httpx import httpx
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_address, get_domain, set_address_paid, set_address_renewed from .crud import get_address, get_domain, set_address_paid, set_address_renewed
@ -10,7 +11,7 @@ from .crud import get_address, get_domain, set_address_paid, set_address_renewed
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
import json
from fastapi import APIRouter from fastapi import APIRouter

View file

@ -3,6 +3,7 @@ import asyncio
from loguru import logger from loguru import logger
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_ticket, set_ticket_paid from .crud import get_ticket, set_ticket_paid
@ -10,7 +11,7 @@ from .crud import get_ticket, set_ticket_paid
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -5,6 +5,7 @@ import httpx
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_pay_link from .crud import get_pay_link
@ -12,7 +13,7 @@ from .crud import get_pay_link
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -10,6 +10,7 @@ from lnbits.core.crud import get_wallet
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice
from lnbits.core.views.api import api_payments_decode from lnbits.core.views.api import api_payments_decode
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_lnurlpayout_from_wallet from .crud import get_lnurlpayout_from_wallet
@ -17,7 +18,7 @@ from .crud import get_lnurlpayout_from_wallet
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -4,6 +4,7 @@ from loguru import logger
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.extensions.satspay.crud import check_address_balance, get_charge from lnbits.extensions.satspay.crud import check_address_balance, get_charge
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
# from .crud import get_ticket, set_ticket_paid # from .crud import get_ticket, set_ticket_paid
@ -11,7 +12,7 @@ from lnbits.tasks import register_invoice_listener
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -9,6 +9,7 @@ from fastapi import HTTPException
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_scrub_by_wallet from .crud import get_scrub_by_wallet
@ -16,7 +17,7 @@ from .crud import get_scrub_by_wallet
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -68,6 +68,21 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Scrub extension</h6> <h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Scrub extension</h6>
<p>
Automatically forward funds (Scrub) that get paid to the LNbits
wallet, to an LNURLpay or Lightning Address.
<br />
More info in Scrub's
<a
href="https://github.com/lnbits/lnbits/blob/main/lnbits/extensions/scrub/README.md#scrub"
target="_blank"
>readme</a
>.
</p>
<p style="font-size: 90%">
<strong>Important: </strong>wallet will need a float to account for
any fees, before being able to push a payment
</p>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>

View file

@ -6,7 +6,7 @@ from loguru import logger
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.crud import create_payment from lnbits.core.crud import create_payment
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
from lnbits.tasks import internal_invoice_queue, register_invoice_listener from lnbits.tasks import internal_invoice_queue, register_invoice_listener
from .crud import get_targets from .crud import get_targets
@ -14,7 +14,7 @@ from .crud import get_targets
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -3,6 +3,7 @@ import asyncio
import httpx import httpx
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .cloudflare import cloudflare_create_subdomain from .cloudflare import cloudflare_create_subdomain
@ -11,7 +12,7 @@ from .crud import get_domain, set_subdomain_paid
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -4,7 +4,7 @@ import json
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.crud import create_payment from lnbits.core.crud import create_payment
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
from lnbits.tasks import internal_invoice_queue, register_invoice_listener from lnbits.tasks import internal_invoice_queue, register_invoice_listener
from .crud import get_tpos from .crud import get_tpos
@ -12,7 +12,7 @@ from .crud import get_tpos
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue, get_current_extension_name())
while True: while True:
payment = await invoice_queue.get() payment = await invoice_queue.get()

View file

@ -80,6 +80,7 @@ class CreatePsbt(BaseModel):
class ExtractPsbt(BaseModel): class ExtractPsbt(BaseModel):
psbtBase64 = "" # // todo snake case psbtBase64 = "" # // todo snake case
inputs: List[TransactionInput] inputs: List[TransactionInput]
network = "Mainnet"
class SignedTransaction(BaseModel): class SignedTransaction(BaseModel):

View file

@ -5,7 +5,7 @@
<send-to <send-to
:data.sync="sendToList" :data.sync="sendToList"
:fee-rate="feeRate" :fee-rate="feeRate"
:tx-size="txSizeNoChange" :tx-size="txSize"
:selected-amount="selectedAmount" :selected-amount="selectedAmount"
:sats-denominated="satsDenominated" :sats-denominated="satsDenominated"
@update:outputs="handleOutputsChange" @update:outputs="handleOutputsChange"

View file

@ -11,7 +11,8 @@ async function payment(path) {
'mempool-endpoint', 'mempool-endpoint',
'sats-denominated', 'sats-denominated',
'serial-signer-ref', 'serial-signer-ref',
'adminkey' 'adminkey',
'network'
], ],
watch: { watch: {
immediate: true, immediate: true,
@ -279,7 +280,8 @@ async function payment(path) {
this.adminkey, this.adminkey,
{ {
psbtBase64, psbtBase64,
inputs: this.tx.inputs inputs: this.tx.inputs,
network: this.network
} }
) )
return data return data

View file

@ -49,7 +49,7 @@
<q-item <q-item
v-for="device in pairedDevices" v-for="device in pairedDevices"
:key="device.id" :key="device.id"
v-if="!selectedPort" v-if="!selectedPort && showPairedDevices"
clickable clickable
v-close-popup v-close-popup
> >

View file

@ -15,7 +15,7 @@ async function serialSigner(path) {
receivedData: '', receivedData: '',
config: {}, config: {},
decryptionKey: null, decryptionKey: null,
sharedSecret: null, // todo: store in secure local storage sharedSecret: null,
hww: { hww: {
password: null, password: null,
@ -51,12 +51,14 @@ async function serialSigner(path) {
}, },
tx: null, // todo: move to hww tx: null, // todo: move to hww
showConsole: false showConsole: false,
showPairedDevices: true
} }
}, },
computed: { computed: {
pairedDevices: { pairedDevices: {
cache: false,
get: function () { get: function () {
return ( return (
JSON.parse(window.localStorage.getItem('lnbits-paired-devices')) || JSON.parse(window.localStorage.getItem('lnbits-paired-devices')) ||
@ -109,7 +111,10 @@ async function serialSigner(path) {
// Wait for the serial port to open. // Wait for the serial port to open.
await this.selectedPort.open(config) await this.selectedPort.open(config)
// do not await
this.startSerialPortReading() this.startSerialPortReading()
// wait to init
sleep(1000)
const textEncoder = new TextEncoderStream() const textEncoder = new TextEncoderStream()
this.writableStreamClosed = textEncoder.readable.pipeTo( this.writableStreamClosed = textEncoder.readable.pipeTo(
@ -225,8 +230,9 @@ async function serialSigner(path) {
while (true) { while (true) {
const {value, done} = await readStringUntil('\n') const {value, done} = await readStringUntil('\n')
if (value) { if (value) {
this.handleSerialPortResponse(value) const {command, commandData} = await this.extractCommand(value)
this.updateSerialPortConsole(value) this.handleSerialPortResponse(command, commandData)
this.updateSerialPortConsole(command)
} }
if (done) return if (done) return
} }
@ -240,8 +246,7 @@ async function serialSigner(path) {
} }
} }
}, },
handleSerialPortResponse: async function (value) { handleSerialPortResponse: async function (command, commandData) {
const {command, commandData} = await this.extractCommand(value)
this.logPublicCommandsResponse(command, commandData) this.logPublicCommandsResponse(command, commandData)
switch (command) { switch (command) {
@ -282,7 +287,7 @@ async function serialSigner(path) {
) )
break break
default: default:
console.log(` %c${value}`, 'background: #222; color: red') console.log(` %c${command}`, 'background: #222; color: red')
} }
}, },
logPublicCommandsResponse: function (command, commandData) { logPublicCommandsResponse: function (command, commandData) {
@ -307,6 +312,8 @@ async function serialSigner(path) {
}, },
hwwPing: async function () { hwwPing: async function () {
try { try {
// Send an empty ping. The serial port buffer might have some jubk data. Flush it.
await this.sendCommandClearText(COMMAND_PING)
await this.sendCommandClearText(COMMAND_PING, [window.location.host]) await this.sendCommandClearText(COMMAND_PING, [window.location.host])
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
@ -582,7 +589,7 @@ async function serialSigner(path) {
hwwCheckPairing: async function () { hwwCheckPairing: async function () {
const iv = window.crypto.getRandomValues(new Uint8Array(16)) const iv = window.crypto.getRandomValues(new Uint8Array(16))
const encrypted = await this.encryptMessage( const encrypted = await this.encryptMessage(
this.sharedSecret, this.sharedSecret, // todo: revisit
iv, iv,
PAIRING_CONTROL_TEXT.length + ' ' + PAIRING_CONTROL_TEXT PAIRING_CONTROL_TEXT.length + ' ' + PAIRING_CONTROL_TEXT
) )
@ -603,10 +610,10 @@ async function serialSigner(path) {
} }
}, },
handleCheckPairingResponse: async function (res = '') { handleCheckPairingResponse: async function (res = '') {
const [statusCode, encryptedMessage] = res.split(' ') const [statusCode, message] = res.split(' ')
switch (statusCode) { switch (statusCode) {
case '0': case '0':
const controlText = await this.decryptData(encryptedMessage) const controlText = await this.decryptData(message)
if (controlText == PAIRING_CONTROL_TEXT) { if (controlText == PAIRING_CONTROL_TEXT) {
this.$q.notify({ this.$q.notify({
type: 'positive', type: 'positive',
@ -622,6 +629,16 @@ async function serialSigner(path) {
}) })
} }
break break
case '1':
this.closeSerialPort()
this.$q.notify({
type: 'warning',
message:
'Re-pairing failed. Remove (forget) device and try again!',
caption: `Error: ${message}`,
timeout: 10000
})
break
default: default:
// noting to do here yet // noting to do here yet
break break
@ -746,7 +763,7 @@ async function serialSigner(path) {
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
message: 'Failed to ask for help!', message: 'Failed to wipe!',
caption: `${error}`, caption: `${error}`,
timeout: 10000 timeout: 10000
}) })
@ -862,6 +879,11 @@ async function serialSigner(path) {
sendCommandSecure: async function (command, attrs = []) { sendCommandSecure: async function (command, attrs = []) {
const message = [command].concat(attrs).join(' ') const message = [command].concat(attrs).join(' ')
const iv = window.crypto.getRandomValues(new Uint8Array(16)) const iv = window.crypto.getRandomValues(new Uint8Array(16))
if (!this.sharedSecret || !this.sharedSecret.length) {
throw new Error(
`Secure connection not estabileshed. Tried to run command: ${command}`
)
}
const encrypted = await this.encryptMessage( const encrypted = await this.encryptMessage(
this.sharedSecret, this.sharedSecret,
iv, iv,
@ -901,6 +923,7 @@ async function serialSigner(path) {
}, },
decryptData: async function (value) { decryptData: async function (value) {
if (!this.sharedSecret) { if (!this.sharedSecret) {
console.log('/error Secure session not established!')
return '/error Secure session not established!' return '/error Secure session not established!'
} }
try { try {
@ -921,6 +944,7 @@ async function serialSigner(path) {
.trim() .trim()
return command return command
} catch (error) { } catch (error) {
console.log('/error Failed to decrypt message from device!')
return '/error Failed to decrypt message from device!' return '/error Failed to decrypt message from device!'
} }
}, },
@ -949,6 +973,11 @@ async function serialSigner(path) {
devices.splice(deviceIndex, 1) devices.splice(deviceIndex, 1)
} }
this.pairedDevices = devices this.pairedDevices = devices
this.showPairedDevices = false
setTimeout(() => {
// force UI refresh
this.showPairedDevices = true
})
}, },
addPairedDevice: function (deviceId, sharedSecretHex, config) { addPairedDevice: function (deviceId, sharedSecretHex, config) {
const devices = this.pairedDevices const devices = this.pairedDevices
@ -960,6 +989,11 @@ async function serialSigner(path) {
config config
}) })
this.pairedDevices = devices this.pairedDevices = devices
this.showPairedDevices = false
setTimeout(() => {
// force UI refresh
this.showPairedDevices = true
})
}, },
updatePairedDeviceConfig(deviceId, config) { updatePairedDeviceConfig(deviceId, config) {
const device = this.getPairedDevice(deviceId) const device = this.getPairedDevice(deviceId)

View file

@ -18,9 +18,21 @@
>directly from browser</a >directly from browser</a
> >
<small> <small>
<br />Created by, <br />Created by
<a target="_blank" style="color: unset" href="https://github.com/arcbtc" <a target="_blank" style="color: unset" href="https://github.com/arcbtc"
>Ben Arc</a >Ben Arc</a
>,
<a
target="_blank"
style="color: unset"
href="https://github.com/talvasconcelos"
>Tiago Vasconcelos</a
>,
<a
target="_blank"
style="color: unset"
href="https://github.com/motorina0"
>motorina0</a
> >
(using, (using,
<a <a

View file

@ -136,6 +136,7 @@
:adminkey="g.user.wallets[0].adminkey" :adminkey="g.user.wallets[0].adminkey"
:serial-signer-ref="$refs.serialSigner" :serial-signer-ref="$refs.serialSigner"
:sats-denominated="config.sats_denominated" :sats-denominated="config.sats_denominated"
:network="config.network"
@broadcast-done="handleBroadcastSuccess" @broadcast-done="handleBroadcastSuccess"
></payment> ></payment>
<!-- todo: no more utxos.data --> <!-- todo: no more utxos.data -->
@ -149,7 +150,7 @@
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none"> <h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Onchain Wallet (watch-only) Extension {{SITE_TITLE}} Onchain Wallet (watch-only) Extension
<small>(v0.2)</small> <small>(v0.3)</small>
</h6> </h6>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">

View file

@ -4,6 +4,7 @@ from http import HTTPStatus
import httpx import httpx
from embit import finalizer, script from embit import finalizer, script
from embit.ec import PublicKey from embit.ec import PublicKey
from embit.networks import NETWORKS
from embit.psbt import PSBT, DerivationPath from embit.psbt import PSBT, DerivationPath
from embit.transaction import Transaction, TransactionInput, TransactionOutput from embit.transaction import Transaction, TransactionInput, TransactionOutput
from fastapi import Query, Request from fastapi import Query, Request
@ -295,6 +296,7 @@ async def api_psbt_create(
async def api_psbt_extract_tx( async def api_psbt_extract_tx(
data: ExtractPsbt, w: WalletTypeInfo = Depends(require_admin_key) data: ExtractPsbt, w: WalletTypeInfo = Depends(require_admin_key)
): ):
network = NETWORKS["main"] if data.network == "Mainnet" else NETWORKS["test"]
res = SignedTransaction() res = SignedTransaction()
try: try:
psbt = PSBT.from_base64(data.psbtBase64) psbt = PSBT.from_base64(data.psbtBase64)
@ -316,7 +318,7 @@ async def api_psbt_extract_tx(
for out in transaction.vout: for out in transaction.vout:
tx["outputs"].append( tx["outputs"].append(
{"amount": out.value, "address": out.script_pubkey.address()} {"amount": out.value, "address": out.script_pubkey.address(network)}
) )
res.tx_json = json.dumps(tx) res.tx_json = json.dumps(tx)
except Exception as e: except Exception as e:

View file

@ -183,3 +183,26 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"] t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
return t return t
def get_current_extension_name() -> str:
"""
Returns the name of the extension that calls this method.
"""
import inspect
import json
import os
callee_filepath = inspect.stack()[1].filename
callee_dirname, callee_filename = os.path.split(callee_filepath)
path = os.path.normpath(callee_dirname)
extension_director_name = path.split(os.sep)[-1]
try:
config_path = os.path.join(callee_dirname, "config.json")
with open(config_path) as json_file:
config = json.load(json_file)
ext_name = config["name"]
except:
ext_name = extension_director_name
return ext_name

View file

@ -1,8 +1,9 @@
import asyncio import asyncio
import time import time
import traceback import traceback
import uuid
from http import HTTPStatus from http import HTTPStatus
from typing import Callable, List from typing import Callable, Dict, List
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from loguru import logger from loguru import logger
@ -18,20 +19,6 @@ from lnbits.settings import WALLET
from .core import db from .core import db
deferred_async: List[Callable] = []
def record_async(func: Callable) -> Callable:
def recorder(state):
deferred_async.append(func)
return recorder
async def run_deferred_async():
for func in deferred_async:
asyncio.create_task(catch_everything_and_restart(func))
async def catch_everything_and_restart(func): async def catch_everything_and_restart(func):
try: try:
@ -50,18 +37,48 @@ async def send_push_promise(a, b) -> None:
pass pass
invoice_listeners: List[asyncio.Queue] = [] class SseListenersDict(dict):
"""
A dict of sse listeners.
"""
def __init__(self, name: str = None):
self.name = name or f"sse_listener_{str(uuid.uuid4())[:8]}"
def __setitem__(self, key, value):
assert type(key) == str, f"{key} is not a string"
assert type(value) == asyncio.Queue, f"{value} is not an asyncio.Queue"
logger.trace(f"sse: adding listener {key} to {self.name}. len = {len(self)+1}")
return super().__setitem__(key, value)
def __delitem__(self, key):
logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
return super().__delitem__(key)
_RaiseKeyError = object() # singleton for no-default behavior
def pop(self, key, v=_RaiseKeyError) -> None:
logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
return super().pop(key)
def register_invoice_listener(send_chan: asyncio.Queue): invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listeners")
def register_invoice_listener(send_chan: asyncio.Queue, name: str = None):
""" """
A method intended for extensions to call when they want to be notified about A method intended for extensions (and core/tasks.py) to call when they want to be notified about
new invoice payments incoming. new invoice payments incoming. Will emit all incoming payments.
""" """
invoice_listeners.append(send_chan) name_unique = f"{name or 'no_name'}_{str(uuid.uuid4())[:8]}"
logger.trace(f"sse: registering invoice listener {name_unique}")
invoice_listeners[name_unique] = send_chan
async def webhook_handler(): async def webhook_handler():
"""
Returns the webhook_handler for the selected wallet if present. Used by API.
"""
handler = getattr(WALLET, "webhook_listener", None) handler = getattr(WALLET, "webhook_listener", None)
if handler: if handler:
return await handler() return await handler()
@ -72,18 +89,36 @@ internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
async def internal_invoice_listener(): async def internal_invoice_listener():
"""
internal_invoice_queue will be filled directly in core/services.py
after the payment was deemed to be settled internally.
Called by the app startup sequence.
"""
while True: while True:
checking_id = await internal_invoice_queue.get() checking_id = await internal_invoice_queue.get()
logger.info("> got internal payment notification", checking_id)
asyncio.create_task(invoice_callback_dispatcher(checking_id)) asyncio.create_task(invoice_callback_dispatcher(checking_id))
async def invoice_listener(): async def invoice_listener():
"""
invoice_listener will collect all invoices that come directly
from the backend wallet.
Called by the app startup sequence.
"""
async for checking_id in WALLET.paid_invoices_stream(): async for checking_id in WALLET.paid_invoices_stream():
logger.info("> got a payment notification", checking_id) logger.info("> got a payment notification", checking_id)
asyncio.create_task(invoice_callback_dispatcher(checking_id)) asyncio.create_task(invoice_callback_dispatcher(checking_id))
async def check_pending_payments(): async def check_pending_payments():
"""
check_pending_payments is called during startup to check for pending payments with
the backend and also to delete expired invoices. Incoming payments will be
checked only once, outgoing pending payments will be checked regularly.
"""
outgoing = True outgoing = True
incoming = True incoming = True
@ -133,9 +168,14 @@ async def perform_balance_checks():
async def invoice_callback_dispatcher(checking_id: str): async def invoice_callback_dispatcher(checking_id: str):
"""
Takes incoming payments, sets pending=False, and dispatches them to
invoice_listeners from core and extensions.
"""
payment = await get_standalone_payment(checking_id, incoming=True) payment = await get_standalone_payment(checking_id, incoming=True)
if payment and payment.is_in: if payment and payment.is_in:
logger.trace("sending invoice callback for payment", checking_id) logger.trace(f"sse sending invoice callback for payment {checking_id}")
await payment.set_pending(False) await payment.set_pending(False)
for send_chan in invoice_listeners: for chan_name, send_chan in invoice_listeners.items():
logger.trace(f"sse sending to chan: {chan_name}")
await send_chan.put(payment) await send_chan.put(payment)

View file

@ -8,9 +8,7 @@ from typing import AsyncGenerator, Dict, Optional
from environs import Env # type: ignore from environs import Env # type: ignore
from loguru import logger from loguru import logger
from lnbits.helpers import urlsafe_short_hash from ..bolt11 import Invoice, decode, encode
from ..bolt11 import decode, encode
from .base import ( from .base import (
InvoiceResponse, InvoiceResponse,
PaymentResponse, PaymentResponse,
@ -24,6 +22,16 @@ env.read_env()
class FakeWallet(Wallet): class FakeWallet(Wallet):
queue: asyncio.Queue = asyncio.Queue(0)
secret: str = env.str("FAKE_WALLET_SECTRET", default="ToTheMoon1")
privkey: str = hashlib.pbkdf2_hmac(
"sha256",
secret.encode("utf-8"),
("FakeWallet").encode("utf-8"),
2048,
32,
).hex()
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
logger.info( logger.info(
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr." "FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
@ -39,18 +47,12 @@ class FakeWallet(Wallet):
) -> InvoiceResponse: ) -> InvoiceResponse:
# we set a default secret since FakeWallet is used for internal=True invoices # we set a default secret since FakeWallet is used for internal=True invoices
# and the user might not have configured a secret yet # and the user might not have configured a secret yet
secret = env.str("FAKE_WALLET_SECTRET", default="ToTheMoon1")
data: Dict = { data: Dict = {
"out": False, "out": False,
"amount": amount, "amount": amount,
"currency": "bc", "currency": "bc",
"privkey": hashlib.pbkdf2_hmac( "privkey": self.privkey,
"sha256",
secret.encode("utf-8"),
("FakeWallet").encode("utf-8"),
2048,
32,
).hex(),
"memo": None, "memo": None,
"description_hash": None, "description_hash": None,
"description": "", "description": "",
@ -86,8 +88,9 @@ class FakeWallet(Wallet):
invoice = decode(bolt11) invoice = decode(bolt11)
if ( if (
hasattr(invoice, "checking_id") hasattr(invoice, "checking_id")
and invoice.checking_id[6:] == data["privkey"][:6] # type: ignore and invoice.checking_id[:6] == self.privkey[:6] # type: ignore
): ):
await self.queue.put(invoice)
return PaymentResponse(True, invoice.payment_hash, 0) return PaymentResponse(True, invoice.payment_hash, 0)
else: else:
return PaymentResponse( return PaymentResponse(
@ -101,7 +104,6 @@ class FakeWallet(Wallet):
return PaymentStatus(None) return PaymentStatus(None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue: asyncio.Queue = asyncio.Queue(0)
while True: while True:
value = await self.queue.get() value: Invoice = await self.queue.get()
yield value yield value.payment_hash

View file

@ -23,7 +23,7 @@ class VoidWallet(Wallet):
raise Unsupported("") raise Unsupported("")
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
logger.info( logger.warning(
"This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits." "This backend does nothing, it is here just as a placeholder, you must configure an actual backend before being able to do anything useful with LNbits."
) )
return StatusResponse(None, 0) return StatusResponse(None, 0)

215
poetry.lock generated
View file

@ -46,6 +46,17 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "async-timeout"
version = "4.0.2"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "21.2.0" version = "21.2.0"
@ -111,13 +122,16 @@ jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"] uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "cerberus" name = "Cerberus"
version = "1.3.4" version = "1.3.4"
description = "Lightweight, extensible schema and data validation tool for Python dictionaries." description = "Lightweight, extensible schema and data validation tool for Python dictionaries."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7" python-versions = ">=2.7"
[package.dependencies]
setuptools = "*"
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2021.5.30" version = "2021.5.30"
@ -182,7 +196,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "6.4.4" version = "6.5.0"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
category = "dev" category = "dev"
optional = false optional = false
@ -206,7 +220,7 @@ python-versions = ">=3.6"
cffi = ">=1.12" cffi = ">=1.12"
[package.extras] [package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools_rust (>=0.11.4)"] sdist = ["setuptools_rust (>=0.11.4)"]
@ -399,7 +413,7 @@ plugins = ["setuptools"]
requirements_deprecated_finder = ["pip-api", "pipreqs"] requirements_deprecated_finder = ["pip-api", "pipreqs"]
[[package]] [[package]]
name = "jinja2" name = "Jinja2"
version = "3.0.1" version = "3.0.1"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
category = "main" category = "main"
@ -441,7 +455,7 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
dev = ["Sphinx (>=2.2.1)", "black (>=19.10b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"] dev = ["Sphinx (>=2.2.1)", "black (>=19.10b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"]
[[package]] [[package]]
name = "markupsafe" name = "MarkupSafe"
version = "2.0.1" version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
category = "main" category = "main"
@ -575,7 +589,7 @@ testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "4.21.6" version = "4.21.7"
description = "" description = ""
category = "main" category = "main"
optional = false optional = false
@ -638,7 +652,7 @@ python-versions = ">=3.7,<4.0"
[[package]] [[package]]
name = "pyln-client" name = "pyln-client"
version = "0.12.0.post1" version = "0.11.1"
description = "Client library and plugin library for Core Lightning" description = "Client library and plugin library for Core Lightning"
category = "main" category = "main"
optional = false optional = false
@ -683,7 +697,7 @@ optional = false
python-versions = "*" python-versions = "*"
[[package]] [[package]]
name = "pyqrcode" name = "PyQRCode"
version = "1.2.1" version = "1.2.1"
description = "A QR code generator written purely in Python with SVG, EPS, PNG and terminal output." description = "A QR code generator written purely in Python with SVG, EPS, PNG and terminal output."
category = "main" category = "main"
@ -694,7 +708,7 @@ python-versions = "*"
PNG = ["pypng (>=0.0.13)"] PNG = ["pypng (>=0.0.13)"]
[[package]] [[package]]
name = "pyscss" name = "pyScss"
version = "1.4.0" version = "1.4.0"
description = "pyScss, a Scss compiler for Python" description = "pyScss, a Scss compiler for Python"
category = "main" category = "main"
@ -707,7 +721,7 @@ pathlib2 = "*"
six = "*" six = "*"
[[package]] [[package]]
name = "pysocks" name = "PySocks"
version = "1.7.1" version = "1.7.1"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
category = "main" category = "main"
@ -777,7 +791,7 @@ python-versions = ">=3.5"
cli = ["click (>=5.0)"] cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "pyyaml" name = "PyYAML"
version = "5.4.1" version = "5.4.1"
description = "YAML parser and emitter for Python" description = "YAML parser and emitter for Python"
category = "main" category = "main"
@ -785,7 +799,7 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]] [[package]]
name = "represent" name = "Represent"
version = "1.6.0.post0" version = "1.6.0.post0"
description = "Create __repr__ automatically or declaratively." description = "Create __repr__ automatically or declaratively."
category = "main" category = "main"
@ -823,6 +837,19 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
cffi = ">=1.3.0" cffi = ">=1.3.0"
[[package]]
name = "setuptools"
version = "65.4.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "shortuuid" name = "shortuuid"
version = "1.0.1" version = "1.0.1"
@ -848,7 +875,7 @@ optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]] [[package]]
name = "sqlalchemy" name = "SQLAlchemy"
version = "1.3.23" version = "1.3.23"
description = "Database Abstraction Library" description = "Database Abstraction Library"
category = "main" category = "main"
@ -860,7 +887,7 @@ mssql = ["pyodbc"]
mssql_pymssql = ["pymssql"] mssql_pymssql = ["pymssql"]
mssql_pyodbc = ["pyodbc"] mssql_pyodbc = ["pyodbc"]
mysql = ["mysqlclient"] mysql = ["mysqlclient"]
oracle = ["cx-oracle"] oracle = ["cx_oracle"]
postgresql = ["psycopg2"] postgresql = ["psycopg2"]
postgresql_pg8000 = ["pg8000 (<1.16.6)"] postgresql_pg8000 = ["pg8000 (<1.16.6)"]
postgresql_psycopg2binary = ["psycopg2-binary"] postgresql_psycopg2binary = ["psycopg2-binary"]
@ -1024,7 +1051,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7" python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
content-hash = "d0556d4a307864ba04a1e5da517884e523396c98a00ae09d9192c37b1d2c555b" content-hash = "c4a01d5bfc24a8008348b6bd954717354554310afaaecbfc2a14222ad25aca42"
[metadata.files] [metadata.files]
aiofiles = [ aiofiles = [
@ -1043,6 +1070,10 @@ asn1crypto = [
{file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"},
{file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"},
] ]
async-timeout = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
attrs = [ attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
@ -1085,7 +1116,7 @@ black = [
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
] ]
cerberus = [ Cerberus = [
{file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"}, {file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"},
] ]
certifi = [ certifi = [
@ -1193,56 +1224,56 @@ colorama = [
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
] ]
coverage = [ coverage = [
{file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
{file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
{file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
{file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
{file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
{file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
{file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
{file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
{file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
{file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
{file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
{file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
{file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
{file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
{file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
{file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
{file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
{file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
{file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
{file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
{file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
{file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
{file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
{file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
{file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
{file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
{file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
{file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
{file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
{file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
{file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
{file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
{file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
{file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
{file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
{file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
{file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
{file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
{file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
{file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
{file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
{file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
{file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
{file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
{file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
{file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
{file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
{file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
{file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
{file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
] ]
cryptography = [ cryptography = [
{file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"}, {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"},
@ -1397,7 +1428,7 @@ isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
] ]
jinja2 = [ Jinja2 = [
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
] ]
@ -1409,7 +1440,7 @@ loguru = [
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
{file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"},
] ]
markupsafe = [ MarkupSafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
@ -1542,20 +1573,20 @@ pluggy = [
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
] ]
protobuf = [ protobuf = [
{file = "protobuf-4.21.6-cp310-abi3-win32.whl", hash = "sha256:49f88d56a9180dbb7f6199c920f5bb5c1dd0172f672983bb281298d57c2ac8eb"}, {file = "protobuf-4.21.7-cp310-abi3-win32.whl", hash = "sha256:c7cb105d69a87416bd9023e64324e1c089593e6dae64d2536f06bcbe49cd97d8"},
{file = "protobuf-4.21.6-cp310-abi3-win_amd64.whl", hash = "sha256:7a6cc8842257265bdfd6b74d088b829e44bcac3cca234c5fdd6052730017b9ea"}, {file = "protobuf-4.21.7-cp310-abi3-win_amd64.whl", hash = "sha256:3ec85328a35a16463c6f419dbce3c0fc42b3e904d966f17f48bae39597c7a543"},
{file = "protobuf-4.21.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ba596b9ffb85c909fcfe1b1a23136224ed678af3faf9912d3fa483d5f9813c4e"}, {file = "protobuf-4.21.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:db9056b6a11cb5131036d734bcbf91ef3ef9235d6b681b2fc431cbfe5a7f2e56"},
{file = "protobuf-4.21.6-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4143513c766db85b9d7c18dbf8339673c8a290131b2a0fe73855ab20770f72b0"}, {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ca200645d6235ce0df3ccfdff1567acbab35c4db222a97357806e015f85b5744"},
{file = "protobuf-4.21.6-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6cea204865595a92a7b240e4b65bcaaca3ad5d2ce25d9db3756eba06041138e"}, {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b019c79e23a80735cc8a71b95f76a49a262f579d6b84fd20a0b82279f40e2cc1"},
{file = "protobuf-4.21.6-cp37-cp37m-win32.whl", hash = "sha256:9666da97129138585b26afcb63ad4887f602e169cafe754a8258541c553b8b5d"}, {file = "protobuf-4.21.7-cp37-cp37m-win32.whl", hash = "sha256:d3f89ccf7182293feba2de2739c8bf34fed1ed7c65a5cf987be00311acac57c1"},
{file = "protobuf-4.21.6-cp37-cp37m-win_amd64.whl", hash = "sha256:308173d3e5a3528787bb8c93abea81d5a950bdce62840d9760effc84127fb39c"}, {file = "protobuf-4.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:a74d96cd960b87b4b712797c741bb3ea3a913f5c2dc4b6cbe9c0f8360b75297d"},
{file = "protobuf-4.21.6-cp38-cp38-win32.whl", hash = "sha256:aa29113ec901281f29d9d27b01193407a98aa9658b8a777b0325e6d97149f5ce"}, {file = "protobuf-4.21.7-cp38-cp38-win32.whl", hash = "sha256:8e09d1916386eca1ef1353767b6efcebc0a6859ed7f73cb7fb974feba3184830"},
{file = "protobuf-4.21.6-cp38-cp38-win_amd64.whl", hash = "sha256:8f9e60f7d44592c66e7b332b6a7b4b6e8d8b889393c79dbc3a91f815118f8eac"}, {file = "protobuf-4.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:9e355f2a839d9930d83971b9f562395e13493f0e9211520f8913bd11efa53c02"},
{file = "protobuf-4.21.6-cp39-cp39-win32.whl", hash = "sha256:80e6540381080715fddac12690ee42d087d0d17395f8d0078dfd6f1181e7be4c"}, {file = "protobuf-4.21.7-cp39-cp39-win32.whl", hash = "sha256:f370c0a71712f8965023dd5b13277444d3cdfecc96b2c778b0e19acbfd60df6e"},
{file = "protobuf-4.21.6-cp39-cp39-win_amd64.whl", hash = "sha256:77b355c8604fe285536155286b28b0c4cbc57cf81b08d8357bf34829ea982860"}, {file = "protobuf-4.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:9643684232b6b340b5e63bb69c9b4904cdd39e4303d498d1a92abddc7e895b7f"},
{file = "protobuf-4.21.6-py2.py3-none-any.whl", hash = "sha256:07a0bb9cc6114f16a39c866dc28b6e3d96fa4ffb9cc1033057412547e6e75cb9"}, {file = "protobuf-4.21.7-py2.py3-none-any.whl", hash = "sha256:8066322588d4b499869bf9f665ebe448e793036b552f68c585a9b28f1e393f66"},
{file = "protobuf-4.21.6-py3-none-any.whl", hash = "sha256:c7c864148a237f058c739ae7a05a2b403c0dfa4ce7d1f3e5213f352ad52d57c6"}, {file = "protobuf-4.21.7-py3-none-any.whl", hash = "sha256:58b81358ec6c0b5d50df761460ae2db58405c063fd415e1101209221a0a810e1"},
{file = "protobuf-4.21.6.tar.gz", hash = "sha256:6b1040a5661cd5f6e610cbca9cfaa2a17d60e2bb545309bc1b278bb05be44bdd"}, {file = "protobuf-4.21.7.tar.gz", hash = "sha256:71d9dba03ed3432c878a801e2ea51e034b0ea01cf3a4344fb60166cb5f6c8757"},
] ]
psycopg2-binary = [ psycopg2-binary = [
{file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"},
@ -1661,8 +1692,8 @@ pyln-bolt7 = [
{file = "pyln_bolt7-1.0.246-py3-none-any.whl", hash = "sha256:54d48ec27fdc8751762cb068b0a9f2757a58fb57933c6d8f8255d02c27eb63c5"}, {file = "pyln_bolt7-1.0.246-py3-none-any.whl", hash = "sha256:54d48ec27fdc8751762cb068b0a9f2757a58fb57933c6d8f8255d02c27eb63c5"},
] ]
pyln-client = [ pyln-client = [
{file = "pyln-client-0.12.0.post1.tar.gz", hash = "sha256:c80338e8e9f435720c0e5f552dc4016fc8fba16d4b79764f881067e0fcd5d5c7"}, {file = "pyln-client-0.11.1.tar.gz", hash = "sha256:f5ea648840b030e2bbcf8c66ee72d25a5817f89854434a28d30e887547138c8e"},
{file = "pyln_client-0.12.0.post1-py3-none-any.whl", hash = "sha256:cfe3404eb88f294015145e668d774dd754b3baec36b44fe773fa354f1e1e48c1"}, {file = "pyln_client-0.11.1-py3-none-any.whl", hash = "sha256:497db443406b80c98c0434e2938eb1b2a17e88fd9aa63b018124068198df6141"},
] ]
pyln-proto = [ pyln-proto = [
{file = "pyln-proto-0.11.1.tar.gz", hash = "sha256:9bed240f41917c4fd526b767218a77d0fbe69242876eef72c35a856796f922d6"}, {file = "pyln-proto-0.11.1.tar.gz", hash = "sha256:9bed240f41917c4fd526b767218a77d0fbe69242876eef72c35a856796f922d6"},
@ -1675,14 +1706,14 @@ pyparsing = [
pypng = [ pypng = [
{file = "pypng-0.0.21-py3-none-any.whl", hash = "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd"}, {file = "pypng-0.0.21-py3-none-any.whl", hash = "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd"},
] ]
pyqrcode = [ PyQRCode = [
{file = "PyQRCode-1.2.1.tar.gz", hash = "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"}, {file = "PyQRCode-1.2.1.tar.gz", hash = "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"},
{file = "PyQRCode-1.2.1.zip", hash = "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6"}, {file = "PyQRCode-1.2.1.zip", hash = "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6"},
] ]
pyscss = [ pyScss = [
{file = "pyScss-1.4.0.tar.gz", hash = "sha256:8f35521ffe36afa8b34c7d6f3195088a7057c185c2b8f15ee459ab19748669ff"}, {file = "pyScss-1.4.0.tar.gz", hash = "sha256:8f35521ffe36afa8b34c7d6f3195088a7057c185c2b8f15ee459ab19748669ff"},
] ]
pysocks = [ PySocks = [
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
@ -1703,7 +1734,7 @@ python-dotenv = [
{file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"},
{file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"},
] ]
pyyaml = [ PyYAML = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
@ -1734,7 +1765,7 @@ pyyaml = [
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
] ]
represent = [ Represent = [
{file = "Represent-1.6.0.post0-py2.py3-none-any.whl", hash = "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"}, {file = "Represent-1.6.0.post0-py2.py3-none-any.whl", hash = "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"},
{file = "Represent-1.6.0.post0.tar.gz", hash = "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0"}, {file = "Represent-1.6.0.post0.tar.gz", hash = "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0"},
] ]
@ -1767,6 +1798,10 @@ secp256k1 = [
{file = "secp256k1-0.14.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9e7c024ff17e9b9d7c392bb2a917da231d6cb40ab119389ff1f51dca10339a4"}, {file = "secp256k1-0.14.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9e7c024ff17e9b9d7c392bb2a917da231d6cb40ab119389ff1f51dca10339a4"},
{file = "secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397"}, {file = "secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397"},
] ]
setuptools = [
{file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"},
{file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"},
]
shortuuid = [ shortuuid = [
{file = "shortuuid-1.0.1-py3-none-any.whl", hash = "sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"}, {file = "shortuuid-1.0.1-py3-none-any.whl", hash = "sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"},
{file = "shortuuid-1.0.1.tar.gz", hash = "sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f"}, {file = "shortuuid-1.0.1.tar.gz", hash = "sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f"},
@ -1779,7 +1814,7 @@ sniffio = [
{file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
{file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
] ]
sqlalchemy = [ SQLAlchemy = [
{file = "SQLAlchemy-1.3.23-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec"}, {file = "SQLAlchemy-1.3.23-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec"},
{file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1"}, {file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1"},
{file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5"}, {file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5"},

View file

@ -15,7 +15,6 @@ asgiref = "3.4.1"
attrs = "21.2.0" attrs = "21.2.0"
bech32 = "1.2.0" bech32 = "1.2.0"
bitstring = "3.1.9" bitstring = "3.1.9"
cerberus = "1.3.4"
certifi = "2021.5.30" certifi = "2021.5.30"
charset-normalizer = "2.0.6" charset-normalizer = "2.0.6"
click = "8.0.1" click = "8.0.1"
@ -62,7 +61,9 @@ cffi = "1.15.0"
websocket-client = "1.3.3" websocket-client = "1.3.3"
grpcio = "^1.49.1" grpcio = "^1.49.1"
protobuf = "^4.21.6" protobuf = "^4.21.6"
pyln-client = "^0.12.0" Cerberus = "^1.3.4"
async-timeout = "^4.0.2"
pyln-client = "0.11.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
isort = "^5.10.1" isort = "^5.10.1"

View file

@ -51,3 +51,5 @@ uvloop==0.16.0
watchfiles==0.16.0 watchfiles==0.16.0
websockets==10.3 websockets==10.3
websocket-client==1.3.3 websocket-client==1.3.3
async-timeout==4.0.2
setuptools==65.4.0