Merge branch 'lnbits:main' into main
|
|
@ -11,7 +11,11 @@ LNBITS_ALLOWED_USERS=""
|
||||||
LNBITS_ADMIN_USERS=""
|
LNBITS_ADMIN_USERS=""
|
||||||
# Extensions only admin can access
|
# Extensions only admin can access
|
||||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||||
|
|
||||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
|
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
|
||||||
|
# Warning: Enabling this will make LNbits ignore this configuration file. Your settings will
|
||||||
|
# be stored in your database and you will be able to change them only through the Admin UI.
|
||||||
|
# Disable this to make LNbits use this config file again.
|
||||||
LNBITS_ADMIN_UI=false
|
LNBITS_ADMIN_UI=false
|
||||||
|
|
||||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||||
|
|
|
||||||
4
Makefile
|
|
@ -6,7 +6,7 @@ format: prettier isort black
|
||||||
|
|
||||||
check: mypy checkprettier checkisort checkblack
|
check: mypy checkprettier checkisort checkblack
|
||||||
|
|
||||||
prettier: $(shell find lnbits -name "*.js" -name ".html")
|
prettier: $(shell find lnbits -name "*.js" -o -name ".html")
|
||||||
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||||
|
|
||||||
black:
|
black:
|
||||||
|
|
@ -18,7 +18,7 @@ mypy:
|
||||||
isort:
|
isort:
|
||||||
poetry run isort .
|
poetry run isort .
|
||||||
|
|
||||||
checkprettier: $(shell find lnbits -name "*.js" -name ".html")
|
checkprettier: $(shell find lnbits -name "*.js" -o -name ".html")
|
||||||
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||||
|
|
||||||
checkblack:
|
checkblack:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an
|
||||||
|
|
||||||
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
|
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
|
||||||
|
|
||||||
See [legend.lnbits.org](https://legend.lnbits.org) for more detailed documentation.
|
See [docs.lnbits.org](https://docs.lnbits.org) for more detailed documentation.
|
||||||
|
|
||||||
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ Wallets can be easily generated and given out to people at events (one click mul
|
||||||
If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||||
|
|
||||||
|
|
||||||
[docs]: https://legend.lnbits.org/
|
[docs]: https://docs.lnbits.org/
|
||||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||||
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
|
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
|
||||||
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
|
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
legend.lnbits.org
|
docs.lnbits.org
|
||||||
|
|
@ -3,7 +3,7 @@ remote_theme: pmarsceill/just-the-docs
|
||||||
color_scheme: dark
|
color_scheme: dark
|
||||||
logo: "/logos/lnbits-full--inverse.png"
|
logo: "/logos/lnbits-full--inverse.png"
|
||||||
search_enabled: true
|
search_enabled: true
|
||||||
url: https://legend.lnbits.org
|
url: https://docs.lnbits.org
|
||||||
aux_links:
|
aux_links:
|
||||||
"LNbits on GitHub":
|
"LNbits on GitHub":
|
||||||
- "//github.com/lnbits/lnbits"
|
- "//github.com/lnbits/lnbits"
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,4 @@ nav_order: 3
|
||||||
API reference
|
API reference
|
||||||
=============
|
=============
|
||||||
|
|
||||||
[Swagger Docs](https://legend.lnbits.org/devs/swagger.html)
|
[Swagger Docs](https://docs.lnbits.org/devs/swagger.html)
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,33 @@ Allowed Users
|
||||||
=============
|
=============
|
||||||
enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids
|
enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids
|
||||||
By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend.
|
By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend.
|
||||||
|
|
||||||
|
|
||||||
|
How to activate
|
||||||
|
=============
|
||||||
|
```
|
||||||
|
$ sudo systemctl stop lnbits.service
|
||||||
|
$ cd ~/lnbits-legend
|
||||||
|
$ sudo nano .env
|
||||||
|
```
|
||||||
|
-> set: `LNBITS_ADMIN_UI=true`
|
||||||
|
|
||||||
|
Now start LNbits once in the terminal window
|
||||||
|
```
|
||||||
|
$ poetry run lnbits
|
||||||
|
```
|
||||||
|
It will now show you the Super User Account:
|
||||||
|
|
||||||
|
`SUCCESS | ✔️ Access super user account at: https://127.0.0.1:5000/wallet?usr=5711d7..`
|
||||||
|
|
||||||
|
The `/wallet?usr=..` is your super user account. You just have to append it to your normal LNbits web domain.
|
||||||
|
|
||||||
|
After that you will find the __`Admin` / `Manage Server`__ between `Wallets` and `Extensions`
|
||||||
|
|
||||||
|
Here you can design the interface, it has TOPUP to fill wallets and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
|
||||||
|
|
||||||
|
Do not forget
|
||||||
|
```
|
||||||
|
sudo systemctl start lnbits.service
|
||||||
|
```
|
||||||
|
A little hint, if you set `RESET TO DEFAULTS`, then a new Super User Account will also be created. The old one is then no longer valid.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
};
|
};
|
||||||
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
||||||
let
|
let
|
||||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
forSystems = systems: f:
|
forSystems = systems: f:
|
||||||
nixpkgs.lib.genAttrs systems
|
nixpkgs.lib.genAttrs systems
|
||||||
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
|
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from binascii import unhexlify
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import List, NamedTuple, Optional
|
from typing import List, NamedTuple, Optional
|
||||||
|
|
||||||
|
|
@ -75,7 +74,7 @@ def decode(pr: str) -> Invoice:
|
||||||
data_length = len(tagdata) / 5
|
data_length = len(tagdata) / 5
|
||||||
|
|
||||||
if tag == "d":
|
if tag == "d":
|
||||||
invoice.description = _trim_to_bytes(tagdata).decode("utf-8")
|
invoice.description = _trim_to_bytes(tagdata).decode()
|
||||||
elif tag == "h" and data_length == 52:
|
elif tag == "h" and data_length == 52:
|
||||||
invoice.description_hash = _trim_to_bytes(tagdata).hex()
|
invoice.description_hash = _trim_to_bytes(tagdata).hex()
|
||||||
elif tag == "p" and data_length == 52:
|
elif tag == "p" and data_length == 52:
|
||||||
|
|
@ -108,7 +107,7 @@ def decode(pr: str) -> Invoice:
|
||||||
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
|
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||||
sig = signature[0:64]
|
sig = signature[0:64]
|
||||||
if invoice.payee:
|
if invoice.payee:
|
||||||
key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
|
key = VerifyingKey.from_string(bytes.fromhex(invoice.payee), curve=SECP256k1)
|
||||||
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
|
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
|
||||||
else:
|
else:
|
||||||
keys = VerifyingKey.from_public_key_recovery(
|
keys = VerifyingKey.from_public_key_recovery(
|
||||||
|
|
@ -131,7 +130,7 @@ def encode(options):
|
||||||
if options["timestamp"]:
|
if options["timestamp"]:
|
||||||
addr.date = int(options["timestamp"])
|
addr.date = int(options["timestamp"])
|
||||||
|
|
||||||
addr.paymenthash = unhexlify(options["paymenthash"])
|
addr.paymenthash = bytes.fromhex(options["paymenthash"])
|
||||||
|
|
||||||
if options["description"]:
|
if options["description"]:
|
||||||
addr.tags.append(("d", options["description"]))
|
addr.tags.append(("d", options["description"]))
|
||||||
|
|
@ -149,8 +148,8 @@ def encode(options):
|
||||||
while len(splits) >= 5:
|
while len(splits) >= 5:
|
||||||
route.append(
|
route.append(
|
||||||
(
|
(
|
||||||
unhexlify(splits[0]),
|
bytes.fromhex(splits[0]),
|
||||||
unhexlify(splits[1]),
|
bytes.fromhex(splits[1]),
|
||||||
int(splits[2]),
|
int(splits[2]),
|
||||||
int(splits[3]),
|
int(splits[3]),
|
||||||
int(splits[4]),
|
int(splits[4]),
|
||||||
|
|
@ -235,7 +234,7 @@ def lnencode(addr, privkey):
|
||||||
raise ValueError("Must include either 'd' or 'h'")
|
raise ValueError("Must include either 'd' or 'h'")
|
||||||
|
|
||||||
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
||||||
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
|
privkey = secp256k1.PrivateKey(bytes.fromhex(privkey))
|
||||||
sig = privkey.ecdsa_sign_recoverable(
|
sig = privkey.ecdsa_sign_recoverable(
|
||||||
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||||
)
|
)
|
||||||
|
|
@ -261,7 +260,7 @@ class LnAddr(object):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
||||||
hexlify(self.pubkey.serialize()).decode("utf-8"),
|
bytes.hex(self.pubkey.serialize()).decode(),
|
||||||
self.amount,
|
self.amount,
|
||||||
self.currency,
|
self.currency,
|
||||||
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
||||||
|
|
|
||||||
|
|
@ -451,6 +451,34 @@ async def update_payment_details(
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def update_payment_extra(
|
||||||
|
payment_hash: str,
|
||||||
|
extra: dict,
|
||||||
|
outgoing: bool = False,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Only update the `extra` field for the payment.
|
||||||
|
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
|
||||||
|
|
||||||
|
row = await (conn or db).fetchone(
|
||||||
|
f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}",
|
||||||
|
(payment_hash,),
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
db_extra = json.loads(row["extra"] if row["extra"] else "{}")
|
||||||
|
db_extra.update(extra)
|
||||||
|
|
||||||
|
await (conn or db).execute(
|
||||||
|
f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ",
|
||||||
|
(json.dumps(db_extra), payment_hash),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
|
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
|
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ async def m007_set_invoice_expiries(db):
|
||||||
)
|
)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
if len(rows):
|
if len(rows):
|
||||||
logger.info(f"Mirgraion: Checking expiry of {len(rows)} invoices")
|
logger.info(f"Migration: Checking expiry of {len(rows)} invoices")
|
||||||
for i, (
|
for i, (
|
||||||
payment_request,
|
payment_request,
|
||||||
checking_id,
|
checking_id,
|
||||||
|
|
@ -238,7 +238,7 @@ async def m007_set_invoice_expiries(db):
|
||||||
invoice.date + invoice.expiry
|
invoice.date + invoice.expiry
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Mirgraion: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||||
)
|
)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import hmac
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Dict, List, NamedTuple, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from ecdsa import SECP256k1, SigningKey # type: ignore
|
from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Extra, validator
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
|
|
@ -46,8 +46,8 @@ class Wallet(BaseModel):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def lnurlauth_key(self, domain: str) -> SigningKey:
|
def lnurlauth_key(self, domain: str) -> SigningKey:
|
||||||
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
|
hashing_key = hashlib.sha256(self.id.encode()).digest()
|
||||||
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
|
linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
|
||||||
|
|
||||||
return SigningKey.from_string(
|
return SigningKey.from_string(
|
||||||
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
||||||
|
|
@ -88,7 +88,7 @@ class Payment(BaseModel):
|
||||||
preimage: str
|
preimage: str
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
expiry: Optional[float]
|
expiry: Optional[float]
|
||||||
extra: Optional[Dict] = {}
|
extra: Dict = {}
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
webhook: Optional[str]
|
webhook: Optional[str]
|
||||||
webhook_status: Optional[int]
|
webhook_status: Optional[int]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from binascii import unhexlify
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
@ -13,12 +12,7 @@ from loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||||
WalletTypeInfo,
|
|
||||||
get_key_type,
|
|
||||||
require_admin_key,
|
|
||||||
require_invoice_key,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||||
from lnbits.requestvars import g
|
from lnbits.requestvars import g
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
|
|
@ -308,7 +302,7 @@ async def perform_lnurlauth(
|
||||||
) -> Optional[LnurlErrorResponse]:
|
) -> Optional[LnurlErrorResponse]:
|
||||||
cb = urlparse(callback)
|
cb = urlparse(callback)
|
||||||
|
|
||||||
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
||||||
|
|
||||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from typing import Dict
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.helpers import get_current_extension_name
|
|
||||||
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,55 @@
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-icon
|
<div class="row">
|
||||||
:name="extension.icon"
|
<div class="col-3">
|
||||||
color="grey-5"
|
<q-img
|
||||||
style="font-size: 4rem"
|
:src="extension.tile"
|
||||||
></q-icon>
|
spinner-color="white"
|
||||||
{% raw %}
|
style="max-width: 100%"
|
||||||
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
|
></q-img>
|
||||||
<small>{{ extension.shortDescription }} </small>{% endraw %}
|
</div>
|
||||||
|
<div class="col-9 q-pl-sm">
|
||||||
|
{% raw %}
|
||||||
|
<div class="text-h5 gt-sm q-mt-sm q-mb-xs">
|
||||||
|
{{ extension.name }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-subtitle2 gt-sm"
|
||||||
|
style="font-size: 11px; height: 34px"
|
||||||
|
>
|
||||||
|
{{ extension.shortDescription }}
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle1 lt-md q-mt-sm q-mb-xs">
|
||||||
|
{{ extension.name }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-subtitle2 lt-md"
|
||||||
|
style="font-size: 9px; height: 34px"
|
||||||
|
>
|
||||||
|
{{ extension.shortDescription }}
|
||||||
|
</div>
|
||||||
|
{% endraw %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div>
|
||||||
|
<q-rating
|
||||||
|
class="gt-sm"
|
||||||
|
disable
|
||||||
|
size="2em"
|
||||||
|
:max="5"
|
||||||
|
color="primary"
|
||||||
|
></q-rating
|
||||||
|
><q-rating
|
||||||
|
class="lt-md"
|
||||||
|
size="1.5em"
|
||||||
|
:max="5"
|
||||||
|
color="primary"
|
||||||
|
></q-rating
|
||||||
|
><q-tooltip>Ratings coming soon</q-tooltip>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
|
<strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
|
||||||
{{LNBITS_DENOMINATION}}
|
{{LNBITS_DENOMINATION}}
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="'{{user.admin}}' == 'True'"
|
v-if="'{{user.super_user}}' == 'True'"
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -36,27 +36,16 @@
|
||||||
v-model="credit"
|
v-model="credit"
|
||||||
>
|
>
|
||||||
<q-input
|
<q-input
|
||||||
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
filled
|
||||||
label="Amount to credit account"
|
label="{{LNBITS_DENOMINATION}} to credit"
|
||||||
|
hint="Press Enter to credit account"
|
||||||
v-model="scope.value"
|
v-model="scope.value"
|
||||||
dense
|
dense
|
||||||
autofocus
|
autofocus
|
||||||
mask="#.##"
|
:mask="'{{LNBITS_DENOMINATION}}' != 'sats' ? '#.##' : '#'"
|
||||||
fill-mask="0"
|
fill-mask="0"
|
||||||
reverse-fill-mask
|
reverse-fill-mask
|
||||||
@keyup.enter="updateBalance(scope.value)"
|
:step="'{{LNBITS_DENOMINATION}}' != 'sats' ? '0.01' : '1'"
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon name="edit" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
v-else
|
|
||||||
type="number"
|
|
||||||
label="Amount to credit account"
|
|
||||||
v-model="scope.value"
|
|
||||||
dense
|
|
||||||
autofocus
|
|
||||||
@keyup.enter="updateBalance(scope.value)"
|
@keyup.enter="updateBalance(scope.value)"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
|
|
@ -231,7 +220,7 @@
|
||||||
<a :href="'lightning:' + props.row.bolt11">
|
<a :href="'lightning:' + props.row.bolt11">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="props.row.bolt11"
|
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
@ -325,7 +314,7 @@
|
||||||
</p>
|
</p>
|
||||||
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||||
<qrcode
|
<qrcode
|
||||||
value="{{wallet.lnurlwithdraw_full}}"
|
value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||||
:options="{width:240}"
|
:options="{width:240}"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -524,7 +513,7 @@
|
||||||
<a :href="'lightning:' + receive.paymentReq">
|
<a :href="'lightning:' + receive.paymentReq">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="receive.paymentReq"
|
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from ..crud import delete_admin_settings, get_admin_settings, update_admin_setti
|
||||||
|
|
||||||
@core_app.get("/admin/api/v1/settings/")
|
@core_app.get("/admin/api/v1/settings/")
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin), # type: ignore
|
user: User = Depends(check_admin),
|
||||||
) -> Optional[AdminSettings]:
|
) -> Optional[AdminSettings]:
|
||||||
admin_settings = await get_admin_settings(user.super_user)
|
admin_settings = await get_admin_settings(user.super_user)
|
||||||
return admin_settings
|
return admin_settings
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import binascii
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
@ -13,6 +12,7 @@ import async_timeout
|
||||||
import httpx
|
import httpx
|
||||||
import pyqrcode
|
import pyqrcode
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
|
Body,
|
||||||
Depends,
|
Depends,
|
||||||
Header,
|
Header,
|
||||||
Query,
|
Query,
|
||||||
|
|
@ -22,7 +22,6 @@ from fastapi import (
|
||||||
WebSocketDisconnect,
|
WebSocketDisconnect,
|
||||||
)
|
)
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
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
|
||||||
|
|
@ -38,7 +37,7 @@ from lnbits.decorators import (
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
from lnbits.helpers import url_for
|
||||||
from lnbits.settings import get_wallet_class, settings
|
from lnbits.settings import get_wallet_class, settings
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
currencies,
|
currencies,
|
||||||
|
|
@ -48,14 +47,11 @@ from lnbits.utils.exchange_rates import (
|
||||||
|
|
||||||
from .. import core_app, db
|
from .. import core_app, db
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_payment,
|
|
||||||
get_payments,
|
get_payments,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
get_total_balance,
|
get_total_balance,
|
||||||
get_wallet,
|
|
||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
save_balance_check,
|
save_balance_check,
|
||||||
update_payment_status,
|
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
|
|
@ -71,6 +67,11 @@ from ..services import (
|
||||||
from ..tasks import api_invoice_listeners
|
from ..tasks import api_invoice_listeners
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.get("/api/v1/health", status_code=HTTPStatus.OK)
|
||||||
|
async def health():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/wallet")
|
@core_app.get("/api/v1/wallet")
|
||||||
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
if wallet.wallet_type == 0:
|
if wallet.wallet_type == 0:
|
||||||
|
|
@ -140,16 +141,14 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
if data.description_hash or data.unhashed_description:
|
if data.description_hash or data.unhashed_description:
|
||||||
try:
|
try:
|
||||||
description_hash = (
|
description_hash = (
|
||||||
binascii.unhexlify(data.description_hash)
|
bytes.fromhex(data.description_hash) if data.description_hash else b""
|
||||||
if data.description_hash
|
|
||||||
else b""
|
|
||||||
)
|
)
|
||||||
unhashed_description = (
|
unhashed_description = (
|
||||||
binascii.unhexlify(data.unhashed_description)
|
bytes.fromhex(data.unhashed_description)
|
||||||
if data.unhashed_description
|
if data.unhashed_description
|
||||||
else b""
|
else b""
|
||||||
)
|
)
|
||||||
except binascii.Error:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
||||||
|
|
@ -214,7 +213,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
lnurl_response = resp["reason"]
|
lnurl_response = resp["reason"]
|
||||||
else:
|
else:
|
||||||
lnurl_response = True
|
lnurl_response = True
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError) as ex:
|
||||||
|
logger.error(ex)
|
||||||
lnurl_response = False
|
lnurl_response = False
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -251,7 +251,7 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||||
)
|
)
|
||||||
async def api_payments_create(
|
async def api_payments_create(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
invoiceData: CreateInvoiceData = Body(...), # type: ignore
|
invoiceData: CreateInvoiceData = Body(...),
|
||||||
):
|
):
|
||||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||||
if not invoiceData.bolt11:
|
if not invoiceData.bolt11:
|
||||||
|
|
@ -387,7 +387,7 @@ async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
|
||||||
jdata = json.dumps(dict(data.dict(), pending=False))
|
jdata = json.dumps(dict(data.dict(), pending=False))
|
||||||
|
|
||||||
yield dict(data=jdata, event=typ)
|
yield dict(data=jdata, event=typ)
|
||||||
except asyncio.CancelledError as e:
|
except asyncio.CancelledError:
|
||||||
logger.debug(f"removing listener for wallet {uid}")
|
logger.debug(f"removing listener for wallet {uid}")
|
||||||
api_invoice_listeners.pop(uid)
|
api_invoice_listeners.pop(uid)
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
@ -536,7 +536,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
||||||
|
|
||||||
params.update(
|
params.update(
|
||||||
description_hash=hashlib.sha256(
|
description_hash=hashlib.sha256(
|
||||||
data["metadata"].encode("utf-8")
|
data["metadata"].encode()
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
)
|
)
|
||||||
metadata = json.loads(data["metadata"])
|
metadata = json.loads(data["metadata"])
|
||||||
|
|
@ -658,7 +658,7 @@ async def img(request: Request, data):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/audit/", dependencies=[Depends(check_admin)])
|
@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)])
|
||||||
async def api_auditor():
|
async def api_auditor():
|
||||||
WALLET = get_wallet_class()
|
WALLET = get_wallet_class()
|
||||||
total_balance = await get_total_balance()
|
total_balance = await get_total_balance()
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import Request, status
|
from fastapi import Depends, Query, Request, status
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.params import Depends, Query
|
|
||||||
from fastapi.responses import FileResponse, RedirectResponse
|
from fastapi.responses import FileResponse, RedirectResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
@ -38,7 +37,7 @@ async def favicon():
|
||||||
|
|
||||||
|
|
||||||
@core_html_routes.get("/", response_class=HTMLResponse)
|
@core_html_routes.get("/", response_class=HTMLResponse)
|
||||||
async def home(request: Request, lightning: str = None):
|
async def home(request: Request, lightning: str = ""):
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"core/index.html", {"request": request, "lnurl": lightning}
|
"core/index.html", {"request": request, "lnurl": lightning}
|
||||||
)
|
)
|
||||||
|
|
@ -49,9 +48,9 @@ async def home(request: Request, lightning: str = None):
|
||||||
)
|
)
|
||||||
async def extensions(
|
async def extensions(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists), # type: ignore
|
user: User = Depends(check_user_exists),
|
||||||
enable: str = Query(None), # type: ignore
|
enable: str = Query(None),
|
||||||
disable: str = Query(None), # type: ignore
|
disable: str = Query(None),
|
||||||
):
|
):
|
||||||
extension_to_enable = enable
|
extension_to_enable = enable
|
||||||
extension_to_disable = disable
|
extension_to_disable = disable
|
||||||
|
|
@ -103,10 +102,10 @@ nothing: create everything<br>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
async def wallet(
|
async def wallet(
|
||||||
request: Request = Query(None), # type: ignore
|
request: Request = Query(None),
|
||||||
nme: Optional[str] = Query(None), # type: ignore
|
nme: Optional[str] = Query(None),
|
||||||
usr: Optional[UUID4] = Query(None), # type: ignore
|
usr: Optional[UUID4] = Query(None),
|
||||||
wal: Optional[UUID4] = Query(None), # type: ignore
|
wal: Optional[UUID4] = Query(None),
|
||||||
):
|
):
|
||||||
user_id = usr.hex if usr else None
|
user_id = usr.hex if usr else None
|
||||||
wallet_id = wal.hex if wal else None
|
wallet_id = wal.hex if wal else None
|
||||||
|
|
@ -124,12 +123,17 @@ async def wallet(
|
||||||
if (
|
if (
|
||||||
len(settings.lnbits_allowed_users) > 0
|
len(settings.lnbits_allowed_users) > 0
|
||||||
and user_id not in settings.lnbits_allowed_users
|
and user_id not in settings.lnbits_allowed_users
|
||||||
|
and user_id not in settings.lnbits_admin_users
|
||||||
|
and user_id != settings.super_user
|
||||||
):
|
):
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"error.html", {"request": request, "err": "User not authorized."}
|
"error.html", {"request": request, "err": "User not authorized."}
|
||||||
)
|
)
|
||||||
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
||||||
user.admin = True
|
user.admin = True
|
||||||
|
if user_id == settings.super_user:
|
||||||
|
user.super_user = True
|
||||||
|
|
||||||
if not wallet_id:
|
if not wallet_id:
|
||||||
if user.wallets and not wallet_name: # type: ignore
|
if user.wallets and not wallet_name: # type: ignore
|
||||||
wallet = user.wallets[0] # type: ignore
|
wallet = user.wallets[0] # type: ignore
|
||||||
|
|
@ -214,7 +218,7 @@ async def lnurl_full_withdraw_callback(request: Request):
|
||||||
|
|
||||||
|
|
||||||
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
|
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
|
||||||
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)): # type: ignore
|
async def deletewallet(wal: str = Query(...), usr: str = Query(...)):
|
||||||
user = await get_user(usr)
|
user = await get_user(usr)
|
||||||
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
|
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
|
||||||
|
|
||||||
|
|
@ -310,7 +314,7 @@ async def manifest(usr: str):
|
||||||
|
|
||||||
|
|
||||||
@core_html_routes.get("/admin", response_class=HTMLResponse)
|
@core_html_routes.get("/admin", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_admin)): # type: ignore
|
async def index(request: Request, user: User = Depends(check_admin)):
|
||||||
WALLET = get_wallet_class()
|
WALLET = get_wallet_class()
|
||||||
_, balance = await WALLET.status()
|
_, balance = await WALLET.status()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from cerberus import Validator # type: ignore
|
from fastapi import Security, status
|
||||||
from fastapi import status
|
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||||
from fastapi.params import Security
|
|
||||||
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
|
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
|
||||||
from fastapi.security.base import SecurityBase
|
from fastapi.security.base import SecurityBase
|
||||||
from pydantic.types import UUID4
|
from pydantic.types import UUID4
|
||||||
|
|
@ -118,8 +115,8 @@ api_key_query = APIKeyQuery(
|
||||||
|
|
||||||
async def get_key_type(
|
async def get_key_type(
|
||||||
r: Request,
|
r: Request,
|
||||||
api_key_header: str = Security(api_key_header), # type: ignore
|
api_key_header: str = Security(api_key_header),
|
||||||
api_key_query: str = Security(api_key_query), # type: ignore
|
api_key_query: str = Security(api_key_query),
|
||||||
) -> WalletTypeInfo:
|
) -> WalletTypeInfo:
|
||||||
# 0: admin
|
# 0: admin
|
||||||
# 1: invoice
|
# 1: invoice
|
||||||
|
|
@ -174,8 +171,8 @@ async def get_key_type(
|
||||||
|
|
||||||
async def require_admin_key(
|
async def require_admin_key(
|
||||||
r: Request,
|
r: Request,
|
||||||
api_key_header: str = Security(api_key_header), # type: ignore
|
api_key_header: str = Security(api_key_header),
|
||||||
api_key_query: str = Security(api_key_query), # type: ignore
|
api_key_query: str = Security(api_key_query),
|
||||||
):
|
):
|
||||||
|
|
||||||
token = api_key_header or api_key_query
|
token = api_key_header or api_key_query
|
||||||
|
|
@ -200,8 +197,8 @@ async def require_admin_key(
|
||||||
|
|
||||||
async def require_invoice_key(
|
async def require_invoice_key(
|
||||||
r: Request,
|
r: Request,
|
||||||
api_key_header: str = Security(api_key_header), # type: ignore
|
api_key_header: str = Security(api_key_header),
|
||||||
api_key_query: str = Security(api_key_query), # type: ignore
|
api_key_query: str = Security(api_key_query),
|
||||||
):
|
):
|
||||||
|
|
||||||
token = api_key_header or api_key_query
|
token = api_key_header or api_key_query
|
||||||
|
|
@ -236,8 +233,8 @@ async def check_user_exists(usr: UUID4) -> User:
|
||||||
if (
|
if (
|
||||||
len(settings.lnbits_allowed_users) > 0
|
len(settings.lnbits_allowed_users) > 0
|
||||||
and g().user.id not in settings.lnbits_allowed_users
|
and g().user.id not in settings.lnbits_allowed_users
|
||||||
and g().user.id != settings.super_user
|
|
||||||
and g().user.id not in settings.lnbits_admin_users
|
and g().user.id not in settings.lnbits_admin_users
|
||||||
|
and g().user.id != settings.super_user
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Bleskomat",
|
"name": "Bleskomat",
|
||||||
"short_description": "Connect a Bleskomat ATM to an lnbits",
|
"short_description": "Connect a Bleskomat ATM to an lnbits",
|
||||||
"icon": "money",
|
"tile": "/bleskomat/static/image/bleskomat.png",
|
||||||
"contributors": ["chill117"]
|
"contributors": ["chill117"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import urllib
|
import urllib
|
||||||
from binascii import unhexlify
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
|
@ -19,7 +18,7 @@ def generate_bleskomat_lnurl_signature(
|
||||||
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
|
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
|
||||||
):
|
):
|
||||||
if api_key_encoding == "hex":
|
if api_key_encoding == "hex":
|
||||||
key = unhexlify(api_key_secret)
|
key = bytes.fromhex(api_key_secret)
|
||||||
elif api_key_encoding == "base64":
|
elif api_key_encoding == "base64":
|
||||||
key = base64.b64decode(api_key_secret)
|
key = base64.b64decode(api_key_secret)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/bleskomat/static/image/bleskomat.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -9,11 +9,13 @@
|
||||||
<p>
|
<p>
|
||||||
This extension allows you to connect a Bleskomat ATM to an lnbits
|
This extension allows you to connect a Bleskomat ATM to an lnbits
|
||||||
wallet. It will work with both the
|
wallet. It will work with both the
|
||||||
<a href="https://github.com/samotari/bleskomat"
|
<a class="text-secondary" href="https://github.com/samotari/bleskomat"
|
||||||
>open-source DIY Bleskomat ATM project</a
|
>open-source DIY Bleskomat ATM project</a
|
||||||
>
|
>
|
||||||
as well as the
|
as well as the
|
||||||
<a href="https://www.bleskomat.com/">commercial Bleskomat ATM</a>.
|
<a class="text-secondary" href="https://www.bleskomat.com/"
|
||||||
|
>commercial Bleskomat ATM</a
|
||||||
|
>.
|
||||||
</p>
|
</p>
|
||||||
<h5 class="text-subtitle1 q-my-none">Connect Your Bleskomat ATM</h5>
|
<h5 class="text-subtitle1 q-my-none">Connect Your Bleskomat ATM</h5>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
This extension allows you to link your Bolt Card (or other compatible NXP NTAG device) with a LNbits instance and use it in a more secure way than a static LNURLw. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
|
This extension allows you to link your Bolt Card (or other compatible NXP NTAG device) with a LNbits instance and use it in a more secure way than a static LNURLw. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
|
||||||
|
|
||||||
<a href="https://www.youtube.com/watch?v=wJ7QLFTRjK0">Tutorial</a>
|
<a class="text-secondary" href="https://www.youtube.com/watch?v=wJ7QLFTRjK0">Tutorial</a>
|
||||||
|
|
||||||
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
||||||
|
|
||||||
|
|
@ -55,6 +55,8 @@ Since v0.1.2 of Boltcard NFC Card Creator it is possible not only reset the keys
|
||||||
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
||||||
- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
||||||
|
|
||||||
|
If you somehow find yourself in some non-standard state (for instance only k3 and k4 remains filled after previous unsuccessful reset), then you need edit the key fields manually (for instance leave k0-k2 to zeroes and provide the right k3 and k4).
|
||||||
|
|
||||||
## Setting the card - computer (hard way)
|
## Setting the card - computer (hard way)
|
||||||
|
|
||||||
Follow the guide.
|
Follow the guide.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Bolt Cards",
|
"name": "Bolt Cards",
|
||||||
"short_description": "Self custody Bolt Cards with one time LNURLw",
|
"short_description": "Self custody Bolt Cards with one time LNURLw",
|
||||||
"icon": "payment",
|
"tile": "/boltcards/static/image/boltcard.png",
|
||||||
"contributors": ["iwarpbtc", "arcbtc", "leesalminen"]
|
"contributors": ["iwarpbtc", "arcbtc", "leesalminen"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import date, datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
@ -124,7 +124,6 @@ async def get_card_by_otp(otp: str) -> Optional[Card]:
|
||||||
|
|
||||||
async def delete_card(card_id: str) -> None:
|
async def delete_card(card_id: str) -> None:
|
||||||
# Delete cards
|
# Delete cards
|
||||||
card = await get_card(card_id)
|
|
||||||
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
|
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
|
||||||
# Delete hits
|
# Delete hits
|
||||||
hits = await get_hits([card_id])
|
hits = await get_hits([card_id])
|
||||||
|
|
@ -146,7 +145,7 @@ async def update_card_counter(counter: int, id: str):
|
||||||
|
|
||||||
|
|
||||||
async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
|
async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
|
||||||
row = await db.execute(
|
await db.execute(
|
||||||
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
|
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
|
||||||
(enable, id),
|
(enable, id),
|
||||||
)
|
)
|
||||||
|
|
@ -161,7 +160,7 @@ async def update_card_otp(otp: str, id: str):
|
||||||
|
|
||||||
|
|
||||||
async def get_hit(hit_id: str) -> Optional[Hit]:
|
async def get_hit(hit_id: str) -> Optional[Hit]:
|
||||||
row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id))
|
row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id,))
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -182,7 +181,7 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
|
||||||
return [Hit(**row) for row in rows]
|
return [Hit(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_hits_today(card_id: str) -> Optional[Hit]:
|
async def get_hits_today(card_id: str) -> List[Hit]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
f"SELECT * FROM boltcards.hits WHERE card_id = ?",
|
f"SELECT * FROM boltcards.hits WHERE card_id = ?",
|
||||||
(card_id,),
|
(card_id,),
|
||||||
|
|
@ -259,7 +258,7 @@ async def create_refund(hit_id, refund_amount) -> Refund:
|
||||||
|
|
||||||
async def get_refund(refund_id: str) -> Optional[Refund]:
|
async def get_refund(refund_id: str) -> Optional[Refund]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id)
|
f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id,)
|
||||||
)
|
)
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
|
@ -267,7 +266,7 @@ async def get_refund(refund_id: str) -> Optional[Refund]:
|
||||||
return Refund.parse_obj(refund)
|
return Refund.parse_obj(refund)
|
||||||
|
|
||||||
|
|
||||||
async def get_refunds(hits_ids: Union[str, List[str]]) -> List[Refund]:
|
async def get_refunds(hits_ids: List[Hit]) -> List[Refund]:
|
||||||
if len(hits_ids) == 0:
|
if len(hits_ids) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,9 @@ import secrets
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import HTTPException, Query, Request
|
||||||
from fastapi.param_functions import Query
|
from lnurl import encode as lnurl_encode
|
||||||
from fastapi.params import Depends, Query
|
from lnurl.types import LnurlPayMetadata
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
|
||||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
|
|
@ -28,14 +24,13 @@ from .crud import (
|
||||||
update_card_counter,
|
update_card_counter,
|
||||||
update_card_otp,
|
update_card_otp,
|
||||||
)
|
)
|
||||||
from .models import CreateCardData
|
|
||||||
from .nxp424 import decryptSUN, getSunMAC
|
from .nxp424 import decryptSUN, getSunMAC
|
||||||
|
|
||||||
###############LNURLWITHDRAW#################
|
###############LNURLWITHDRAW#################
|
||||||
|
|
||||||
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
|
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
|
||||||
@boltcards_ext.get("/api/v1/scan/{external_id}")
|
@boltcards_ext.get("/api/v1/scan/{external_id}")
|
||||||
async def api_scan(p, c, request: Request, external_id: str = None):
|
async def api_scan(p, c, request: Request, external_id: str = Query(None)):
|
||||||
# some wallets send everything as lower case, no bueno
|
# some wallets send everything as lower case, no bueno
|
||||||
p = p.upper()
|
p = p.upper()
|
||||||
c = c.upper()
|
c = c.upper()
|
||||||
|
|
@ -63,6 +58,7 @@ async def api_scan(p, c, request: Request, external_id: str = None):
|
||||||
await update_card_counter(ctr_int, card.id)
|
await update_card_counter(ctr_int, card.id)
|
||||||
|
|
||||||
# gathering some info for hit record
|
# gathering some info for hit record
|
||||||
|
assert request.client
|
||||||
ip = request.client.host
|
ip = request.client.host
|
||||||
if "x-real-ip" in request.headers:
|
if "x-real-ip" in request.headers:
|
||||||
ip = request.headers["x-real-ip"]
|
ip = request.headers["x-real-ip"]
|
||||||
|
|
@ -95,7 +91,6 @@ async def api_scan(p, c, request: Request, external_id: str = None):
|
||||||
name="boltcards.lnurl_callback",
|
name="boltcards.lnurl_callback",
|
||||||
)
|
)
|
||||||
async def lnurl_callback(
|
async def lnurl_callback(
|
||||||
request: Request,
|
|
||||||
pr: str = Query(None),
|
pr: str = Query(None),
|
||||||
k1: str = Query(None),
|
k1: str = Query(None),
|
||||||
):
|
):
|
||||||
|
|
@ -120,7 +115,9 @@ async def lnurl_callback(
|
||||||
return {"status": "ERROR", "reason": "Failed to decode payment request"}
|
return {"status": "ERROR", "reason": "Failed to decode payment request"}
|
||||||
|
|
||||||
card = await get_card(hit.card_id)
|
card = await get_card(hit.card_id)
|
||||||
|
assert card
|
||||||
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
|
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
|
||||||
|
assert hit
|
||||||
try:
|
try:
|
||||||
await pay_invoice(
|
await pay_invoice(
|
||||||
wallet_id=card.wallet,
|
wallet_id=card.wallet,
|
||||||
|
|
@ -155,7 +152,7 @@ async def api_auth(a, request: Request):
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"card_name": card.card_name,
|
"card_name": card.card_name,
|
||||||
"id": 1,
|
"id": str(1),
|
||||||
"k0": card.k0,
|
"k0": card.k0,
|
||||||
"k1": card.k1,
|
"k1": card.k1,
|
||||||
"k2": card.k2,
|
"k2": card.k2,
|
||||||
|
|
@ -163,7 +160,7 @@ async def api_auth(a, request: Request):
|
||||||
"k4": card.k2,
|
"k4": card.k2,
|
||||||
"lnurlw_base": "lnurlw://" + lnurlw_base,
|
"lnurlw_base": "lnurlw://" + lnurlw_base,
|
||||||
"protocol_name": "new_bolt_card_response",
|
"protocol_name": "new_bolt_card_response",
|
||||||
"protocol_version": 1,
|
"protocol_version": str(1),
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
@ -179,7 +176,9 @@ async def api_auth(a, request: Request):
|
||||||
)
|
)
|
||||||
async def lnurlp_response(req: Request, hit_id: str = Query(None)):
|
async def lnurlp_response(req: Request, hit_id: str = Query(None)):
|
||||||
hit = await get_hit(hit_id)
|
hit = await get_hit(hit_id)
|
||||||
|
assert hit
|
||||||
card = await get_card(hit.card_id)
|
card = await get_card(hit.card_id)
|
||||||
|
assert card
|
||||||
if not hit:
|
if not hit:
|
||||||
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
||||||
if not card.enable:
|
if not card.enable:
|
||||||
|
|
@ -199,21 +198,21 @@ async def lnurlp_response(req: Request, hit_id: str = Query(None)):
|
||||||
response_class=HTMLResponse,
|
response_class=HTMLResponse,
|
||||||
name="boltcards.lnurlp_callback",
|
name="boltcards.lnurlp_callback",
|
||||||
)
|
)
|
||||||
async def lnurlp_callback(
|
async def lnurlp_callback(hit_id: str = Query(None), amount: str = Query(None)):
|
||||||
req: Request, hit_id: str = Query(None), amount: str = Query(None)
|
|
||||||
):
|
|
||||||
hit = await get_hit(hit_id)
|
hit = await get_hit(hit_id)
|
||||||
|
assert hit
|
||||||
card = await get_card(hit.card_id)
|
card = await get_card(hit.card_id)
|
||||||
|
assert card
|
||||||
if not hit:
|
if not hit:
|
||||||
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
||||||
|
|
||||||
payment_hash, payment_request = await create_invoice(
|
_, payment_request = await create_invoice(
|
||||||
wallet_id=card.wallet,
|
wallet_id=card.wallet,
|
||||||
amount=int(amount) / 1000,
|
amount=int(int(amount) / 1000),
|
||||||
memo=f"Refund {hit_id}",
|
memo=f"Refund {hit_id}",
|
||||||
unhashed_description=LnurlPayMetadata(
|
unhashed_description=LnurlPayMetadata(
|
||||||
json.dumps([["text/plain", "Refund"]])
|
json.dumps([["text/plain", "Refund"]])
|
||||||
).encode("utf-8"),
|
).encode(),
|
||||||
extra={"refund": hit_id},
|
extra={"refund": hit_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
|
import json
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Query, Request
|
||||||
from fastapi.params import Query
|
|
||||||
from lnurl import Lnurl
|
from lnurl import Lnurl
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode
|
||||||
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
from lnurl.types import LnurlPayMetadata
|
||||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.main import BaseModel
|
|
||||||
|
|
||||||
ZERO_KEY = "00000000000000000000000000000000"
|
ZERO_KEY = "00000000000000000000000000000000"
|
||||||
|
|
||||||
|
|
@ -32,6 +29,7 @@ class Card(BaseModel):
|
||||||
otp: str
|
otp: str
|
||||||
time: int
|
time: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Card":
|
def from_row(cls, row: Row) -> "Card":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
@ -40,7 +38,7 @@ class Card(BaseModel):
|
||||||
return lnurl_encode(url)
|
return lnurl_encode(url)
|
||||||
|
|
||||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||||
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
return LnurlPayMetadata(json.dumps([["text/plain", self.card_name]]))
|
||||||
|
|
||||||
|
|
||||||
class CreateCardData(BaseModel):
|
class CreateCardData(BaseModel):
|
||||||
|
|
@ -69,6 +67,7 @@ class Hit(BaseModel):
|
||||||
amount: int
|
amount: int
|
||||||
time: int
|
time: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Hit":
|
def from_row(cls, row: Row) -> "Hit":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
@ -79,5 +78,6 @@ class Refund(BaseModel):
|
||||||
refund_amount: int
|
refund_amount: int
|
||||||
time: int
|
time: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Refund":
|
def from_row(cls, row: Row) -> "Refund":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/boltcards/static/image/boltcard.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
|
@ -1,8 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
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.helpers import get_current_extension_name
|
||||||
|
|
@ -21,22 +19,23 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
|
||||||
if not payment.extra.get("refund"):
|
if not payment.extra.get("refund"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if payment.extra.get("wh_status"):
|
if payment.extra.get("wh_status"):
|
||||||
# this webhook has already been sent
|
# this webhook has already been sent
|
||||||
return
|
return
|
||||||
hit = await get_hit(payment.extra.get("refund"))
|
|
||||||
|
hit = await get_hit(str(payment.extra.get("refund")))
|
||||||
|
|
||||||
if hit:
|
if hit:
|
||||||
refund = await create_refund(
|
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
||||||
hit_id=hit.id, refund_amount=(payment.amount / 1000)
|
|
||||||
)
|
|
||||||
await mark_webhook_sent(payment, 1)
|
await mark_webhook_sent(payment, 1)
|
||||||
|
|
||||||
|
|
||||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||||
|
|
||||||
payment.extra["wh_status"] = status
|
payment.extra["wh_status"] = status
|
||||||
|
|
||||||
await core_db.execute(
|
await core_db.execute(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
Manage your Bolt Cards self custodian way<br />
|
Manage your Bolt Cards self custodian way<br />
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards"
|
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards"
|
||||||
>More details</a
|
>More details</a
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,7 @@
|
||||||
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
||||||
(QR for <strong>create</strong> the card in
|
(QR for <strong>create</strong> the card in
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="color: inherit"
|
style="color: inherit"
|
||||||
|
|
@ -395,6 +396,7 @@
|
||||||
<p class="text-center" v-show="qrCodeDialog.wipe">
|
<p class="text-center" v-show="qrCodeDialog.wipe">
|
||||||
(QR for <strong>wipe</strong> the card in
|
(QR for <strong>wipe</strong> the card in
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="color: inherit"
|
style="color: inherit"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.params import Depends
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import secrets
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi.params import Depends, Query
|
from fastapi import Depends, HTTPException, Query
|
||||||
from loguru import logger
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from starlette.requests import Request
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
|
@ -15,13 +11,11 @@ from .crud import (
|
||||||
delete_card,
|
delete_card,
|
||||||
enable_disable_card,
|
enable_disable_card,
|
||||||
get_card,
|
get_card,
|
||||||
get_card_by_otp,
|
|
||||||
get_card_by_uid,
|
get_card_by_uid,
|
||||||
get_cards,
|
get_cards,
|
||||||
get_hits,
|
get_hits,
|
||||||
get_refunds,
|
get_refunds,
|
||||||
update_card,
|
update_card,
|
||||||
update_card_otp,
|
|
||||||
)
|
)
|
||||||
from .models import CreateCardData
|
from .models import CreateCardData
|
||||||
|
|
||||||
|
|
@ -33,7 +27,8 @@ async def api_cards(
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
return [card.dict() for card in await get_cards(wallet_ids)]
|
return [card.dict() for card in await get_cards(wallet_ids)]
|
||||||
|
|
||||||
|
|
@ -41,9 +36,8 @@ async def api_cards(
|
||||||
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
|
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
|
||||||
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
|
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_card_create_or_update(
|
async def api_card_create_or_update(
|
||||||
# req: Request,
|
|
||||||
data: CreateCardData,
|
data: CreateCardData,
|
||||||
card_id: str = None,
|
card_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
|
@ -95,6 +89,7 @@ async def api_card_create_or_update(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
card = await create_card(wallet_id=wallet.wallet.id, data=data)
|
card = await create_card(wallet_id=wallet.wallet.id, data=data)
|
||||||
|
assert card
|
||||||
return card.dict()
|
return card.dict()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -110,6 +105,7 @@ async def enable_card(
|
||||||
if card.wallet != wallet.wallet.id:
|
if card.wallet != wallet.wallet.id:
|
||||||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||||
card = await enable_disable_card(enable=enable, id=card_id)
|
card = await enable_disable_card(enable=enable, id=card_id)
|
||||||
|
assert card
|
||||||
return card.dict()
|
return card.dict()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -136,7 +132,8 @@ async def api_hits(
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
cards = await get_cards(wallet_ids)
|
cards = await get_cards(wallet_ids)
|
||||||
cards_ids = []
|
cards_ids = []
|
||||||
|
|
@ -147,21 +144,19 @@ async def api_hits(
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.get("/api/v1/refunds")
|
@boltcards_ext.get("/api/v1/refunds")
|
||||||
async def api_hits(
|
async def api_refunds(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
cards = await get_cards(wallet_ids)
|
cards = await get_cards(wallet_ids)
|
||||||
cards_ids = []
|
cards_ids = []
|
||||||
for card in cards:
|
for card in cards:
|
||||||
cards_ids.append(card.id)
|
cards_ids.append(card.id)
|
||||||
hits = await get_hits(cards_ids)
|
hits = await get_hits(cards_ids)
|
||||||
hits_ids = []
|
|
||||||
for hit in hits:
|
|
||||||
hits_ids.append(hit.id)
|
|
||||||
|
|
||||||
return [refund.dict() for refund in await get_refunds(hits_ids)]
|
return [refund.dict() for refund in await get_refunds(hits)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
|
|
@ -15,6 +16,14 @@ def boltz_renderer():
|
||||||
return template_renderer(["lnbits/extensions/boltz/templates"])
|
return template_renderer(["lnbits/extensions/boltz/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
boltz_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/boltz/static",
|
||||||
|
"app": StaticFiles(directory="lnbits/extensions/boltz/static"),
|
||||||
|
"name": "boltz_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
from .tasks import check_for_pending_swaps, wait_for_paid_invoices
|
from .tasks import check_for_pending_swaps, wait_for_paid_invoices
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from binascii import hexlify, unhexlify
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Awaitable, Union
|
from typing import Awaitable, Union
|
||||||
|
|
||||||
|
|
@ -56,7 +55,7 @@ async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
refund_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
refund_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
||||||
refund_pubkey_hex = hexlify(refund_privkey.sec()).decode("UTF-8")
|
refund_pubkey_hex = bytes.hex(refund_privkey.sec()).decode()
|
||||||
|
|
||||||
res = req_wrap(
|
res = req_wrap(
|
||||||
"post",
|
"post",
|
||||||
|
|
@ -121,7 +120,7 @@ async def create_reverse_swap(
|
||||||
return False
|
return False
|
||||||
|
|
||||||
claim_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
claim_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
||||||
claim_pubkey_hex = hexlify(claim_privkey.sec()).decode("UTF-8")
|
claim_pubkey_hex = bytes.hex(claim_privkey.sec()).decode()
|
||||||
preimage = os.urandom(32)
|
preimage = os.urandom(32)
|
||||||
preimage_hash = sha256(preimage).hexdigest()
|
preimage_hash = sha256(preimage).hexdigest()
|
||||||
|
|
||||||
|
|
@ -311,12 +310,12 @@ async def create_onchain_tx(
|
||||||
sequence = 0xFFFFFFFE
|
sequence = 0xFFFFFFFE
|
||||||
else:
|
else:
|
||||||
privkey = ec.PrivateKey.from_wif(swap.claim_privkey)
|
privkey = ec.PrivateKey.from_wif(swap.claim_privkey)
|
||||||
preimage = unhexlify(swap.preimage)
|
preimage = bytes.fromhex(swap.preimage)
|
||||||
onchain_address = swap.onchain_address
|
onchain_address = swap.onchain_address
|
||||||
sequence = 0xFFFFFFFF
|
sequence = 0xFFFFFFFF
|
||||||
|
|
||||||
locktime = swap.timeout_block_height
|
locktime = swap.timeout_block_height
|
||||||
redeem_script = unhexlify(swap.redeem_script)
|
redeem_script = bytes.fromhex(swap.redeem_script)
|
||||||
|
|
||||||
fees = get_fee_estimation()
|
fees = get_fee_estimation()
|
||||||
|
|
||||||
|
|
@ -324,7 +323,7 @@ async def create_onchain_tx(
|
||||||
|
|
||||||
script_pubkey = script.address_to_scriptpubkey(onchain_address)
|
script_pubkey = script.address_to_scriptpubkey(onchain_address)
|
||||||
|
|
||||||
vin = [TransactionInput(unhexlify(txid), vout_cnt, sequence=sequence)]
|
vin = [TransactionInput(bytes.fromhex(txid), vout_cnt, sequence=sequence)]
|
||||||
vout = [TransactionOutput(vout_amount - fees, script_pubkey)]
|
vout = [TransactionOutput(vout_amount - fees, script_pubkey)]
|
||||||
tx = Transaction(vin=vin, vout=vout)
|
tx = Transaction(vin=vin, vout=vout)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Boltz",
|
"name": "Boltz",
|
||||||
"short_description": "Perform onchain/offchain swaps",
|
"short_description": "Perform onchain/offchain swaps",
|
||||||
"icon": "swap_horiz",
|
"tile": "/boltz/static/image/boltz.png",
|
||||||
"contributors": ["dni"]
|
"contributors": ["dni"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from binascii import hexlify
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import websockets
|
import websockets
|
||||||
|
|
@ -84,7 +83,7 @@ def get_mempool_blockheight() -> int:
|
||||||
|
|
||||||
|
|
||||||
async def send_onchain_tx(tx: Transaction):
|
async def send_onchain_tx(tx: Transaction):
|
||||||
raw = hexlify(tx.serialize())
|
raw = bytes.hex(tx.serialize())
|
||||||
logger.debug(f"Boltz - mempool sending onchain tx...")
|
logger.debug(f"Boltz - mempool sending onchain tx...")
|
||||||
req_wrap(
|
req_wrap(
|
||||||
"post",
|
"post",
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/boltz/static/image/boltz.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
|
@ -24,12 +24,13 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Link :
|
Link :
|
||||||
<a target="_blank" href="https://boltz.exchange"
|
<a class="text-secondary" target="_blank" href="https://boltz.exchange"
|
||||||
>https://boltz.exchange
|
>https://boltz.exchange
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
||||||
>More details</a
|
>More details</a
|
||||||
|
|
@ -38,7 +39,12 @@
|
||||||
<p>
|
<p>
|
||||||
<small
|
<small
|
||||||
>Created by,
|
>Created by,
|
||||||
<a target="_blank" href="https://github.com/dni">dni</a></small
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/dni"
|
||||||
|
>dni</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "Cashu",
|
"name": "Cashu",
|
||||||
"short_description": "Ecash mint and wallet",
|
"short_description": "Ecash mint and wallet",
|
||||||
"icon": "account_balance",
|
"tile": "/cashu/static/image/cashu.png",
|
||||||
"contributors": ["calle", "vlad", "arcbtc"],
|
"contributors": ["calle", "vlad", "arcbtc"],
|
||||||
"hidden": false
|
"hidden": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from binascii import hexlify, unhexlify
|
|
||||||
from typing import Any, List, Optional, Union
|
from typing import Any, List, Optional, Union
|
||||||
|
|
||||||
from cashu.core.base import MintKeyset
|
from cashu.core.base import MintKeyset
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/cashu/static/image/cashu.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -28,6 +28,7 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra and not payment.extra.get("tag") == "cashu":
|
if payment.extra.get("tag") != "cashu":
|
||||||
return
|
return
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,24 @@
|
||||||
<p>Create Cashu ecash mints and wallets.</p>
|
<p>Create Cashu ecash mints and wallets.</p>
|
||||||
<small
|
<small
|
||||||
>Created by
|
>Created by
|
||||||
<a href="https://github.com/arcbtc" target="_blank">arcbtc</a>,
|
<a
|
||||||
<a href="https://github.com/motorina0" target="_blank">vlad</a>,
|
class="text-secondary"
|
||||||
<a href="https://github.com/calle" target="_blank">calle</a>.</small
|
href="https://github.com/arcbtc"
|
||||||
|
target="_blank"
|
||||||
|
>arcbtc</a
|
||||||
|
>,
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/motorina0"
|
||||||
|
target="_blank"
|
||||||
|
>vlad</a
|
||||||
|
>,
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/calle"
|
||||||
|
target="_blank"
|
||||||
|
>calle</a
|
||||||
|
>.</small
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
></q-icon>
|
></q-icon>
|
||||||
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
class="q-my-xl text-white"
|
class="q-my-xl text-white"
|
||||||
style="font-size: 1.5rem"
|
style="font-size: 1.5rem"
|
||||||
href="../wallet?mint_id={{ mint_id }}"
|
href="../wallet?mint_id={{ mint_id }}"
|
||||||
|
|
@ -24,7 +25,11 @@
|
||||||
<h5 class="q-my-md">Read the following carefully!</h5>
|
<h5 class="q-my-md">Read the following carefully!</h5>
|
||||||
<p>
|
<p>
|
||||||
This is a
|
This is a
|
||||||
<a href="https://cashu.space/" style="color: white" target="”_blank”"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://cashu.space/"
|
||||||
|
style="color: white"
|
||||||
|
target="”_blank”"
|
||||||
>Cashu</a
|
>Cashu</a
|
||||||
>
|
>
|
||||||
mint. Cashu is an ecash system for Bitcoin.
|
mint. Cashu is an ecash system for Bitcoin.
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ page_container %}
|
||||||
size="lg"
|
size="lg"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="q-mr-md cursor-pointer"
|
class="q-mr-md cursor-pointer"
|
||||||
@click="recheckInvoice(props.row.hash)"
|
@click="checkInvoice(props.row.hash)"
|
||||||
>
|
>
|
||||||
Check
|
Check
|
||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
@ -616,10 +616,10 @@ page_container %}
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center q-mb-lg">
|
<div v-else class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + invoiceData.bolt11">
|
<a class="text-secondary" :href="'lightning:' + invoiceData.bolt11">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="invoiceData.bolt11"
|
:value="'lightning:' + invoiceData.bolt11.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
>
|
>
|
||||||
|
|
@ -681,7 +681,7 @@ page_container %}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center q-mb-lg">
|
<div v-else class="text-center q-mb-lg">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<!-- <a :href="'cashu:' + sendData.tokensBase64"> -->
|
<!-- <a class="text-secondary" :href="'cashu:' + sendData.tokensBase64"> -->
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="disclaimerDialog.base_url + '?mint_id=' + mintId + '&recv_token=' + sendData.tokensBase64"
|
:value="disclaimerDialog.base_url + '?mint_id=' + mintId + '&recv_token=' + sendData.tokensBase64"
|
||||||
|
|
@ -1528,57 +1528,17 @@ page_container %}
|
||||||
return proofs.reduce((s, t) => (s += t.amount), 0)
|
return proofs.reduce((s, t) => (s += t.amount), 0)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteProofs: function (proofs) {
|
||||||
|
// delete proofs from this.proofs
|
||||||
|
const usedSecrets = proofs.map(p => p.secret)
|
||||||
|
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
||||||
|
this.storeProofs()
|
||||||
|
return this.proofs
|
||||||
|
},
|
||||||
|
|
||||||
//////////// API ///////////
|
//////////// API ///////////
|
||||||
clearAllWorkers: function () {
|
|
||||||
if (this.invoiceCheckListener) {
|
|
||||||
clearInterval(this.invoiceCheckListener)
|
|
||||||
}
|
|
||||||
if (this.tokensCheckSpendableListener) {
|
|
||||||
clearInterval(this.tokensCheckSpendableListener)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
invoiceCheckWorker: async function () {
|
|
||||||
let nInterval = 0
|
|
||||||
this.clearAllWorkers()
|
|
||||||
this.invoiceCheckListener = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
nInterval += 1
|
|
||||||
|
|
||||||
// exit loop after 2m
|
// MINT
|
||||||
if (nInterval > 40) {
|
|
||||||
console.log('### stopping invoice check worker')
|
|
||||||
this.clearAllWorkers()
|
|
||||||
}
|
|
||||||
console.log('### invoiceCheckWorker setInterval', nInterval)
|
|
||||||
console.log(this.invoiceData)
|
|
||||||
|
|
||||||
// this will throw an error if the invoice is pending
|
|
||||||
await this.recheckInvoice(this.invoiceData.hash, false)
|
|
||||||
|
|
||||||
// only without error (invoice paid) will we reach here
|
|
||||||
console.log('### stopping invoice check worker')
|
|
||||||
this.clearAllWorkers()
|
|
||||||
this.invoiceData.bolt11 = ''
|
|
||||||
this.showInvoiceDetails = false
|
|
||||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Payment received',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log('not paid yet')
|
|
||||||
}
|
|
||||||
}, 3000)
|
|
||||||
},
|
|
||||||
|
|
||||||
requestMintButton: async function () {
|
requestMintButton: async function () {
|
||||||
await this.requestMint()
|
await this.requestMint()
|
||||||
|
|
@ -1586,8 +1546,12 @@ page_container %}
|
||||||
await this.invoiceCheckWorker()
|
await this.invoiceCheckWorker()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /mint
|
||||||
|
|
||||||
requestMint: async function () {
|
requestMint: async function () {
|
||||||
// gets an invoice from the mint to get new tokens
|
/*
|
||||||
|
gets an invoice from the mint to get new tokens
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -1611,7 +1575,14 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /mint
|
||||||
|
|
||||||
mintApi: async function (amounts, payment_hash, verbose = true) {
|
mintApi: async function (amounts, payment_hash, verbose = true) {
|
||||||
|
/*
|
||||||
|
asks the mint to check whether the invoice with payment_hash has been paid
|
||||||
|
and requests signing of the attached outputs (blindedMessages)
|
||||||
|
*/
|
||||||
console.log('### promises', payment_hash)
|
console.log('### promises', payment_hash)
|
||||||
try {
|
try {
|
||||||
let secrets = await this.generateSecrets(amounts)
|
let secrets = await this.generateSecrets(amounts)
|
||||||
|
|
@ -1647,7 +1618,19 @@ page_container %}
|
||||||
}
|
}
|
||||||
this.proofs = this.proofs.concat(proofs)
|
this.proofs = this.proofs.concat(proofs)
|
||||||
this.storeProofs()
|
this.storeProofs()
|
||||||
|
|
||||||
|
// update UI
|
||||||
await this.setInvoicePaid(payment_hash)
|
await this.setInvoicePaid(payment_hash)
|
||||||
|
tokensBase64 = btoa(JSON.stringify(proofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: amount,
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
|
||||||
return proofs
|
return proofs
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
@ -1657,62 +1640,20 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitToSend: async function (proofs, amount, invlalidate = false) {
|
|
||||||
// splits proofs so the user can keep firstProofs, send scndProofs
|
|
||||||
try {
|
|
||||||
const spendableProofs = proofs.filter(p => !p.reserved)
|
|
||||||
if (this.sumProofs(spendableProofs) < amount) {
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Balance too low',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
throw Error('balance too low.')
|
|
||||||
}
|
|
||||||
let {fristProofs, scndProofs} = await this.split(
|
|
||||||
spendableProofs,
|
|
||||||
amount
|
|
||||||
)
|
|
||||||
|
|
||||||
// set scndProofs in this.proofs as reserved
|
// SPLIT
|
||||||
const usedSecrets = proofs.map(p => p.secret)
|
|
||||||
for (let i = 0; i < this.proofs.length; i++) {
|
|
||||||
if (usedSecrets.includes(this.proofs[i].secret)) {
|
|
||||||
this.proofs[i].reserved = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (invlalidate) {
|
|
||||||
// delete tokens from db
|
|
||||||
this.proofs = fristProofs
|
|
||||||
// add new fristProofs, scndProofs to this.proofs
|
|
||||||
this.storeProofs()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {fristProofs, scndProofs}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
split: async function (proofs, amount) {
|
split: async function (proofs, amount) {
|
||||||
|
/*
|
||||||
|
supplies proofs and requests a split from the mint of these
|
||||||
|
proofs at a specific amount
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
if (proofs.length == 0) {
|
if (proofs.length == 0) {
|
||||||
throw new Error('no proofs provided.')
|
throw new Error('no proofs provided.')
|
||||||
}
|
}
|
||||||
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
||||||
// delete proofs from this.proofs
|
this.deleteProofs(proofs)
|
||||||
const usedSecrets = proofs.map(p => p.secret)
|
|
||||||
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
|
||||||
// add new fristProofs, scndProofs to this.proofs
|
// add new fristProofs, scndProofs to this.proofs
|
||||||
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
||||||
this.storeProofs()
|
this.storeProofs()
|
||||||
|
|
@ -1723,6 +1664,9 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /split
|
||||||
|
|
||||||
splitApi: async function (proofs, amount) {
|
splitApi: async function (proofs, amount) {
|
||||||
try {
|
try {
|
||||||
const total = this.sumProofs(proofs)
|
const total = this.sumProofs(proofs)
|
||||||
|
|
@ -1782,7 +1726,62 @@ page_container %}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
splitToSend: async function (proofs, amount, invlalidate = false) {
|
||||||
|
/*
|
||||||
|
splits proofs so the user can keep firstProofs, send scndProofs.
|
||||||
|
then sets scndProofs as reserved.
|
||||||
|
|
||||||
|
if invalidate, scndProofs (the one to send) are invalidated
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
const spendableProofs = proofs.filter(p => !p.reserved)
|
||||||
|
if (this.sumProofs(spendableProofs) < amount) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Balance too low',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
throw Error('balance too low.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// call /split
|
||||||
|
|
||||||
|
let {fristProofs, scndProofs} = await this.split(
|
||||||
|
spendableProofs,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
// set scndProofs in this.proofs as reserved
|
||||||
|
const usedSecrets = proofs.map(p => p.secret)
|
||||||
|
for (let i = 0; i < this.proofs.length; i++) {
|
||||||
|
if (usedSecrets.includes(this.proofs[i].secret)) {
|
||||||
|
this.proofs[i].reserved = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (invlalidate) {
|
||||||
|
// delete scndProofs from db
|
||||||
|
this.deleteProofs(scndProofs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {fristProofs, scndProofs}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
redeem: async function () {
|
redeem: async function () {
|
||||||
|
/*
|
||||||
|
uses split to receive new tokens.
|
||||||
|
*/
|
||||||
this.showReceiveTokens = false
|
this.showReceiveTokens = false
|
||||||
console.log('### receive tokens', this.receiveData.tokensBase64)
|
console.log('### receive tokens', this.receiveData.tokensBase64)
|
||||||
try {
|
try {
|
||||||
|
|
@ -1793,6 +1792,9 @@ page_container %}
|
||||||
const proofs = JSON.parse(tokenJson)
|
const proofs = JSON.parse(tokenJson)
|
||||||
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
||||||
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
|
||||||
// HACK: we need to do this so the balance updates
|
// HACK: we need to do this so the balance updates
|
||||||
this.proofs = this.proofs.concat([])
|
this.proofs = this.proofs.concat([])
|
||||||
|
|
||||||
|
|
@ -1827,13 +1829,18 @@ page_container %}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendTokens: async function () {
|
sendTokens: async function () {
|
||||||
|
/*
|
||||||
|
calls splitToSend, displays token and kicks off the spendableWorker
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
// keep firstProofs, send scndProofs
|
// keep firstProofs, send scndProofs and delete them (invalidate=true)
|
||||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||||
this.proofs,
|
this.proofs,
|
||||||
this.sendData.amount,
|
this.sendData.amount,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// update UI
|
||||||
this.sendData.tokens = scndProofs
|
this.sendData.tokens = scndProofs
|
||||||
console.log('### this.sendData.tokens', this.sendData.tokens)
|
console.log('### this.sendData.tokens', this.sendData.tokens)
|
||||||
this.sendData.tokensBase64 = btoa(
|
this.sendData.tokensBase64 = btoa(
|
||||||
|
|
@ -1846,33 +1853,19 @@ page_container %}
|
||||||
date: currentDateStr(),
|
date: currentDateStr(),
|
||||||
token: this.sendData.tokensBase64
|
token: this.sendData.tokensBase64
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// store "pending" outgoing tokens in history table
|
||||||
this.storehistoryTokens()
|
this.storehistoryTokens()
|
||||||
|
|
||||||
this.checkTokenSpendableWorker()
|
this.checkTokenSpendableWorker()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkFees: async function (payment_request) {
|
|
||||||
const payload = {
|
// /melt
|
||||||
pr: payment_request
|
|
||||||
}
|
|
||||||
console.log('#### payload', JSON.stringify(payload))
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
`/cashu/api/v1/${this.mintId}/checkfees`,
|
|
||||||
'',
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
console.log('#### checkFees', payment_request, data.fee)
|
|
||||||
return data.fee
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
melt: async function () {
|
melt: async function () {
|
||||||
// todo: get fees from server and add to inputs
|
// todo: get fees from server and add to inputs
|
||||||
this.payInvoiceData.blocking = true
|
this.payInvoiceData.blocking = true
|
||||||
|
|
@ -1924,8 +1917,20 @@ page_container %}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
// delete spent tokens from db
|
// delete spent tokens from db
|
||||||
this.proofs = fristProofs
|
this.deleteProofs(scndProofs)
|
||||||
this.storeProofs()
|
|
||||||
|
// update UI
|
||||||
|
|
||||||
|
tokensBase64 = btoa(JSON.stringify(scndProofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: -amount,
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
amount: -amount,
|
amount: -amount,
|
||||||
bolt11: this.payInvoiceData.data.request,
|
bolt11: this.payInvoiceData.data.request,
|
||||||
|
|
@ -1953,13 +1958,95 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /check
|
||||||
|
|
||||||
|
checkProofsSpendable: async function (proofs, update_history = false) {
|
||||||
|
/*
|
||||||
|
checks with the mint whether an array of proofs is still
|
||||||
|
spendable or already invalidated
|
||||||
|
*/
|
||||||
|
const payload = {
|
||||||
|
proofs: proofs.flat()
|
||||||
|
}
|
||||||
|
console.log('#### payload', JSON.stringify(payload))
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/cashu/api/v1/${this.mintId}/check`,
|
||||||
|
'',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete proofs from database if it is spent
|
||||||
|
let spentProofs = proofs.filter((p, pidx) => !data[pidx])
|
||||||
|
if (spentProofs.length) {
|
||||||
|
this.deleteProofs(spentProofs)
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
if (update_history) {
|
||||||
|
tokensBase64 = btoa(JSON.stringify(spentProofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: -this.sumProofs(spentProofs),
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// /checkfees
|
||||||
|
checkFees: async function (payment_request) {
|
||||||
|
const payload = {
|
||||||
|
pr: payment_request
|
||||||
|
}
|
||||||
|
console.log('#### payload', JSON.stringify(payload))
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/cashu/api/v1/${this.mintId}/checkfees`,
|
||||||
|
'',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
console.log('#### checkFees', payment_request, data.fee)
|
||||||
|
return data.fee
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// /keys
|
||||||
|
|
||||||
|
fetchMintKeys: async function () {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/cashu/api/v1/${this.mintId}/keys`
|
||||||
|
)
|
||||||
|
this.keys = data
|
||||||
|
localStorage.setItem(
|
||||||
|
this.mintKey(this.mintId, 'keys'),
|
||||||
|
JSON.stringify(data)
|
||||||
|
)
|
||||||
|
},
|
||||||
setInvoicePaid: async function (payment_hash) {
|
setInvoicePaid: async function (payment_hash) {
|
||||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||||
invoice.status = 'paid'
|
invoice.status = 'paid'
|
||||||
this.storeinvoicesCashu()
|
this.storeinvoicesCashu()
|
||||||
},
|
},
|
||||||
recheckInvoice: async function (payment_hash, verbose = true) {
|
checkInvoice: async function (payment_hash, verbose = true) {
|
||||||
console.log('### recheckInvoice.hash', payment_hash)
|
console.log('### checkInvoice.hash', payment_hash)
|
||||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||||
try {
|
try {
|
||||||
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
||||||
|
|
@ -1969,15 +2056,15 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
recheckPendingInvoices: async function () {
|
checkPendingInvoices: async function () {
|
||||||
for (const invoice of this.invoicesCashu) {
|
for (const invoice of this.invoicesCashu) {
|
||||||
if (invoice.status === 'pending' && invoice.sat > 0) {
|
if (invoice.status === 'pending' && invoice.amount > 0) {
|
||||||
this.recheckInvoice(invoice.hash, false)
|
this.checkInvoice(invoice.hash, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
recheckPendingTokens: async function () {
|
checkPendingTokens: async function () {
|
||||||
for (const token of this.historyTokens) {
|
for (const token of this.historyTokens) {
|
||||||
if (token.status === 'pending' && token.amount < 0) {
|
if (token.status === 'pending' && token.amount < 0) {
|
||||||
this.checkTokenSpendable(token.token, false)
|
this.checkTokenSpendable(token.token, false)
|
||||||
|
|
@ -1990,6 +2077,113 @@ page_container %}
|
||||||
this.storehistoryTokens()
|
this.storehistoryTokens()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkTokenSpendable: async function (token, verbose = true) {
|
||||||
|
/*
|
||||||
|
checks whether a base64-encoded token (from the history table) has been spent already.
|
||||||
|
if it is spent, the appropraite entry in the history table is set to paid.
|
||||||
|
*/
|
||||||
|
const tokenJson = atob(token)
|
||||||
|
const proofs = JSON.parse(tokenJson)
|
||||||
|
let data = await this.checkProofsSpendable(proofs)
|
||||||
|
|
||||||
|
// iterate through response of form {0: true, 1: false, ...}
|
||||||
|
let paid = false
|
||||||
|
for (const [key, spendable] of Object.entries(data)) {
|
||||||
|
if (!spendable) {
|
||||||
|
this.setTokenPaid(token)
|
||||||
|
paid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paid) {
|
||||||
|
console.log('### token paid')
|
||||||
|
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Token paid',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('### token not paid yet')
|
||||||
|
if (verbose) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
color: 'grey',
|
||||||
|
message: 'Token still pending',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.sendData.tokens = token
|
||||||
|
}
|
||||||
|
return paid
|
||||||
|
},
|
||||||
|
|
||||||
|
////////////// WORKERS //////////////
|
||||||
|
|
||||||
|
clearAllWorkers: function () {
|
||||||
|
if (this.invoiceCheckListener) {
|
||||||
|
clearInterval(this.invoiceCheckListener)
|
||||||
|
}
|
||||||
|
if (this.tokensCheckSpendableListener) {
|
||||||
|
clearInterval(this.tokensCheckSpendableListener)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invoiceCheckWorker: async function () {
|
||||||
|
let nInterval = 0
|
||||||
|
this.clearAllWorkers()
|
||||||
|
this.invoiceCheckListener = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
nInterval += 1
|
||||||
|
|
||||||
|
// exit loop after 2m
|
||||||
|
if (nInterval > 40) {
|
||||||
|
console.log('### stopping invoice check worker')
|
||||||
|
this.clearAllWorkers()
|
||||||
|
}
|
||||||
|
console.log('### invoiceCheckWorker setInterval', nInterval)
|
||||||
|
console.log(this.invoiceData)
|
||||||
|
|
||||||
|
// this will throw an error if the invoice is pending
|
||||||
|
await this.checkInvoice(this.invoiceData.hash, false)
|
||||||
|
|
||||||
|
// only without error (invoice paid) will we reach here
|
||||||
|
console.log('### stopping invoice check worker')
|
||||||
|
this.clearAllWorkers()
|
||||||
|
this.invoiceData.bolt11 = ''
|
||||||
|
this.showInvoiceDetails = false
|
||||||
|
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Payment received',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('not paid yet')
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
},
|
||||||
checkTokenSpendableWorker: async function () {
|
checkTokenSpendableWorker: async function () {
|
||||||
let nInterval = 0
|
let nInterval = 0
|
||||||
this.clearAllWorkers()
|
this.clearAllWorkers()
|
||||||
|
|
@ -2021,83 +2215,6 @@ page_container %}
|
||||||
}, 3000)
|
}, 3000)
|
||||||
},
|
},
|
||||||
|
|
||||||
checkTokenSpendable: async function (token, verbose = true) {
|
|
||||||
const tokenJson = atob(token)
|
|
||||||
const proofs = JSON.parse(tokenJson)
|
|
||||||
const payload = {
|
|
||||||
proofs: proofs.flat()
|
|
||||||
}
|
|
||||||
console.log('#### payload', JSON.stringify(payload))
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
`/cashu/api/v1/${this.mintId}/check`,
|
|
||||||
'',
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
// iterate through response of form {0: true, 1: false, ...}
|
|
||||||
let paid = false
|
|
||||||
for (const [key, spendable] of Object.entries(data)) {
|
|
||||||
if (!spendable) {
|
|
||||||
this.setTokenPaid(token)
|
|
||||||
paid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (paid) {
|
|
||||||
console.log('### token paid')
|
|
||||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Token paid',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('### token not paid yet')
|
|
||||||
if (verbose) {
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
color: 'grey',
|
|
||||||
message: 'Token still pending',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.sendData.tokens = token
|
|
||||||
}
|
|
||||||
return paid
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchMintKeys: async function () {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/cashu/api/v1/${this.mintId}/keys`
|
|
||||||
)
|
|
||||||
this.keys = data
|
|
||||||
localStorage.setItem(
|
|
||||||
this.mintKey(this.mintId, 'keys'),
|
|
||||||
JSON.stringify(data)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -2116,62 +2233,62 @@ page_container %}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
checkInvoice: function () {
|
// checkInvoice: function () {
|
||||||
console.log('#### checkInvoice')
|
// console.log('#### checkInvoice')
|
||||||
try {
|
// try {
|
||||||
const invoice = decode(this.payInvoiceData.data.request)
|
// const invoice = decode(this.payInvoiceData.data.request)
|
||||||
|
|
||||||
const cleanInvoice = {
|
// const cleanInvoice = {
|
||||||
msat: invoice.human_readable_part.amount,
|
// msat: invoice.human_readable_part.amount,
|
||||||
sat: invoice.human_readable_part.amount / 1000,
|
// sat: invoice.human_readable_part.amount / 1000,
|
||||||
fsat: LNbits.utils.formatSat(
|
// fsat: LNbits.utils.formatSat(
|
||||||
invoice.human_readable_part.amount / 1000
|
// invoice.human_readable_part.amount / 1000
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
_.each(invoice.data.tags, tag => {
|
// _.each(invoice.data.tags, tag => {
|
||||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
// if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||||
if (tag.description === 'payment_hash') {
|
// if (tag.description === 'payment_hash') {
|
||||||
cleanInvoice.hash = tag.value
|
// cleanInvoice.hash = tag.value
|
||||||
} else if (tag.description === 'description') {
|
// } else if (tag.description === 'description') {
|
||||||
cleanInvoice.description = tag.value
|
// cleanInvoice.description = tag.value
|
||||||
} else if (tag.description === 'expiry') {
|
// } else if (tag.description === 'expiry') {
|
||||||
var expireDate = new Date(
|
// var expireDate = new Date(
|
||||||
(invoice.data.time_stamp + tag.value) * 1000
|
// (invoice.data.time_stamp + tag.value) * 1000
|
||||||
)
|
// )
|
||||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
// cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||||
expireDate,
|
// expireDate,
|
||||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
// 'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
)
|
// )
|
||||||
cleanInvoice.expired = false // TODO
|
// cleanInvoice.expired = false // TODO
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.payInvoiceData.invoice = cleanInvoice
|
// this.payInvoiceData.invoice = cleanInvoice
|
||||||
})
|
// })
|
||||||
|
|
||||||
console.log(
|
// console.log(
|
||||||
'#### this.payInvoiceData.invoice',
|
// '#### this.payInvoiceData.invoice',
|
||||||
this.payInvoiceData.invoice
|
// this.payInvoiceData.invoice
|
||||||
)
|
// )
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
this.$q.notify({
|
// this.$q.notify({
|
||||||
timeout: 5000,
|
// timeout: 5000,
|
||||||
type: 'warning',
|
// type: 'warning',
|
||||||
message: 'Could not decode invoice',
|
// message: 'Could not decode invoice',
|
||||||
caption: error + '',
|
// caption: error + '',
|
||||||
position: 'top',
|
// position: 'top',
|
||||||
actions: [
|
// actions: [
|
||||||
{
|
// {
|
||||||
icon: 'close',
|
// icon: 'close',
|
||||||
color: 'white',
|
// color: 'white',
|
||||||
handler: () => {}
|
// handler: () => {}
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
})
|
// })
|
||||||
throw error
|
// throw error
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
|
|
||||||
////////////// STORAGE /////////////
|
////////////// STORAGE /////////////
|
||||||
|
|
||||||
|
|
@ -2335,8 +2452,9 @@ page_container %}
|
||||||
console.log('#### this.mintId', this.mintId)
|
console.log('#### this.mintId', this.mintId)
|
||||||
console.log('#### this.mintName', this.mintName)
|
console.log('#### this.mintName', this.mintName)
|
||||||
|
|
||||||
this.recheckPendingInvoices()
|
this.checkProofsSpendable(this.proofs, true)
|
||||||
this.recheckPendingTokens()
|
this.checkPendingInvoices()
|
||||||
|
this.checkPendingTokens()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.params import Depends
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
@ -18,7 +17,7 @@ templates = Jinja2Templates(directory="templates")
|
||||||
@cashu_ext.get("/", response_class=HTMLResponse)
|
@cashu_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(
|
async def index(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists), # type: ignore
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
return cashu_renderer().TemplateResponse(
|
return cashu_renderer().TemplateResponse(
|
||||||
"cashu/index.html", {"request": request, "user": user.dict()}
|
"cashu/index.html", {"request": request, "user": user.dict()}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import json
|
|
||||||
import math
|
import math
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
# -------- cashu imports
|
# -------- cashu imports
|
||||||
from cashu.core.base import (
|
from cashu.core.base import (
|
||||||
BlindedSignature,
|
BlindedSignature,
|
||||||
|
|
@ -17,14 +14,10 @@ from cashu.core.base import (
|
||||||
MeltRequest,
|
MeltRequest,
|
||||||
MintRequest,
|
MintRequest,
|
||||||
PostSplitResponse,
|
PostSplitResponse,
|
||||||
Proof,
|
|
||||||
SplitRequest,
|
SplitRequest,
|
||||||
)
|
)
|
||||||
from fastapi import Query
|
from fastapi import Depends, Query
|
||||||
from fastapi.params import Depends
|
|
||||||
from lnurl import decode as decode_lnurl
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from secp256k1 import PublicKey
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
|
|
@ -35,7 +28,6 @@ from lnbits.core.services import (
|
||||||
fee_reserve,
|
fee_reserve,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
)
|
)
|
||||||
from lnbits.core.views.api import api_payment
|
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
from lnbits.wallets.base import PaymentStatus
|
from lnbits.wallets.base import PaymentStatus
|
||||||
|
|
@ -46,9 +38,16 @@ from .models import Cashu
|
||||||
|
|
||||||
# --------- extension imports
|
# --------- extension imports
|
||||||
|
|
||||||
|
# WARNING: Do not set this to False in production! This will create
|
||||||
|
# tokens for free otherwise. This is for testing purposes only!
|
||||||
|
|
||||||
LIGHTNING = True
|
LIGHTNING = True
|
||||||
|
|
||||||
|
if not LIGHTNING:
|
||||||
|
logger.warning(
|
||||||
|
"Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
|
||||||
|
)
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
############### LNBITS MINTS ###########
|
############### LNBITS MINTS ###########
|
||||||
########################################
|
########################################
|
||||||
|
|
@ -56,7 +55,7 @@ LIGHTNING = True
|
||||||
|
|
||||||
@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
|
@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
|
||||||
async def api_cashus(
|
async def api_cashus(
|
||||||
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get all mints of this wallet.
|
Get all mints of this wallet.
|
||||||
|
|
@ -73,7 +72,7 @@ async def api_cashus(
|
||||||
@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
|
@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
|
||||||
async def api_cashu_create(
|
async def api_cashu_create(
|
||||||
data: Cashu,
|
data: Cashu,
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new mint for this wallet.
|
Create a new mint for this wallet.
|
||||||
|
|
@ -91,7 +90,7 @@ async def api_cashu_create(
|
||||||
|
|
||||||
@cashu_ext.delete("/api/v1/mints/{cashu_id}")
|
@cashu_ext.delete("/api/v1/mints/{cashu_id}")
|
||||||
async def api_cashu_delete(
|
async def api_cashu_delete(
|
||||||
cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # type: ignore
|
cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Delete an existing cashu mint.
|
Delete an existing cashu mint.
|
||||||
|
|
@ -130,6 +129,28 @@ async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
|
||||||
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}")
|
||||||
|
async def keyset_keys(
|
||||||
|
cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None)
|
||||||
|
) -> dict[int, str]:
|
||||||
|
"""
|
||||||
|
Get the public keys of the mint of a specificy keyset id.
|
||||||
|
The id is encoded in base64_urlsafe and needs to be converted back to
|
||||||
|
normal base64 before it can be processed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||||
|
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||||
|
keyset = ledger.get_keyset(keyset_id=id)
|
||||||
|
return keyset
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
||||||
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
||||||
"""Get the public keys of the mint"""
|
"""Get the public keys of the mint"""
|
||||||
|
|
@ -182,7 +203,7 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||||
async def mint_coins(
|
async def mint(
|
||||||
data: MintRequest,
|
data: MintRequest,
|
||||||
cashu_id: str = Query(None),
|
cashu_id: str = Query(None),
|
||||||
payment_hash: str = Query(None),
|
payment_hash: str = Query(None),
|
||||||
|
|
@ -197,6 +218,8 @@ async def mint_coins(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||||
|
|
||||||
if LIGHTNING:
|
if LIGHTNING:
|
||||||
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||||
db=ledger.db, hash=payment_hash
|
db=ledger.db, hash=payment_hash
|
||||||
|
|
@ -206,42 +229,55 @@ async def mint_coins(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
detail="Mint does not know this invoice.",
|
detail="Mint does not know this invoice.",
|
||||||
)
|
)
|
||||||
if invoice.issued == True:
|
if invoice.issued:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
detail="Tokens already issued for this invoice.",
|
detail="Tokens already issued for this invoice.",
|
||||||
)
|
)
|
||||||
|
|
||||||
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
# set this invoice as issued
|
||||||
if total_requested > invoice.amount:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
|
||||||
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
|
||||||
)
|
|
||||||
|
|
||||||
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
|
||||||
|
|
||||||
if LIGHTNING and status.paid != True:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
|
||||||
|
|
||||||
promises = await ledger._generate_promises(
|
|
||||||
B_s=data.blinded_messages, keyset=keyset
|
|
||||||
)
|
|
||||||
assert len(promises), HTTPException(
|
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="No promises returned."
|
|
||||||
)
|
|
||||||
await ledger.crud.update_lightning_invoice(
|
await ledger.crud.update_lightning_invoice(
|
||||||
db=ledger.db, hash=payment_hash, issued=True
|
db=ledger.db, hash=payment_hash, issued=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
status: PaymentStatus = await check_transaction_status(
|
||||||
|
cashu.wallet, payment_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
||||||
|
if total_requested > invoice.amount:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
|
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not status.paid:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||||
|
)
|
||||||
|
|
||||||
|
promises = await ledger._generate_promises(
|
||||||
|
B_s=data.blinded_messages, keyset=keyset
|
||||||
|
)
|
||||||
|
return promises
|
||||||
|
except (Exception, HTTPException) as e:
|
||||||
|
logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
|
||||||
|
# unset issued flag because something went wrong
|
||||||
|
await ledger.crud.update_lightning_invoice(
|
||||||
|
db=ledger.db, hash=payment_hash, issued=False
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=getattr(e, "status_code")
|
||||||
|
or HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail=str(e) or getattr(e, "detail"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# only used for testing when LIGHTNING=false
|
||||||
|
promises = await ledger._generate_promises(
|
||||||
|
B_s=data.blinded_messages, keyset=keyset
|
||||||
|
)
|
||||||
return promises
|
return promises
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||||
|
|
@ -285,28 +321,38 @@ async def melt_coins(
|
||||||
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
||||||
)
|
)
|
||||||
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
||||||
await pay_invoice(
|
try:
|
||||||
wallet_id=cashu.wallet,
|
await pay_invoice(
|
||||||
payment_request=invoice,
|
wallet_id=cashu.wallet,
|
||||||
description=f"Pay cashu invoice",
|
payment_request=invoice,
|
||||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
description=f"Pay cashu invoice",
|
||||||
)
|
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||||
|
)
|
||||||
logger.debug(
|
except Exception as e:
|
||||||
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
|
||||||
)
|
raise e
|
||||||
status: PaymentStatus = await check_transaction_status(
|
finally:
|
||||||
cashu.wallet, invoice_obj.payment_hash
|
logger.debug(
|
||||||
)
|
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
||||||
if status.paid == True:
|
)
|
||||||
logger.debug("Cashu: Payment successful, invalidating proofs")
|
status: PaymentStatus = await check_transaction_status(
|
||||||
await ledger._invalidate_proofs(proofs)
|
cashu.wallet, invoice_obj.payment_hash
|
||||||
|
)
|
||||||
|
if status.paid == True:
|
||||||
|
logger.debug(
|
||||||
|
f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
|
||||||
|
)
|
||||||
|
await ledger._invalidate_proofs(proofs)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=f"Cashu: {str(e)}",
|
detail=f"Cashu: {str(e)}",
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}")
|
||||||
# delete proofs from pending list
|
# delete proofs from pending list
|
||||||
await ledger._unset_proofs_pending(proofs)
|
await ledger._unset_proofs_pending(proofs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "Streamer Copilot",
|
"name": "Streamer Copilot",
|
||||||
"short_description": "Video tips/animations/webhooks",
|
"short_description": "Video tips/animations/webhooks",
|
||||||
"icon": "face",
|
"tile": "/copilot/static/bitcoin-streaming.png",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"arcbtc"
|
"arcbtc"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ async def lnurl_callback(
|
||||||
memo=cp.lnurl_title,
|
memo=cp.lnurl_title,
|
||||||
unhashed_description=(
|
unhashed_description=(
|
||||||
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
|
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
|
||||||
).encode("utf-8"),
|
).encode(),
|
||||||
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},
|
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},
|
||||||
)
|
)
|
||||||
payResponse = {"pr": payment_request, "routes": []}
|
payResponse = {"pr": payment_request, "routes": []}
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/copilot/static/bitcoin-streaming.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -24,12 +24,12 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
webhook = None
|
if payment.extra.get("tag") != "copilot":
|
||||||
data = None
|
|
||||||
if not payment.extra or payment.extra.get("tag") != "copilot":
|
|
||||||
# not an copilot invoice
|
# not an copilot invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
|
webhook = None
|
||||||
|
data = None
|
||||||
copilot = await get_copilot(payment.extra.get("copilotid", -1))
|
copilot = await get_copilot(payment.extra.get("copilotid", -1))
|
||||||
|
|
||||||
if not copilot:
|
if not copilot:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
||||||
animation<br />
|
animation<br />
|
||||||
<small>
|
<small>
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
Created by,
|
||||||
|
<a class="text-secondary" href="https://github.com/benarc"
|
||||||
|
>Ben Arc</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
>
|
>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="copilot.lnurl"
|
:value="'lightning:' + copilot.lnurl"
|
||||||
:options="{width:250}"
|
:options="{width:250}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import Request, WebSocket, WebSocketDisconnect
|
from fastapi import Depends, Request
|
||||||
from fastapi.params import Depends
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.responses import HTMLResponse # type: ignore
|
from starlette.responses import HTMLResponse # type: ignore
|
||||||
|
|
||||||
|
|
@ -9,15 +8,12 @@ from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
from . import copilot_ext, copilot_renderer
|
from . import copilot_ext, copilot_renderer
|
||||||
from .crud import get_copilot
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@copilot_ext.get("/", response_class=HTMLResponse)
|
@copilot_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
|
||||||
):
|
|
||||||
return copilot_renderer().TemplateResponse(
|
return copilot_renderer().TemplateResponse(
|
||||||
"copilot/index.html", {"request": request, "user": user.dict()}
|
"copilot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Depends, Query, Request
|
||||||
from fastapi.param_functions import Query
|
|
||||||
from fastapi.params import Depends
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.services import websocketUpdater
|
from lnbits.core.services import websocketUpdater
|
||||||
|
|
@ -22,9 +20,7 @@ from .models import CreateCopilotData
|
||||||
|
|
||||||
|
|
||||||
@copilot_ext.get("/api/v1/copilot")
|
@copilot_ext.get("/api/v1/copilot")
|
||||||
async def api_copilots_retrieve(
|
async def api_copilots_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
|
||||||
):
|
|
||||||
wallet_user = wallet.wallet.user
|
wallet_user = wallet.wallet.user
|
||||||
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
||||||
try:
|
try:
|
||||||
|
|
@ -37,7 +33,7 @@ async def api_copilots_retrieve(
|
||||||
async def api_copilot_retrieve(
|
async def api_copilot_retrieve(
|
||||||
req: Request,
|
req: Request,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
if not copilot:
|
if not copilot:
|
||||||
|
|
@ -54,7 +50,7 @@ async def api_copilot_retrieve(
|
||||||
async def api_copilot_create_or_update(
|
async def api_copilot_create_or_update(
|
||||||
data: CreateCopilotData,
|
data: CreateCopilotData,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
data.user = wallet.wallet.user
|
data.user = wallet.wallet.user
|
||||||
data.wallet = wallet.wallet.id
|
data.wallet = wallet.wallet.id
|
||||||
|
|
@ -68,7 +64,7 @@ async def api_copilot_create_or_update(
|
||||||
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
||||||
async def api_copilot_delete(
|
async def api_copilot_delete(
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Discord Bot",
|
"name": "Discord Bot",
|
||||||
"short_description": "Generate users and wallets",
|
"short_description": "Generate users and wallets",
|
||||||
"icon": "person_add",
|
"tile": "/discordbot/static/image/discordbot.png",
|
||||||
"contributors": ["bitcoingamer21"]
|
"contributors": ["bitcoingamer21"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/discordbot/static/image/discordbot.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -11,18 +11,24 @@
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
Connect your LNbits instance to a
|
Connect your LNbits instance to a
|
||||||
<a href="https://github.com/chrislennon/lnbits-discord-bot"
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/chrislennon/lnbits-discord-bot"
|
||||||
>Discord Bot</a
|
>Discord Bot</a
|
||||||
>
|
>
|
||||||
leveraging LNbits as a community based lightning node.<br />
|
leveraging LNbits as a community based lightning node.<br />
|
||||||
<small>
|
<small>
|
||||||
Created by,
|
Created by,
|
||||||
<a href="https://github.com/chrislennon">Chris Lennon</a></small
|
<a class="text-secondary" href="https://github.com/chrislennon"
|
||||||
|
>Chris Lennon</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>
|
||||||
Based on User Manager, by
|
Based on User Manager, by
|
||||||
<a href="https://github.com/benarc">Ben Arc</a></small
|
<a class="text-secondary" href="https://github.com/benarc"
|
||||||
|
>Ben Arc</a
|
||||||
|
></small
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
This extension is designed to be used through its API by a Discord Bot,
|
This extension is designed to be used through its API by a Discord Bot,
|
||||||
currently you have to install the bot
|
currently you have to install the bot
|
||||||
<a
|
<a
|
||||||
|
class="text-secondary"
|
||||||
href="https://github.com/chrislennon/lnbits-discord-bot/#installation"
|
href="https://github.com/chrislennon/lnbits-discord-bot/#installation"
|
||||||
>yourself</a
|
>yourself</a
|
||||||
><br />
|
><br />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from fastapi import Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.params import Depends
|
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
|
|
@ -9,9 +8,7 @@ from . import discordbot_ext, discordbot_renderer
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/", response_class=HTMLResponse)
|
@discordbot_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
|
||||||
):
|
|
||||||
return discordbot_renderer().TemplateResponse(
|
return discordbot_renderer().TemplateResponse(
|
||||||
"discordbot/index.html", {"request": request, "user": user.dict()}
|
"discordbot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Depends, Query
|
||||||
from fastapi.params import Depends
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core import update_user_extension
|
from lnbits.core import update_user_extension
|
||||||
|
|
@ -28,16 +27,14 @@ from .models import CreateUserData, CreateUserWallet
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_users(
|
async def api_discordbot_users(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
):
|
):
|
||||||
user_id = wallet.wallet.user
|
user_id = wallet.wallet.user
|
||||||
return [user.dict() for user in await get_discordbot_users(user_id)]
|
return [user.dict() for user in await get_discordbot_users(user_id)]
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_user(
|
async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
|
||||||
):
|
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
if user:
|
if user:
|
||||||
return user.dict()
|
return user.dict()
|
||||||
|
|
@ -45,7 +42,7 @@ async def api_discordbot_user(
|
||||||
|
|
||||||
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
||||||
async def api_discordbot_users_create(
|
async def api_discordbot_users_create(
|
||||||
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
user = await create_discordbot_user(data)
|
user = await create_discordbot_user(data)
|
||||||
full = user.dict()
|
full = user.dict()
|
||||||
|
|
@ -57,7 +54,7 @@ async def api_discordbot_users_create(
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
||||||
async def api_discordbot_users_delete(
|
async def api_discordbot_users_delete(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
|
@ -89,7 +86,7 @@ async def api_discordbot_activate_extension(
|
||||||
|
|
||||||
@discordbot_ext.post("/api/v1/wallets")
|
@discordbot_ext.post("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets_create(
|
async def api_discordbot_wallets_create(
|
||||||
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
user = await create_discordbot_wallet(
|
user = await create_discordbot_wallet(
|
||||||
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
||||||
|
|
@ -99,7 +96,7 @@ async def api_discordbot_wallets_create(
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets")
|
@discordbot_ext.get("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets(
|
async def api_discordbot_wallets(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
):
|
):
|
||||||
admin_id = wallet.wallet.user
|
admin_id = wallet.wallet.user
|
||||||
return await get_discordbot_wallets(admin_id)
|
return await get_discordbot_wallets(admin_id)
|
||||||
|
|
@ -107,21 +104,21 @@ async def api_discordbot_wallets(
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
||||||
async def api_discordbot_wallet_transactions(
|
async def api_discordbot_wallet_transactions(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
return await get_discordbot_wallet_transactions(wallet_id)
|
return await get_discordbot_wallet_transactions(wallet_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
||||||
async def api_discordbot_users_wallets(
|
async def api_discordbot_users_wallets(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
return await get_discordbot_users_wallets(user_id)
|
return await get_discordbot_users_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
||||||
async def api_discordbot_wallets_delete(
|
async def api_discordbot_wallets_delete(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
):
|
):
|
||||||
get_wallet = await get_discordbot_wallet(wallet_id)
|
get_wallet = await get_discordbot_wallet(wallet_id)
|
||||||
if not get_wallet:
|
if not get_wallet:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
|
|
@ -11,6 +12,14 @@ db = Database("ext_events")
|
||||||
|
|
||||||
events_ext: APIRouter = APIRouter(prefix="/events", tags=["Events"])
|
events_ext: APIRouter = APIRouter(prefix="/events", tags=["Events"])
|
||||||
|
|
||||||
|
events_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/events/static",
|
||||||
|
"app": StaticFiles(packages=[("lnbits", "extensions/events/static")]),
|
||||||
|
"name": "events_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def events_renderer():
|
def events_renderer():
|
||||||
return template_renderer(["lnbits/extensions/events/templates"])
|
return template_renderer(["lnbits/extensions/events/templates"])
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Events",
|
"name": "Events",
|
||||||
"short_description": "Sell and register event tickets",
|
"short_description": "Sell and register event tickets",
|
||||||
"icon": "local_activity",
|
"tile": "/events/static/image/events.png",
|
||||||
"contributors": ["benarc"]
|
"contributors": ["benarc"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/events/static/image/events.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
|
|
@ -1,15 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
from http import HTTPStatus
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
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.extensions.events.models import CreateTicket
|
from lnbits.extensions.events.models import CreateTicket
|
||||||
from lnbits.helpers import get_current_extension_name
|
from lnbits.helpers import get_current_extension_name
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
@ -29,11 +20,17 @@ async def wait_for_paid_invoices():
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
# (avoid loops)
|
# (avoid loops)
|
||||||
if (
|
if (
|
||||||
"events" == payment.extra.get("tag")
|
payment.extra
|
||||||
|
and "events" == payment.extra.get("tag")
|
||||||
and payment.extra.get("name")
|
and payment.extra.get("name")
|
||||||
and payment.extra.get("email")
|
and payment.extra.get("email")
|
||||||
):
|
):
|
||||||
CreateTicket.name = str(payment.extra.get("name"))
|
await api_ticket_send_ticket(
|
||||||
CreateTicket.email = str(payment.extra.get("email"))
|
payment.memo,
|
||||||
await api_ticket_send_ticket(payment.memo, payment.payment_hash, CreateTicket)
|
payment.payment_hash,
|
||||||
|
CreateTicket(
|
||||||
|
name=str(payment.extra.get("name")),
|
||||||
|
email=str(payment.extra.get("email")),
|
||||||
|
),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
Events comes with a shareable ticket scanner, which can be used to
|
Events comes with a shareable ticket scanner, which can be used to
|
||||||
register attendees.<br />
|
register attendees.<br />
|
||||||
<small>
|
<small>
|
||||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>
|
Created by,
|
||||||
|
<a class="text-secondary" href="https://github.com/benarc">Ben Arc</a>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + receive.paymentReq">
|
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="paymentReq"
|
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||||
:options="{width: 340}"
|
:options="{width: 340}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.params import Depends
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi import Depends, Query
|
||||||
from fastapi.params import Depends
|
|
||||||
from loguru import logger
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.core.services import create_invoice
|
from lnbits.core.services import create_invoice
|
||||||
|
|
@ -38,7 +35,8 @@ async def api_events(
|
||||||
wallet_ids = [wallet.wallet.id]
|
wallet_ids = [wallet.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
user = await get_user(wallet.wallet.user)
|
||||||
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
return [event.dict() for event in await get_events(wallet_ids)]
|
return [event.dict() for event in await get_events(wallet_ids)]
|
||||||
|
|
||||||
|
|
@ -92,7 +90,8 @@ async def api_tickets(
|
||||||
wallet_ids = [wallet.wallet.id]
|
wallet_ids = [wallet.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
user = await get_user(wallet.wallet.user)
|
||||||
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
|
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
|
||||||
|
|
||||||
|
|
@ -119,26 +118,32 @@ async def api_ticket_make_ticket(event_id, name, email):
|
||||||
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
|
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
|
||||||
async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
|
async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
|
||||||
event = await get_event(event_id)
|
event = await get_event(event_id)
|
||||||
try:
|
if not event:
|
||||||
status = await api_payment(payment_hash)
|
raise HTTPException(
|
||||||
if status["paid"]:
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
ticket = await create_ticket(
|
detail=f"Event could not be fetched.",
|
||||||
payment_hash=payment_hash,
|
)
|
||||||
wallet=event.wallet,
|
|
||||||
event=event_id,
|
status = await api_payment(payment_hash)
|
||||||
name=data.name,
|
if status["paid"]:
|
||||||
email=data.email,
|
|
||||||
|
exists = await get_ticket(payment_hash)
|
||||||
|
if exists:
|
||||||
|
return {"paid": True, "ticket_id": exists.id}
|
||||||
|
|
||||||
|
ticket = await create_ticket(
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
wallet=event.wallet,
|
||||||
|
event=event_id,
|
||||||
|
name=data.name,
|
||||||
|
email=data.email,
|
||||||
|
)
|
||||||
|
if not ticket:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail=f"Event could not be fetched.",
|
||||||
)
|
)
|
||||||
|
return {"paid": True, "ticket_id": ticket.id}
|
||||||
if not ticket:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail=f"Event could not be fetched.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"paid": True, "ticket_id": ticket.id}
|
|
||||||
except Exception:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid")
|
|
||||||
return {"paid": False}
|
return {"paid": False}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Build your own!!",
|
"name": "Build your own!!",
|
||||||
"short_description": "Join us, make an extension",
|
"short_description": "Join us, make an extension",
|
||||||
"icon": "info",
|
"tile": "/cashu/static/image/tile.png",
|
||||||
"contributors": ["github_username"]
|
"contributors": ["github_username"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.params import Depends
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
|
@ -14,7 +13,7 @@ templates = Jinja2Templates(directory="templates")
|
||||||
@example_ext.get("/", response_class=HTMLResponse)
|
@example_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(
|
async def index(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists), # type: ignore
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
return example_renderer().TemplateResponse(
|
return example_renderer().TemplateResponse(
|
||||||
"example/index.html", {"request": request, "user": user.dict()}
|
"example/index.html", {"request": request, "user": user.dict()}
|
||||||
|
|
|
||||||
13
lnbits/extensions/gerty/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Gerty
|
||||||
|
|
||||||
|
## Your desktop bitcoin assistant
|
||||||
|
|
||||||
|
Buy here `<link>`
|
||||||
|
|
||||||
|
blah blah blah
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Enable extension
|
||||||
|
2. Fill out form
|
||||||
|
3. point gerty at the server and give it the Gerty ID
|
||||||
28
lnbits/extensions/gerty/__init__.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from lnbits.db import Database
|
||||||
|
from lnbits.helpers import template_renderer
|
||||||
|
|
||||||
|
db = Database("ext_gerty")
|
||||||
|
|
||||||
|
gerty_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/gerty/static",
|
||||||
|
"app": StaticFiles(packages=[("lnbits", "extensions/gerty/static")]),
|
||||||
|
"name": "gerty_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
gerty_ext: APIRouter = APIRouter(prefix="/gerty", tags=["Gerty"])
|
||||||
|
|
||||||
|
|
||||||
|
def gerty_renderer():
|
||||||
|
return template_renderer(["lnbits/extensions/gerty/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .views import * # noqa
|
||||||
|
from .views_api import * # noqa
|
||||||
6
lnbits/extensions/gerty/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Gerty",
|
||||||
|
"short_description": "Desktop bitcoin Assistant",
|
||||||
|
"tile": "/gerty/static/gerty.png",
|
||||||
|
"contributors": ["arcbtc", "blackcoffeebtc"]
|
||||||
|
}
|
||||||
138
lnbits/extensions/gerty/crud.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import Gerty, Mempool, MempoolEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
|
||||||
|
gerty_id = urlsafe_short_hash()
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO gerty.gertys (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
utc_offset,
|
||||||
|
type,
|
||||||
|
wallet,
|
||||||
|
lnbits_wallets,
|
||||||
|
mempool_endpoint,
|
||||||
|
exchange,
|
||||||
|
display_preferences,
|
||||||
|
refresh_time,
|
||||||
|
urls
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
gerty_id,
|
||||||
|
data.name,
|
||||||
|
data.utc_offset,
|
||||||
|
data.type,
|
||||||
|
wallet_id,
|
||||||
|
data.lnbits_wallets,
|
||||||
|
data.mempool_endpoint,
|
||||||
|
data.exchange,
|
||||||
|
data.display_preferences,
|
||||||
|
data.refresh_time,
|
||||||
|
data.urls,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
assert gerty, "Newly created gerty couldn't be retrieved"
|
||||||
|
return gerty
|
||||||
|
|
||||||
|
|
||||||
|
async def update_gerty(gerty_id: str, **kwargs) -> Optional[Gerty]:
|
||||||
|
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||||
|
await db.execute(
|
||||||
|
f"UPDATE gerty.gertys SET {q} WHERE id = ?", (*kwargs.values(), gerty_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await get_gerty(gerty_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_gerty(gerty_id: str) -> Optional[Gerty]:
|
||||||
|
row = await db.fetchone("SELECT * FROM gerty.gertys WHERE id = ?", (gerty_id,))
|
||||||
|
return Gerty(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_gertys(wallet_ids: Union[str, List[str]]) -> List[Gerty]:
|
||||||
|
if isinstance(wallet_ids, str):
|
||||||
|
wallet_ids = [wallet_ids]
|
||||||
|
|
||||||
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"SELECT * FROM gerty.gertys WHERE wallet IN ({q})", (*wallet_ids,)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [Gerty(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_gerty(gerty_id: str) -> None:
|
||||||
|
await db.execute("DELETE FROM gerty.gertys WHERE id = ?", (gerty_id,))
|
||||||
|
|
||||||
|
|
||||||
|
#############MEMPOOL###########
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mempool_info(endPoint: str, gerty) -> dict:
|
||||||
|
logger.debug(endPoint)
|
||||||
|
endpoints = MempoolEndpoint()
|
||||||
|
url = ""
|
||||||
|
for endpoint in endpoints:
|
||||||
|
if endPoint == endpoint[0]:
|
||||||
|
url = endpoint[1]
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT * FROM gerty.mempool WHERE endpoint = ? AND mempool_endpoint = ?",
|
||||||
|
(
|
||||||
|
endPoint,
|
||||||
|
gerty.mempool_endpoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(gerty.mempool_endpoint + url)
|
||||||
|
logger.debug(gerty.mempool_endpoint + url)
|
||||||
|
mempool_id = urlsafe_short_hash()
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO gerty.mempool (
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
endpoint,
|
||||||
|
time,
|
||||||
|
mempool_endpoint
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
mempool_id,
|
||||||
|
json.dumps(response.json()),
|
||||||
|
endPoint,
|
||||||
|
time.time(),
|
||||||
|
gerty.mempool_endpoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
if int(time.time()) - row.time > 20:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(gerty.mempool_endpoint + url)
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE gerty.mempool SET data = ?, time = ? WHERE endpoint = ? AND mempool_endpoint = ?",
|
||||||
|
(
|
||||||
|
json.dumps(response.json()),
|
||||||
|
time.time(),
|
||||||
|
endPoint,
|
||||||
|
gerty.mempool_endpoint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
return json.loads(row.data)
|
||||||
946
lnbits/extensions/gerty/helpers.py
Normal file
|
|
@ -0,0 +1,946 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import textwrap
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_wallet_for_key
|
||||||
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
|
||||||
|
|
||||||
|
from .crud import get_mempool_info
|
||||||
|
from .number_prefixer import *
|
||||||
|
|
||||||
|
|
||||||
|
def get_percent_difference(current, previous, precision=3):
|
||||||
|
difference = (current - previous) / current * 100
|
||||||
|
return "{0}{1}%".format("+" if difference > 0 else "", round(difference, precision))
|
||||||
|
|
||||||
|
|
||||||
|
# A helper function get a nicely formated dict for the text
|
||||||
|
def get_text_item_dict(
|
||||||
|
text: str,
|
||||||
|
font_size: int,
|
||||||
|
x_pos: int = -1,
|
||||||
|
y_pos: int = -1,
|
||||||
|
gerty_type: str = "Gerty",
|
||||||
|
):
|
||||||
|
# Get line size by font size
|
||||||
|
line_width = 20
|
||||||
|
if font_size <= 12:
|
||||||
|
line_width = 60
|
||||||
|
elif font_size <= 15:
|
||||||
|
line_width = 45
|
||||||
|
elif font_size <= 20:
|
||||||
|
line_width = 35
|
||||||
|
elif font_size <= 40:
|
||||||
|
line_width = 25
|
||||||
|
|
||||||
|
# Get font sizes for Gerty mini
|
||||||
|
if gerty_type.lower() == "mini gerty":
|
||||||
|
if font_size <= 12:
|
||||||
|
font_size = 1
|
||||||
|
if font_size <= 15:
|
||||||
|
font_size = 1
|
||||||
|
elif font_size <= 20:
|
||||||
|
font_size = 2
|
||||||
|
elif font_size <= 40:
|
||||||
|
font_size = 2
|
||||||
|
else:
|
||||||
|
font_size = 5
|
||||||
|
|
||||||
|
# wrap the text
|
||||||
|
wrapper = textwrap.TextWrapper(width=line_width)
|
||||||
|
word_list = wrapper.wrap(text=text)
|
||||||
|
# logger.debug("number of chars = {0}".format(len(text)))
|
||||||
|
|
||||||
|
multilineText = "\n".join(word_list)
|
||||||
|
# logger.debug("number of lines = {0}".format(len(word_list)))
|
||||||
|
|
||||||
|
# logger.debug('multilineText')
|
||||||
|
# logger.debug(multilineText)
|
||||||
|
|
||||||
|
data_text = {"value": multilineText, "size": font_size}
|
||||||
|
if x_pos == -1 and y_pos == -1:
|
||||||
|
data_text["position"] = "center"
|
||||||
|
else:
|
||||||
|
data_text["x"] = x_pos if x_pos > 0 else 0
|
||||||
|
data_text["y"] = y_pos if x_pos > 0 else 0
|
||||||
|
return data_text
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_suffix(dayNumber):
|
||||||
|
if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
|
||||||
|
return "th"
|
||||||
|
else:
|
||||||
|
return ["st", "nd", "rd"][dayNumber % 10 - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_remaining(seconds, granularity=2):
|
||||||
|
intervals = (
|
||||||
|
# ('weeks', 604800), # 60 * 60 * 24 * 7
|
||||||
|
("days", 86400), # 60 * 60 * 24
|
||||||
|
("hours", 3600), # 60 * 60
|
||||||
|
("minutes", 60),
|
||||||
|
("seconds", 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for name, count in intervals:
|
||||||
|
value = seconds // count
|
||||||
|
if value:
|
||||||
|
seconds -= value * count
|
||||||
|
if value == 1:
|
||||||
|
name = name.rstrip("s")
|
||||||
|
result.append("{} {}".format(round(value), name))
|
||||||
|
return ", ".join(result[:granularity])
|
||||||
|
|
||||||
|
|
||||||
|
# format a number for nice display output
|
||||||
|
def format_number(number, precision=None):
|
||||||
|
return "{:,}".format(round(number, precision))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mining_dashboard(gerty):
|
||||||
|
areas = []
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
# current hashrate
|
||||||
|
r = await get_mempool_info("hashrate_1w", gerty)
|
||||||
|
data = r
|
||||||
|
hashrateNow = data["currentHashrate"]
|
||||||
|
hashrateOneWeekAgo = data["hashrates"][6]["avgHashrate"]
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current mining hashrate", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}hash".format(si_format(hashrateNow, 6, True, " ")),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} vs 7 days ago".format(
|
||||||
|
get_percent_difference(hashrateNow, hashrateOneWeekAgo, 3)
|
||||||
|
),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
|
||||||
|
# timeAvg
|
||||||
|
text = []
|
||||||
|
progress = "{0}%".format(round(r["progressPercent"], 2))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Progress through current epoch",
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=progress, font_size=60, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficulty adjustment
|
||||||
|
text = []
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Time to next difficulty adjustment",
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=get_time_remaining(stat / 1000, 3),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficultyChange
|
||||||
|
text = []
|
||||||
|
difficultyChange = round(r["difficultyChange"], 2)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Estimated difficulty change",
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}{1}%".format(
|
||||||
|
"+" if difficultyChange > 0 else "", round(difficultyChange, 2)
|
||||||
|
),
|
||||||
|
font_size=60,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
r = await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
data = r
|
||||||
|
stat = {}
|
||||||
|
stat["current"] = data["currentDifficulty"]
|
||||||
|
stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2]["difficulty"]
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lightning_stats(gerty):
|
||||||
|
data = await get_mempool_info("statistics", gerty)
|
||||||
|
areas = []
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text="Channel Count", font_size=12, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(data["latest"]["channel_count"]),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["channel_count"],
|
||||||
|
previous=data["previous"]["channel_count"],
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text="Number of Nodes", font_size=12, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(data["latest"]["node_count"]),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["node_count"], previous=data["previous"]["node_count"]
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text="Total Capacity", font_size=12, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
avg_capacity = float(data["latest"]["total_capacity"]) / float(100000000)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} BTC".format(format_number(avg_capacity, 2)),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["total_capacity"],
|
||||||
|
previous=data["previous"]["total_capacity"],
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Average Channel Capacity", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} sats".format(format_number(data["latest"]["avg_capacity"])),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["avg_capacity"],
|
||||||
|
previous=data["previous"]["avg_capacity"],
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_update_time(sleep_time_seconds: int = 0, utc_offset: int = 0):
|
||||||
|
utc_now = datetime.utcnow()
|
||||||
|
next_refresh_time = utc_now + timedelta(0, sleep_time_seconds)
|
||||||
|
local_refresh_time = next_refresh_time + timedelta(hours=utc_offset)
|
||||||
|
return "{0} {1}".format(
|
||||||
|
"I'll wake up at" if gerty_should_sleep(utc_offset) else "Next update at",
|
||||||
|
local_refresh_time.strftime("%H:%M on %e %b %Y"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gerty_should_sleep(utc_offset: int = 0):
|
||||||
|
utc_now = datetime.utcnow()
|
||||||
|
local_time = utc_now + timedelta(hours=utc_offset)
|
||||||
|
hours = int(local_time.strftime("%H"))
|
||||||
|
if hours >= 22 and hours <= 23:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mining_stat(stat_slug: str, gerty):
|
||||||
|
text = []
|
||||||
|
if stat_slug == "mining_current_hash_rate":
|
||||||
|
stat = await api_get_mining_stat(stat_slug, gerty)
|
||||||
|
current = "{0}hash".format(si_format(stat["current"], 6, True, " "))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current Mining Hashrate", font_size=20, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=current, font_size=40, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
# compare vs previous time period
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=stat["current"], previous=stat["1w"]
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} in last 7 days".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "mining_current_difficulty":
|
||||||
|
stat = await api_get_mining_stat(stat_slug, gerty)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current Mining Difficulty", font_size=20, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(stat["current"]), font_size=40, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=stat["current"], previous=stat["previous"]
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} since last adjustment".format(difference),
|
||||||
|
font_size=12,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# text.append(get_text_item_dict("Required threshold for mining proof-of-work", 12))
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def api_get_mining_stat(stat_slug: str, gerty):
|
||||||
|
stat = {}
|
||||||
|
if stat_slug == "mining_current_hash_rate":
|
||||||
|
r = await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
data = r
|
||||||
|
stat["current"] = data["currentHashrate"]
|
||||||
|
stat["1w"] = data["hashrates"][len(data["hashrates"]) - 7]["avgHashrate"]
|
||||||
|
elif stat_slug == "mining_current_difficulty":
|
||||||
|
r = await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
data = r
|
||||||
|
stat["current"] = data["currentDifficulty"]
|
||||||
|
stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2]["difficulty"]
|
||||||
|
return stat
|
||||||
|
|
||||||
|
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
|
||||||
|
async def get_satoshi():
|
||||||
|
maxQuoteLength = 186
|
||||||
|
with open(
|
||||||
|
os.path.join(settings.lnbits_path, "extensions/gerty/static/satoshi.json")
|
||||||
|
) as fd:
|
||||||
|
satoshiQuotes = json.load(fd)
|
||||||
|
quote = satoshiQuotes[random.randint(0, len(satoshiQuotes) - 1)]
|
||||||
|
# logger.debug(quote.text)
|
||||||
|
if len(quote["text"]) > maxQuoteLength:
|
||||||
|
logger.trace("Quote is too long, getting another")
|
||||||
|
return await get_satoshi()
|
||||||
|
else:
|
||||||
|
return quote
|
||||||
|
|
||||||
|
|
||||||
|
# Get a screen slug by its position in the screens_list
|
||||||
|
def get_screen_slug_by_index(index: int, screens_list):
|
||||||
|
if index <= len(screens_list) - 1:
|
||||||
|
return list(screens_list)[index - 1]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Get a list of text items for the screen number
|
||||||
|
async def get_screen_data(screen_num: int, screens_list: list, gerty):
|
||||||
|
screen_slug = get_screen_slug_by_index(screen_num, screens_list)
|
||||||
|
# first get the relevant slug from the display_preferences
|
||||||
|
areas: List = []
|
||||||
|
title = ""
|
||||||
|
|
||||||
|
if screen_slug == "dashboard":
|
||||||
|
title = gerty.name
|
||||||
|
areas = await get_dashboard(gerty)
|
||||||
|
|
||||||
|
if screen_slug == "lnbits_wallets_balance":
|
||||||
|
wallets = await get_lnbits_wallet_balances(gerty)
|
||||||
|
|
||||||
|
for wallet in wallets:
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}'s Wallet".format(wallet["name"]),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} sats".format(format_number(wallet["balance"])),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
elif screen_slug == "url_checker":
|
||||||
|
for url in json.loads(gerty.urls):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
text = []
|
||||||
|
try:
|
||||||
|
response = await client.get(url)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=url,
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=str(response.status_code),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=url,
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=str("DOWN"),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
elif screen_slug == "fun_satoshi_quotes":
|
||||||
|
areas.append(await get_satoshi_quotes(gerty))
|
||||||
|
elif screen_slug == "fun_exchange_market_rate":
|
||||||
|
areas.append(await get_exchange_rate(gerty))
|
||||||
|
elif screen_slug == "onchain_difficulty_epoch_progress":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "onchain_block_height":
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(await get_mempool_info("tip_height", gerty)),
|
||||||
|
font_size=80,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
elif screen_slug == "onchain_difficulty_retarget_date":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "onchain_difficulty_blocks_remaining":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "onchain_difficulty_epoch_time_remaining":
|
||||||
|
areas.append(await get_onchain_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "dashboard_onchain":
|
||||||
|
title = "Onchain Data"
|
||||||
|
areas = await get_onchain_dashboard(gerty)
|
||||||
|
elif screen_slug == "mempool_recommended_fees":
|
||||||
|
areas.append(await get_mempool_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "mempool_tx_count":
|
||||||
|
areas.append(await get_mempool_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "mining_current_hash_rate":
|
||||||
|
areas.append(await get_mining_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "mining_current_difficulty":
|
||||||
|
areas.append(await get_mining_stat(screen_slug, gerty))
|
||||||
|
elif screen_slug == "dashboard_mining":
|
||||||
|
title = "Mining Data"
|
||||||
|
areas = await get_mining_dashboard(gerty)
|
||||||
|
elif screen_slug == "lightning_dashboard":
|
||||||
|
title = "Lightning Network"
|
||||||
|
areas = await get_lightning_stats(gerty)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"title": title,
|
||||||
|
"areas": areas,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# Get the dashboard screen
|
||||||
|
async def get_dashboard(gerty):
|
||||||
|
areas = []
|
||||||
|
# XC rate
|
||||||
|
text = []
|
||||||
|
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(amount), font_size=40, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="BTC{0} price".format(gerty.exchange),
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
# balance
|
||||||
|
text = []
|
||||||
|
wallets = await get_lnbits_wallet_balances(gerty)
|
||||||
|
text = []
|
||||||
|
for wallet in wallets:
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(wallet["name"]), font_size=15, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} sats".format(format_number(wallet["balance"])),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# Mempool fees
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=format_number(await get_mempool_info("tip_height", gerty)),
|
||||||
|
font_size=40,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current block height", font_size=15, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficulty adjustment time
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=await get_time_remaining_next_difficulty_adjustment(gerty) or "0",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="until next difficulty adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lnbits_wallet_balances(gerty):
|
||||||
|
# Get Wallet info
|
||||||
|
wallets = []
|
||||||
|
if gerty.lnbits_wallets != "":
|
||||||
|
for lnbits_wallet in json.loads(gerty.lnbits_wallets):
|
||||||
|
wallet = await get_wallet_for_key(key=lnbits_wallet)
|
||||||
|
if wallet:
|
||||||
|
wallets.append(
|
||||||
|
{
|
||||||
|
"name": wallet.name,
|
||||||
|
"balance": wallet.balance_msat / 1000,
|
||||||
|
"inkey": wallet.inkey,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return wallets
|
||||||
|
|
||||||
|
|
||||||
|
async def get_placeholder_text(gerty):
|
||||||
|
return [
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Some placeholder text",
|
||||||
|
x_pos=15,
|
||||||
|
y_pos=10,
|
||||||
|
font_size=50,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
),
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Some placeholder text",
|
||||||
|
x_pos=15,
|
||||||
|
y_pos=10,
|
||||||
|
font_size=50,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_satoshi_quotes(gerty):
|
||||||
|
# Get Satoshi quotes
|
||||||
|
text = []
|
||||||
|
quote = await get_satoshi()
|
||||||
|
if quote:
|
||||||
|
if quote["text"]:
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=quote["text"], font_size=15, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if quote["date"]:
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Satoshi Nakamoto - {0}".format(quote["date"]),
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# Get Exchange Value
|
||||||
|
async def get_exchange_rate(gerty):
|
||||||
|
text = []
|
||||||
|
if gerty.exchange != "":
|
||||||
|
try:
|
||||||
|
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
|
||||||
|
if amount:
|
||||||
|
price = format_number(amount)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Current {0}/BTC price".format(gerty.exchange),
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=price, font_size=80, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def get_onchain_stat(stat_slug: str, gerty):
|
||||||
|
text = []
|
||||||
|
if (
|
||||||
|
stat_slug == "onchain_difficulty_epoch_progress"
|
||||||
|
or stat_slug == "onchain_difficulty_retarget_date"
|
||||||
|
or stat_slug == "onchain_difficulty_blocks_remaining"
|
||||||
|
or stat_slug == "onchain_difficulty_epoch_time_remaining"
|
||||||
|
):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
if stat_slug == "onchain_difficulty_epoch_progress":
|
||||||
|
stat = round(r["progressPercent"])
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Progress through current difficulty epoch",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}%".format(stat), font_size=80, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "onchain_difficulty_retarget_date":
|
||||||
|
stat = r["estimatedRetargetDate"]
|
||||||
|
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Date of next difficulty adjustment",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=dt, font_size=40, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
elif stat_slug == "onchain_difficulty_blocks_remaining":
|
||||||
|
stat = r["remainingBlocks"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Blocks until next difficulty adjustment",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(format_number(stat)),
|
||||||
|
font_size=80,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "onchain_difficulty_epoch_time_remaining":
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Time until next difficulty adjustment",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=get_time_remaining(stat / 1000, 4),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def get_onchain_dashboard(gerty):
|
||||||
|
areas = []
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
text = []
|
||||||
|
stat = round(r["progressPercent"])
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Progress through epoch", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}%".format(stat), font_size=60, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
stat = r["estimatedRetargetDate"]
|
||||||
|
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Date of next adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(text=dt, font_size=20, gerty_type=gerty.type)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
stat = r["remainingBlocks"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Blocks until adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(format_number(stat)),
|
||||||
|
font_size=60,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Time until adjustment", font_size=12, gerty_type=gerty.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text=get_time_remaining(stat / 1000, 4),
|
||||||
|
font_size=20,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def get_time_remaining_next_difficulty_adjustment(gerty):
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
r = await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
stat = r["remainingTime"]
|
||||||
|
time = get_time_remaining(stat / 1000, 3)
|
||||||
|
return time
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mempool_stat(stat_slug: str, gerty):
|
||||||
|
text = []
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
if stat_slug == "mempool_tx_count":
|
||||||
|
r = await get_mempool_info("mempool", gerty)
|
||||||
|
if stat_slug == "mempool_tx_count":
|
||||||
|
stat = round(r["count"])
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="Transactions in the mempool",
|
||||||
|
font_size=15,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0}".format(format_number(stat)),
|
||||||
|
font_size=80,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif stat_slug == "mempool_recommended_fees":
|
||||||
|
y_offset = 60
|
||||||
|
fees = await get_mempool_info("fees_recommended", gerty)
|
||||||
|
pos_y = 80 + y_offset
|
||||||
|
text.append(get_text_item_dict("mempool.space", 40, 160, pos_y, gerty.type))
|
||||||
|
pos_y = 180 + y_offset
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("Recommended Tx Fees", 20, 240, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
|
||||||
|
pos_y = 280 + y_offset
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("None"), 15, 30, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("Low"), 15, 235, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("Medium"), 15, 460, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("High"), 15, 750, pos_y, gerty.type)
|
||||||
|
)
|
||||||
|
|
||||||
|
pos_y = 340 + y_offset
|
||||||
|
font_size = 15
|
||||||
|
fee_append = "/vB"
|
||||||
|
fee_rate = fees["economyFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=30,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["hourFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=235,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["halfHourFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=460,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["fastestFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
text="{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size=font_size,
|
||||||
|
x_pos=750,
|
||||||
|
y_pos=pos_y,
|
||||||
|
gerty_type=gerty.type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return text
|
||||||
59
lnbits/extensions/gerty/migrations.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
async def m001_initial(db):
|
||||||
|
"""
|
||||||
|
Initial Gertys table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE gerty.gertys (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
refresh_time INT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
lnbits_wallets TEXT,
|
||||||
|
mempool_endpoint TEXT,
|
||||||
|
exchange TEXT,
|
||||||
|
display_preferences TEXT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m002_add_utc_offset_col(db):
|
||||||
|
"""
|
||||||
|
support for UTC offset
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE gerty.gertys ADD COLUMN utc_offset INT;")
|
||||||
|
|
||||||
|
|
||||||
|
async def m003_add_gerty_model_col(db):
|
||||||
|
"""
|
||||||
|
support for Gerty model col
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE gerty.gertys ADD COLUMN type TEXT;")
|
||||||
|
|
||||||
|
|
||||||
|
#########MEMPOOL MIGRATIONS########
|
||||||
|
|
||||||
|
|
||||||
|
async def m004_initial(db):
|
||||||
|
"""
|
||||||
|
Initial Gertys table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE gerty.mempool (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
mempool_endpoint TEXT NOT NULL,
|
||||||
|
endpoint TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
time TIMESTAMP
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m005_add_gerty_model_col(db):
|
||||||
|
"""
|
||||||
|
support for Gerty model col
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE gerty.gertys ADD COLUMN urls TEXT;")
|
||||||
47
lnbits/extensions/gerty/models.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from sqlite3 import Row
|
||||||
|
|
||||||
|
from fastapi import Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Gerty(BaseModel):
|
||||||
|
id: str = Query(None)
|
||||||
|
name: str
|
||||||
|
refresh_time: int = Query(None)
|
||||||
|
utc_offset: int = Query(None)
|
||||||
|
wallet: str = Query(None)
|
||||||
|
type: str
|
||||||
|
lnbits_wallets: str = Query(
|
||||||
|
None
|
||||||
|
) # Wallets to keep an eye on, {"wallet-id": "wallet-read-key, etc"}
|
||||||
|
mempool_endpoint: str = Query(None) # Mempool endpoint to use
|
||||||
|
exchange: str = Query(
|
||||||
|
None
|
||||||
|
) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
|
||||||
|
display_preferences: str = Query(None)
|
||||||
|
urls: str = Query(None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "Gerty":
|
||||||
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
|
#########MEMPOOL MODELS###########
|
||||||
|
|
||||||
|
|
||||||
|
class MempoolEndpoint(BaseModel):
|
||||||
|
fees_recommended: str = "/api/v1/fees/recommended"
|
||||||
|
hashrate_1w: str = "/api/v1/mining/hashrate/1w"
|
||||||
|
hashrate_1m: str = "/api/v1/mining/hashrate/1m"
|
||||||
|
statistics: str = "/api/v1/lightning/statistics/latest"
|
||||||
|
difficulty_adjustment: str = "/api/v1/difficulty-adjustment"
|
||||||
|
tip_height: str = "/api/blocks/tip/height"
|
||||||
|
mempool: str = "/api/mempool"
|
||||||
|
|
||||||
|
|
||||||
|
class Mempool(BaseModel):
|
||||||
|
id: str = Query(None)
|
||||||
|
mempool_endpoint: str = Query(None)
|
||||||
|
endpoint: str = Query(None)
|
||||||
|
data: str = Query(None)
|
||||||
|
time: int = Query(None)
|
||||||
66
lnbits/extensions/gerty/number_prefixer.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def si_classifier(val):
|
||||||
|
suffixes = {
|
||||||
|
24: {"long_suffix": "yotta", "short_suffix": "Y", "scalar": 10**24},
|
||||||
|
21: {"long_suffix": "zetta", "short_suffix": "Z", "scalar": 10**21},
|
||||||
|
18: {"long_suffix": "exa", "short_suffix": "E", "scalar": 10**18},
|
||||||
|
15: {"long_suffix": "peta", "short_suffix": "P", "scalar": 10**15},
|
||||||
|
12: {"long_suffix": "tera", "short_suffix": "T", "scalar": 10**12},
|
||||||
|
9: {"long_suffix": "giga", "short_suffix": "G", "scalar": 10**9},
|
||||||
|
6: {"long_suffix": "mega", "short_suffix": "M", "scalar": 10**6},
|
||||||
|
3: {"long_suffix": "kilo", "short_suffix": "k", "scalar": 10**3},
|
||||||
|
0: {"long_suffix": "", "short_suffix": "", "scalar": 10**0},
|
||||||
|
-3: {"long_suffix": "milli", "short_suffix": "m", "scalar": 10**-3},
|
||||||
|
-6: {"long_suffix": "micro", "short_suffix": "µ", "scalar": 10**-6},
|
||||||
|
-9: {"long_suffix": "nano", "short_suffix": "n", "scalar": 10**-9},
|
||||||
|
-12: {"long_suffix": "pico", "short_suffix": "p", "scalar": 10**-12},
|
||||||
|
-15: {"long_suffix": "femto", "short_suffix": "f", "scalar": 10**-15},
|
||||||
|
-18: {"long_suffix": "atto", "short_suffix": "a", "scalar": 10**-18},
|
||||||
|
-21: {"long_suffix": "zepto", "short_suffix": "z", "scalar": 10**-21},
|
||||||
|
-24: {"long_suffix": "yocto", "short_suffix": "y", "scalar": 10**-24},
|
||||||
|
}
|
||||||
|
exponent = int(math.floor(math.log10(abs(val)) / 3.0) * 3)
|
||||||
|
return suffixes.get(exponent, None)
|
||||||
|
|
||||||
|
|
||||||
|
def si_formatter(value):
|
||||||
|
"""
|
||||||
|
Return a triple of scaled value, short suffix, long suffix, or None if
|
||||||
|
the value cannot be classified.
|
||||||
|
"""
|
||||||
|
classifier = si_classifier(value)
|
||||||
|
if classifier == None:
|
||||||
|
# Don't know how to classify this value
|
||||||
|
return None
|
||||||
|
|
||||||
|
scaled = value / classifier["scalar"]
|
||||||
|
return (scaled, classifier["short_suffix"], classifier["long_suffix"])
|
||||||
|
|
||||||
|
|
||||||
|
def si_format(value, precision=4, long_form=False, separator=""):
|
||||||
|
"""
|
||||||
|
"SI prefix" formatted string: return a string with the given precision
|
||||||
|
and an appropriate order-of-3-magnitudes suffix, e.g.:
|
||||||
|
si_format(1001.0) => '1.00K'
|
||||||
|
si_format(0.00000000123, long_form=True, separator=' ') => '1.230 nano'
|
||||||
|
"""
|
||||||
|
scaled, short_suffix, long_suffix = si_formatter(value)
|
||||||
|
|
||||||
|
if scaled == None:
|
||||||
|
# Don't know how to format this value
|
||||||
|
return value
|
||||||
|
|
||||||
|
suffix = long_suffix if long_form else short_suffix
|
||||||
|
|
||||||
|
if abs(scaled) < 10:
|
||||||
|
precision = precision - 1
|
||||||
|
elif abs(scaled) < 100:
|
||||||
|
precision = precision - 2
|
||||||
|
else:
|
||||||
|
precision = precision - 3
|
||||||
|
|
||||||
|
return "{scaled:.{precision}f}{separator}{suffix}".format(
|
||||||
|
scaled=scaled, precision=precision, separator=separator, suffix=suffix
|
||||||
|
)
|
||||||
BIN
lnbits/extensions/gerty/static/gerty.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
lnbits/extensions/gerty/static/gerty.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
1099
lnbits/extensions/gerty/static/satoshi.json
Normal file
1099
lnbits/extensions/gerty/static/satoshi_long.json
Normal file
25
lnbits/extensions/gerty/templates/gerty/_api_docs.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
Gerty (your bitcoin assistant): Use the software Gerty or
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
target="_blank"
|
||||||
|
href="https://shop.lnbits.com/product/gerty-a-bitcoin-assistant"
|
||||||
|
>hardware Gerty</a
|
||||||
|
><br />
|
||||||
|
<small>
|
||||||
|
Created by,
|
||||||
|
<a class="text-secondary" href="https://github.com/blackcoffeexbt"
|
||||||
|
>Black Coffee</a
|
||||||
|
>,
|
||||||
|
<a class="text-secondary" href="https://github.com/benarc"
|
||||||
|
>Ben Arc</a
|
||||||
|
></small
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://shop.lnbits.com/product/gerty-a-bitcoin-assistant"
|
||||||
|
><img src="/gerty/static/gerty.jpg" style="max-width: 100%"
|
||||||
|
/></a>
|
||||||
|
</q-card-section>
|
||||||
256
lnbits/extensions/gerty/templates/gerty/gerty.html
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
{% extends "public.html" %} {% block toolbar_title %} Gerty: {% raw %}{{
|
||||||
|
gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="q-pa-md row items-start q-gutter-md"
|
||||||
|
v-if="fun_exchange_market_rate || fun_satoshi_quotes"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
v-if="fun_exchange_market_rate"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-sm"
|
||||||
|
style="background: none !important"
|
||||||
|
>
|
||||||
|
<q-card-section class="text-h1 q-pa-none">
|
||||||
|
<small> <b>{{fun_exchange_market_rate["amount"]}}</b></small>
|
||||||
|
<small class="text-h4"
|
||||||
|
>{{fun_exchange_market_rate["unit"].split(" ")[1]}}</small
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card
|
||||||
|
v-if="fun_satoshi_quotes['quote']"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-none text-body1 blockquote"
|
||||||
|
style="background: none !important"
|
||||||
|
>
|
||||||
|
<blockquote class="text-right" style="max-width: 900px">
|
||||||
|
<p>"{{fun_satoshi_quotes["quote"]}}"</p>
|
||||||
|
<small>~ Satoshi {{fun_satoshi_quotes["date"]}}</small>
|
||||||
|
</blockquote>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="q-pa-md row items-start q-gutter-md"
|
||||||
|
v-if="lnbits_wallets_balance[0]"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="q-pa-sm"
|
||||||
|
v-for="(wallet, t) in lnbits_wallets_balance"
|
||||||
|
:style="`background-color: ${wallet.color1} !important`"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-none q-pa-sm"
|
||||||
|
>
|
||||||
|
<q-card-section class="text-h1 q-pa-none">
|
||||||
|
<small> <b>{{wallet["amount"]}}</b></small>
|
||||||
|
<small class="text-h4">({{wallet["name"]}})</small>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="q-pa-md row items-start q-gutter-md"
|
||||||
|
v-if="dashboard_onchain[0] || dashboard_mining[0] || lightning_dashboard[0] || url_checker[0]"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="q-pa-sm"
|
||||||
|
v-if="dashboard_onchain[0]"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-sm"
|
||||||
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Onchain</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<p v-for="(item, t) in dashboard_onchain">
|
||||||
|
<b>{{item[0].value}}: </b>{{item[1].value}}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card class="q-pa-sm" v-if="dashboard_mining[0]" unelevated class="q-pa-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Mining</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<p v-for="(item, t) in dashboard_mining">
|
||||||
|
<b>{{item[0].value}}:</b> {{item[1].value}}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card
|
||||||
|
class="q-pa-sm"
|
||||||
|
v-if="lightning_dashboard[0]"
|
||||||
|
unelevated
|
||||||
|
class="q-pa-sm"
|
||||||
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Lightning (Last 7 days)</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<p v-for="(item, t) in lightning_dashboard">
|
||||||
|
<b>{{item[0].value}}:</b> {{item[1].value}}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<q-card class="q-pa-sm" v-if="url_checker" unelevated class="q-pa-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Servers to check</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<div class="row q-pb-md" v-for="(item, t) in url_checker">
|
||||||
|
<div class="col-8">
|
||||||
|
<small>
|
||||||
|
<b style="word-wrap: break-word; max-width: 230px; display: block">
|
||||||
|
<a class="text-secondary" class="text-primary">
|
||||||
|
{{item[0].value}}
|
||||||
|
</a>
|
||||||
|
</b>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-chip
|
||||||
|
v-if="item[1].value < 300"
|
||||||
|
square
|
||||||
|
size="sm"
|
||||||
|
color="green"
|
||||||
|
text-color="white"
|
||||||
|
icon="sentiment_satisfied"
|
||||||
|
>
|
||||||
|
{{item[1].value}}
|
||||||
|
</q-chip>
|
||||||
|
<q-chip
|
||||||
|
v-else-if="item[1].value >= 300"
|
||||||
|
square
|
||||||
|
size="sm"
|
||||||
|
color="yellow"
|
||||||
|
text-color="white"
|
||||||
|
icon="sentiment_dissatisfied"
|
||||||
|
>
|
||||||
|
{{item[1].value}}
|
||||||
|
</q-chip>
|
||||||
|
<q-chip
|
||||||
|
v-else
|
||||||
|
square
|
||||||
|
size="sm"
|
||||||
|
color="red"
|
||||||
|
text-color="white"
|
||||||
|
icon="sentiment_dissatisfied"
|
||||||
|
>
|
||||||
|
{{item[1].value}}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endraw %} {% endblock %} {% block scripts %}
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
lnbits_wallets_balance: {},
|
||||||
|
dashboard_onchain: {},
|
||||||
|
fun_satoshi_quotes: {},
|
||||||
|
fun_exchange_market_rate: {
|
||||||
|
unit: ''
|
||||||
|
},
|
||||||
|
dashboard_mining: {},
|
||||||
|
lightning_dashboard: {},
|
||||||
|
url_checker: {},
|
||||||
|
dashboard_mining: {},
|
||||||
|
gerty: [],
|
||||||
|
gerty_id: `{{gerty}}`,
|
||||||
|
gertyname: '',
|
||||||
|
walletColors: [
|
||||||
|
{first: '#3f51b5', second: '#1a237e'},
|
||||||
|
{first: '#9c27b0', second: '#4a148c'},
|
||||||
|
{first: '#e91e63', second: '#880e4f'},
|
||||||
|
{first: '#009688', second: '#004d40'},
|
||||||
|
{first: '#ff9800', second: '#e65100'},
|
||||||
|
{first: '#2196f3', second: '#0d47a1'},
|
||||||
|
{first: '#4caf50', second: '#1b5e20'}
|
||||||
|
],
|
||||||
|
gertywallets: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getGertyInfo: async function () {
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/gerty/api/v1/gerty/pages/${this.gerty_id}/${i}`
|
||||||
|
)
|
||||||
|
this.gerty[i] = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.gerty.length; i++) {
|
||||||
|
if (this.gerty[i].screen.group == 'lnbits_wallets_balance') {
|
||||||
|
for (let q = 0; q < this.gerty[i].screen.areas.length; q++) {
|
||||||
|
this.lnbits_wallets_balance[q] = {
|
||||||
|
name: this.gerty[i].screen.areas[q][0].value,
|
||||||
|
amount: this.gerty[i].screen.areas[q][1].value,
|
||||||
|
color1: this.walletColors[q].first,
|
||||||
|
color2: this.walletColors[q].second
|
||||||
|
}
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'url_checker') {
|
||||||
|
this.url_checker = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'dashboard_onchain') {
|
||||||
|
this.dashboard_onchain = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'dashboard_mining') {
|
||||||
|
this.dashboard_mining = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'lightning_dashboard') {
|
||||||
|
this.lightning_dashboard = this.gerty[i].screen.areas
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'fun_satoshi_quotes') {
|
||||||
|
this.fun_satoshi_quotes['quote'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][0].value
|
||||||
|
this.fun_satoshi_quotes['date'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][1].value
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
if (this.gerty[i].screen.group == 'fun_exchange_market_rate') {
|
||||||
|
this.fun_exchange_market_rate['unit'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][0].value
|
||||||
|
this.fun_exchange_market_rate['amount'] = this.gerty[
|
||||||
|
i
|
||||||
|
].screen.areas[0][1].value
|
||||||
|
this.gertyname = this.gerty[i].settings.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(this.getGertyInfo, 20000)
|
||||||
|
this.$forceUpdate()
|
||||||
|
return this.gerty
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getGertyInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
798
lnbits/extensions/gerty/templates/gerty/index.html
Normal file
|
|
@ -0,0 +1,798 @@
|
||||||
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
|
%} {% block page %}
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||||
|
>New Gerty
|
||||||
|
</q-btn>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="text-subtitle1 q-my-none">Gerty</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:data="gertys"
|
||||||
|
row-key="id"
|
||||||
|
:columns="gertysTable.columns"
|
||||||
|
:pagination.sync="gertysTable.pagination"
|
||||||
|
>
|
||||||
|
{% raw %}
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
:class="`col__${col.name} text-truncate elipsis`"
|
||||||
|
>
|
||||||
|
{{ col.label }}
|
||||||
|
</q-th>
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="sentiment_satisfied"
|
||||||
|
color="green"
|
||||||
|
type="a"
|
||||||
|
:href="props.row.gerty"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<q-tooltip>Launch software Gerty</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="code"
|
||||||
|
color="pink"
|
||||||
|
type="a"
|
||||||
|
:href="props.row.gertyJson"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<q-tooltip>View Gerty API</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ (col.name == 'tip_options' && col.value ?
|
||||||
|
JSON.parse(col.value).join(", ") : col.value) }}
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="updateformDialog(props.row.id)"
|
||||||
|
icon="edit"
|
||||||
|
color="light-blue"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteGerty(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9">
|
||||||
|
<h6 class="text-subtitle1 q-my-none">
|
||||||
|
{{ SITE_TITLE }} Gerty extension
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
label="Swagger API"
|
||||||
|
type="a"
|
||||||
|
href="../docs#/gerty"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-list> {% include "gerty/_api_docs.html" %} </q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form @submit="sendFormDataGerty" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="Son of Gerty"
|
||||||
|
></q-input>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.fun_satoshi_quotes"
|
||||||
|
val="xs"
|
||||||
|
label="Satoshi Quotes"
|
||||||
|
><q-tooltip
|
||||||
|
>Displays random quotes from Satoshi</q-tooltip
|
||||||
|
></q-checkbox
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.fun_exchange_market_rate"
|
||||||
|
val="xs"
|
||||||
|
label="Fiat to BTC price"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.lnbits_wallets_balance"
|
||||||
|
val="xs"
|
||||||
|
label="LNbits"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.dashboard_onchain"
|
||||||
|
val="xs"
|
||||||
|
label="Onchain"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.dashboard_mining"
|
||||||
|
val="xs"
|
||||||
|
label="Mining"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.lightning_dashboard"
|
||||||
|
val="xs"
|
||||||
|
label="Lightning"
|
||||||
|
></q-checkbox>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-pl-md"
|
||||||
|
size="xs"
|
||||||
|
v-model="formDialog.data.display_preferences.url_checker"
|
||||||
|
val="xs"
|
||||||
|
label="URL Checker"
|
||||||
|
></q-checkbox>
|
||||||
|
<br />
|
||||||
|
<q-select
|
||||||
|
v-if="formDialog.data.display_preferences.fun_exchange_market_rate"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.data.exchange"
|
||||||
|
:options="currencyOptions"
|
||||||
|
label="Exchange rate"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-if="formDialog.data.display_preferences.lnbits_wallets_balance"
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.data.lnbits_wallets"
|
||||||
|
use-input
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
hide-dropdown-icon
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
label="Invoice keys of wallets to watch"
|
||||||
|
>
|
||||||
|
<q-tooltip>Hit enter to add values</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-if="formDialog.data.display_preferences.url_checker"
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="formDialog.data.urls"
|
||||||
|
use-input
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
hide-dropdown-icon
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
label="Urls to watch."
|
||||||
|
>
|
||||||
|
<q-tooltip>Hit enter to add values</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-toggle
|
||||||
|
label="*Advanced"
|
||||||
|
v-model="toggleStates.advanced"
|
||||||
|
@input="setAdvanced"
|
||||||
|
></q-toggle>
|
||||||
|
<br />
|
||||||
|
<q-input
|
||||||
|
v-if="toggleStates.advanced"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.mempool_endpoint"
|
||||||
|
label="Mempool link"
|
||||||
|
class="q-pb-sm"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="toggleStates.advanced"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.refresh_time"
|
||||||
|
label="Refresh time in seconds"
|
||||||
|
class="q-pb-md"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
>The amount of time in seconds between screen updates
|
||||||
|
</q-tooltip>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="formDialog.data.name == null"
|
||||||
|
type="submit"
|
||||||
|
class="q-mr-md"
|
||||||
|
v-if="!formDialog.data.id"
|
||||||
|
>Create Gerty
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="formDialog.data.name == null"
|
||||||
|
type="submit"
|
||||||
|
>Update Gerty
|
||||||
|
</q-btn>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script>
|
||||||
|
var mapGerty = function (obj) {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||||
|
obj.gerty = ['/gerty/', obj.id].join('')
|
||||||
|
obj.gertyJson = ['/gerty/api/v1/gerty/pages/', obj.id, '/0'].join('')
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
toggleStates: {
|
||||||
|
fun: false,
|
||||||
|
onchain: false,
|
||||||
|
mempool: false,
|
||||||
|
mining: false,
|
||||||
|
lightning: false,
|
||||||
|
advanced: false
|
||||||
|
},
|
||||||
|
oldToggleStates: {},
|
||||||
|
gertys: [],
|
||||||
|
currencyOptions: [
|
||||||
|
'USD',
|
||||||
|
'EUR',
|
||||||
|
'GBP',
|
||||||
|
'AED',
|
||||||
|
'AFN',
|
||||||
|
'ALL',
|
||||||
|
'AMD',
|
||||||
|
'ANG',
|
||||||
|
'AOA',
|
||||||
|
'ARS',
|
||||||
|
'AUD',
|
||||||
|
'AWG',
|
||||||
|
'AZN',
|
||||||
|
'BAM',
|
||||||
|
'BBD',
|
||||||
|
'BDT',
|
||||||
|
'BGN',
|
||||||
|
'BHD',
|
||||||
|
'BIF',
|
||||||
|
'BMD',
|
||||||
|
'BND',
|
||||||
|
'BOB',
|
||||||
|
'BRL',
|
||||||
|
'BSD',
|
||||||
|
'BTN',
|
||||||
|
'BWP',
|
||||||
|
'BYN',
|
||||||
|
'BZD',
|
||||||
|
'CAD',
|
||||||
|
'CDF',
|
||||||
|
'CHF',
|
||||||
|
'CLF',
|
||||||
|
'CLP',
|
||||||
|
'CNH',
|
||||||
|
'CNY',
|
||||||
|
'COP',
|
||||||
|
'CRC',
|
||||||
|
'CUC',
|
||||||
|
'CUP',
|
||||||
|
'CVE',
|
||||||
|
'CZK',
|
||||||
|
'DJF',
|
||||||
|
'DKK',
|
||||||
|
'DOP',
|
||||||
|
'DZD',
|
||||||
|
'EGP',
|
||||||
|
'ERN',
|
||||||
|
'ETB',
|
||||||
|
'EUR',
|
||||||
|
'FJD',
|
||||||
|
'FKP',
|
||||||
|
'GBP',
|
||||||
|
'GEL',
|
||||||
|
'GGP',
|
||||||
|
'GHS',
|
||||||
|
'GIP',
|
||||||
|
'GMD',
|
||||||
|
'GNF',
|
||||||
|
'GTQ',
|
||||||
|
'GYD',
|
||||||
|
'HKD',
|
||||||
|
'HNL',
|
||||||
|
'HRK',
|
||||||
|
'HTG',
|
||||||
|
'HUF',
|
||||||
|
'IDR',
|
||||||
|
'ILS',
|
||||||
|
'IMP',
|
||||||
|
'INR',
|
||||||
|
'IQD',
|
||||||
|
'IRR',
|
||||||
|
'IRT',
|
||||||
|
'ISK',
|
||||||
|
'JEP',
|
||||||
|
'JMD',
|
||||||
|
'JOD',
|
||||||
|
'JPY',
|
||||||
|
'KES',
|
||||||
|
'KGS',
|
||||||
|
'KHR',
|
||||||
|
'KMF',
|
||||||
|
'KPW',
|
||||||
|
'KRW',
|
||||||
|
'KWD',
|
||||||
|
'KYD',
|
||||||
|
'KZT',
|
||||||
|
'LAK',
|
||||||
|
'LBP',
|
||||||
|
'LKR',
|
||||||
|
'LRD',
|
||||||
|
'LSL',
|
||||||
|
'LYD',
|
||||||
|
'MAD',
|
||||||
|
'MDL',
|
||||||
|
'MGA',
|
||||||
|
'MKD',
|
||||||
|
'MMK',
|
||||||
|
'MNT',
|
||||||
|
'MOP',
|
||||||
|
'MRO',
|
||||||
|
'MUR',
|
||||||
|
'MVR',
|
||||||
|
'MWK',
|
||||||
|
'MXN',
|
||||||
|
'MYR',
|
||||||
|
'MZN',
|
||||||
|
'NAD',
|
||||||
|
'NGN',
|
||||||
|
'NIO',
|
||||||
|
'NOK',
|
||||||
|
'NPR',
|
||||||
|
'NZD',
|
||||||
|
'OMR',
|
||||||
|
'PAB',
|
||||||
|
'PEN',
|
||||||
|
'PGK',
|
||||||
|
'PHP',
|
||||||
|
'PKR',
|
||||||
|
'PLN',
|
||||||
|
'PYG',
|
||||||
|
'QAR',
|
||||||
|
'RON',
|
||||||
|
'RSD',
|
||||||
|
'RUB',
|
||||||
|
'RWF',
|
||||||
|
'SAR',
|
||||||
|
'SBD',
|
||||||
|
'SCR',
|
||||||
|
'SDG',
|
||||||
|
'SEK',
|
||||||
|
'SGD',
|
||||||
|
'SHP',
|
||||||
|
'SLL',
|
||||||
|
'SOS',
|
||||||
|
'SRD',
|
||||||
|
'SSP',
|
||||||
|
'STD',
|
||||||
|
'SVC',
|
||||||
|
'SYP',
|
||||||
|
'SZL',
|
||||||
|
'THB',
|
||||||
|
'TJS',
|
||||||
|
'TMT',
|
||||||
|
'TND',
|
||||||
|
'TOP',
|
||||||
|
'TRY',
|
||||||
|
'TTD',
|
||||||
|
'TWD',
|
||||||
|
'TZS',
|
||||||
|
'UAH',
|
||||||
|
'UGX',
|
||||||
|
'USD',
|
||||||
|
'UYU',
|
||||||
|
'UZS',
|
||||||
|
'VEF',
|
||||||
|
'VES',
|
||||||
|
'VND',
|
||||||
|
'VUV',
|
||||||
|
'WST',
|
||||||
|
'XAF',
|
||||||
|
'XAG',
|
||||||
|
'XAU',
|
||||||
|
'XCD',
|
||||||
|
'XDR',
|
||||||
|
'XOF',
|
||||||
|
'XPD',
|
||||||
|
'XPF',
|
||||||
|
'XPT',
|
||||||
|
'YER',
|
||||||
|
'ZAR',
|
||||||
|
'ZMW',
|
||||||
|
'ZWL'
|
||||||
|
],
|
||||||
|
gertysTable: {
|
||||||
|
columns: [
|
||||||
|
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||||
|
{
|
||||||
|
name: 'exchange',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Exchange',
|
||||||
|
field: 'exchange'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mempool_endpoint',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Mempool Endpoint',
|
||||||
|
field: 'mempool_endpoint'
|
||||||
|
},
|
||||||
|
{name: 'id', align: 'left', label: 'Gerty ID', field: 'id'}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
type: 'Mini Gerty',
|
||||||
|
exchange: 'USD',
|
||||||
|
utc_offset: new Date().getTimezoneOffset(),
|
||||||
|
display_preferences: {
|
||||||
|
dashboard: false,
|
||||||
|
fun_satoshi_quotes: false,
|
||||||
|
fun_exchange_market_rate: false,
|
||||||
|
dashboard_onchain: false,
|
||||||
|
mempool_recommended_fees: false,
|
||||||
|
dashboard_mining: false,
|
||||||
|
lightning_dashboard: false,
|
||||||
|
onchain: false,
|
||||||
|
onchain_difficulty_epoch_progress: false,
|
||||||
|
onchain_difficulty_retarget_date: false,
|
||||||
|
onchain_difficulty_blocks_remaining: false,
|
||||||
|
onchain_difficulty_epoch_time_remaining: false,
|
||||||
|
onchain_block_height: false,
|
||||||
|
mempool_tx_count: false,
|
||||||
|
mining_current_hash_rate: false,
|
||||||
|
mining_current_difficulty: false,
|
||||||
|
lnbits_wallets_balance: false,
|
||||||
|
url_checker: false
|
||||||
|
},
|
||||||
|
lnbits_wallets: [],
|
||||||
|
urls: [],
|
||||||
|
mempool_endpoint: 'https://mempool.space',
|
||||||
|
refresh_time: 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setAdvanced: function () {
|
||||||
|
self = this
|
||||||
|
self.formDialog.data.mempool_endpoint = 'https://mempool.space'
|
||||||
|
self.formDialog.data.refresh_time = 300
|
||||||
|
},
|
||||||
|
setWallets: function () {
|
||||||
|
self = this
|
||||||
|
if (!self.formDialog.data.display_preferences.lnbits_wallets_balance) {
|
||||||
|
self.formDialog.data.lnbits_wallets = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setUrls: function () {
|
||||||
|
self = this
|
||||||
|
if (!self.formDialog.data.display_preferences.url_checker) {
|
||||||
|
self.formDialog.data.urls = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setOnchain: function () {
|
||||||
|
self = this
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_epoch_progress =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_retarget_date =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_blocks_remaining = !self
|
||||||
|
.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_difficulty_epoch_time_remaining =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
self.formDialog.data.display_preferences.onchain_block_height =
|
||||||
|
self.toggleStates.onchain
|
||||||
|
},
|
||||||
|
setMining: function () {
|
||||||
|
self = this
|
||||||
|
self.formDialog.data.display_preferences.mining_current_hash_rate =
|
||||||
|
self.toggleStates.mining
|
||||||
|
self.formDialog.data.display_preferences.mining_current_difficulty =
|
||||||
|
self.toggleStates.mining
|
||||||
|
},
|
||||||
|
closeFormDialog: function () {
|
||||||
|
this.formDialog.data = {
|
||||||
|
utc_offset: 0,
|
||||||
|
lnbits_wallets: [],
|
||||||
|
urls: [],
|
||||||
|
mempool_endpoint: 'https://mempool.space',
|
||||||
|
refresh_time: 300,
|
||||||
|
display_preferences: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getGertys: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/gerty/api/v1/gerty?all_wallets=true',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.gertys = response.data.map(function (obj) {
|
||||||
|
return mapGerty(obj)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateformDialog: function (formId) {
|
||||||
|
var gerty = _.findWhere(this.gertys, {id: formId})
|
||||||
|
this.formDialog.data.id = gerty.id
|
||||||
|
this.formDialog.data.name = gerty.name
|
||||||
|
this.formDialog.data.type = gerty.type
|
||||||
|
this.formDialog.data.utc_offset = gerty.utc_offset
|
||||||
|
this.formDialog.data.lnbits_wallets = JSON.parse(gerty.lnbits_wallets)
|
||||||
|
this.formDialog.data.urls = JSON.parse(gerty.urls)
|
||||||
|
;(this.formDialog.data.exchange = gerty.exchange),
|
||||||
|
(this.formDialog.data.mempool_endpoint = gerty.mempool_endpoint),
|
||||||
|
(this.formDialog.data.refresh_time = gerty.refresh_time),
|
||||||
|
(this.formDialog.data.display_preferences = JSON.parse(
|
||||||
|
gerty.display_preferences
|
||||||
|
)),
|
||||||
|
(this.formDialog.show = true)
|
||||||
|
},
|
||||||
|
sendFormDataGerty: function () {
|
||||||
|
if (this.formDialog.data.id) {
|
||||||
|
this.updateGerty(
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
this.formDialog.data
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.createGerty(
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
this.formDialog.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createGerty: function () {
|
||||||
|
if (
|
||||||
|
this.formDialog.data.display_preferences.dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.lightning_dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.url_checker
|
||||||
|
) {
|
||||||
|
this.formDialog.data.type = 'Gerty'
|
||||||
|
}
|
||||||
|
var data = {
|
||||||
|
name: this.formDialog.data.name,
|
||||||
|
utc_offset: this.formDialog.data.utc_offset,
|
||||||
|
type: this.formDialog.data.type,
|
||||||
|
lnbits_wallets: JSON.stringify(this.formDialog.data.lnbits_wallets),
|
||||||
|
urls: JSON.stringify(this.formDialog.data.urls),
|
||||||
|
exchange: this.formDialog.data.exchange,
|
||||||
|
mempool_endpoint: this.formDialog.data.mempool_endpoint,
|
||||||
|
refresh_time: this.formDialog.data.refresh_time,
|
||||||
|
display_preferences: JSON.stringify(
|
||||||
|
this.formDialog.data.display_preferences
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/gerty/api/v1/gerty',
|
||||||
|
this.g.user.wallets[0].inkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.formDialog.show = false
|
||||||
|
self.gertys.push(mapGerty(response.data))
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateGerty: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
if (
|
||||||
|
this.formDialog.data.display_preferences.dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain ||
|
||||||
|
this.formDialog.data.display_preferences.lightning_dashboard ||
|
||||||
|
this.formDialog.data.display_preferences.url_checker
|
||||||
|
) {
|
||||||
|
this.formDialog.data.type = 'Gerty'
|
||||||
|
}
|
||||||
|
data.utc_offset = this.formDialog.data.utc_offset
|
||||||
|
data.type = this.formDialog.data.type
|
||||||
|
data.lnbits_wallets = JSON.stringify(
|
||||||
|
this.formDialog.data.lnbits_wallets
|
||||||
|
)
|
||||||
|
data.urls = JSON.stringify(this.formDialog.data.urls)
|
||||||
|
data.display_preferences = JSON.stringify(
|
||||||
|
this.formDialog.data.display_preferences
|
||||||
|
)
|
||||||
|
LNbits.api
|
||||||
|
.request('PUT', '/gerty/api/v1/gerty/' + data.id, wallet, data)
|
||||||
|
.then(function (response) {
|
||||||
|
self.gertys = _.reject(self.gertys, function (obj) {
|
||||||
|
return obj.id == data.id
|
||||||
|
})
|
||||||
|
self.formDialog.show = false
|
||||||
|
self.gertys.push(mapGerty(response.data))
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteGerty: function (gertyId) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
var gerty = _.findWhere(self.gertys, {id: gertyId})
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this Gerty?')
|
||||||
|
.onOk(function () {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/gerty/api/v1/gerty/' + gertyId,
|
||||||
|
_.findWhere(self.g.user.wallets, {id: gerty.wallet}).adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.gertys = _.reject(self.gertys, function (obj) {
|
||||||
|
return obj.id == gertyId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportCSV: function () {
|
||||||
|
LNbits.utils.exportCSV(this.gertysTable.columns, this.gertys)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMiniGerty() {
|
||||||
|
return this.formDialog.data.type == 'Mini Gerty'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
if (this.g.user.wallets.length) {
|
||||||
|
this.getGertys()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'formDialog.data.type': {
|
||||||
|
handler(value) {
|
||||||
|
if (value == 'Mini Gerty') {
|
||||||
|
this.formDialog.data.display_preferences.dashboard = false
|
||||||
|
this.formDialog.data.display_preferences.dashboard_onchain = false
|
||||||
|
this.formDialog.data.display_preferences.dashboard_mining = false
|
||||||
|
this.formDialog.data.display_preferences.lightning_dashboard = false
|
||||||
|
this.formDialog.data.display_preferences.fun_satoshi_quotes = false
|
||||||
|
this.formDialog.data.display_preferences.mempool_recommended_fees = false
|
||||||
|
this.formDialog.data.display_preferences.onchain = false
|
||||||
|
this.formDialog.data.display_preferences.url_checker = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleStates: {
|
||||||
|
handler(toggleStatesValue) {
|
||||||
|
// Switch all the toggles in each section to the relevant state
|
||||||
|
for (const [toggleKey, toggleValue] of Object.entries(
|
||||||
|
toggleStatesValue
|
||||||
|
)) {
|
||||||
|
if (this.oldToggleStates[toggleKey] !== toggleValue) {
|
||||||
|
for (const [dpKey, dpValue] of Object.entries(
|
||||||
|
this.formDialog.data.display_preferences
|
||||||
|
)) {
|
||||||
|
if (dpKey.indexOf(toggleKey) === 0) {
|
||||||
|
this.formDialog.data.display_preferences[dpKey] = toggleValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This is a weird hack we have to use to get VueJS to persist the previous toggle state between
|
||||||
|
// watches. VueJS passes the old and new values by reference so when comparing objects they
|
||||||
|
// will have the same values unless we do this
|
||||||
|
this.oldToggleStates = JSON.parse(JSON.stringify(toggleStatesValue))
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %} {% block styles %}
|
||||||
|
<style>
|
||||||
|
.col__display_preferences {
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
33
lnbits/extensions/gerty/views.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import Depends, Request
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
from lnbits.core.models import User
|
||||||
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
|
from . import gerty_ext, gerty_renderer
|
||||||
|
from .crud import get_gerty
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
|
return gerty_renderer().TemplateResponse(
|
||||||
|
"gerty/index.html", {"request": request, "user": user.dict()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/{gerty_id}", response_class=HTMLResponse)
|
||||||
|
async def display(request: Request, gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
||||||
|
)
|
||||||
|
return gerty_renderer().TemplateResponse(
|
||||||
|
"gerty/gerty.html", {"request": request, "gerty": gerty_id}
|
||||||
|
)
|
||||||
188
lnbits/extensions/gerty/views_api.py
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import json
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import Depends, Query
|
||||||
|
from loguru import logger
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_user
|
||||||
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
|
||||||
|
from . import gerty_ext
|
||||||
|
from .crud import (
|
||||||
|
create_gerty,
|
||||||
|
delete_gerty,
|
||||||
|
get_gerty,
|
||||||
|
get_gertys,
|
||||||
|
get_mempool_info,
|
||||||
|
update_gerty,
|
||||||
|
)
|
||||||
|
from .helpers import (
|
||||||
|
gerty_should_sleep,
|
||||||
|
get_next_update_time,
|
||||||
|
get_satoshi,
|
||||||
|
get_screen_data,
|
||||||
|
get_screen_slug_by_index,
|
||||||
|
)
|
||||||
|
from .models import Gerty
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty", status_code=HTTPStatus.OK)
|
||||||
|
async def api_gertys(
|
||||||
|
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||||
|
):
|
||||||
|
wallet_ids = [wallet.wallet.id]
|
||||||
|
if all_wallets:
|
||||||
|
user = await get_user(wallet.wallet.user)
|
||||||
|
wallet_ids = user.wallet_ids if user else []
|
||||||
|
|
||||||
|
return [gerty.dict() for gerty in await get_gertys(wallet_ids)]
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.post("/api/v1/gerty", status_code=HTTPStatus.CREATED)
|
||||||
|
@gerty_ext.put("/api/v1/gerty/{gerty_id}", status_code=HTTPStatus.OK)
|
||||||
|
async def api_link_create_or_update(
|
||||||
|
data: Gerty,
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
|
gerty_id: str = Query(None),
|
||||||
|
):
|
||||||
|
if gerty_id:
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
if gerty.wallet != wallet.wallet.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="Come on, seriously, this isn't your Gerty!",
|
||||||
|
)
|
||||||
|
|
||||||
|
data.wallet = wallet.wallet.id
|
||||||
|
gerty = await update_gerty(gerty_id, **data.dict())
|
||||||
|
assert gerty, HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
gerty = await create_gerty(wallet_id=wallet.wallet.id, data=data)
|
||||||
|
|
||||||
|
return {**gerty.dict()}
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.delete("/api/v1/gerty/{gerty_id}")
|
||||||
|
async def api_gerty_delete(
|
||||||
|
gerty_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
if gerty.wallet != wallet.wallet.id:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your Gerty.")
|
||||||
|
|
||||||
|
await delete_gerty(gerty_id)
|
||||||
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/satoshiquote", status_code=HTTPStatus.OK)
|
||||||
|
async def api_gerty_satoshi():
|
||||||
|
return await get_satoshi()
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/pages/{gerty_id}/{p}")
|
||||||
|
async def api_gerty_json(gerty_id: str, p: int = 0): # page number
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
|
||||||
|
if not gerty:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
display_preferences = json.loads(gerty.display_preferences)
|
||||||
|
|
||||||
|
enabled_screen_count = 0
|
||||||
|
|
||||||
|
enabled_screens = []
|
||||||
|
|
||||||
|
for screen_slug in display_preferences:
|
||||||
|
is_screen_enabled = display_preferences[screen_slug]
|
||||||
|
if is_screen_enabled:
|
||||||
|
enabled_screen_count += 1
|
||||||
|
enabled_screens.append(screen_slug)
|
||||||
|
|
||||||
|
logger.debug("Screens " + str(enabled_screens))
|
||||||
|
data = await get_screen_data(p, enabled_screens, gerty)
|
||||||
|
|
||||||
|
next_screen_number = 0 if ((p + 1) >= enabled_screen_count) else p + 1
|
||||||
|
|
||||||
|
# get the sleep time
|
||||||
|
sleep_time = gerty.refresh_time if gerty.refresh_time else 300
|
||||||
|
utc_offset = gerty.utc_offset if gerty.utc_offset else 0
|
||||||
|
if gerty_should_sleep(utc_offset):
|
||||||
|
sleep_time_hours = 8
|
||||||
|
sleep_time = 60 * 60 * sleep_time_hours
|
||||||
|
|
||||||
|
return {
|
||||||
|
"settings": {
|
||||||
|
"refreshTime": sleep_time,
|
||||||
|
"requestTimestamp": get_next_update_time(sleep_time, utc_offset),
|
||||||
|
"nextScreenNumber": next_screen_number,
|
||||||
|
"showTextBoundRect": False,
|
||||||
|
"name": gerty.name,
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"slug": get_screen_slug_by_index(p, enabled_screens),
|
||||||
|
"group": get_screen_slug_by_index(p, enabled_screens),
|
||||||
|
"title": data["title"],
|
||||||
|
"areas": data["areas"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
###########CACHED MEMPOOL##############
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/fees-recommended/{gerty_id}")
|
||||||
|
async def api_gerty_get_fees_recommended(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("fees_recommended", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/hashrate-1w/{gerty_id}")
|
||||||
|
async def api_gerty_get_hashrate_1w(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("hashrate_1w", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/hashrate-1m/{gerty_id}")
|
||||||
|
async def api_gerty_get_hashrate_1m(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("hashrate_1m", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/statistics/{gerty_id}")
|
||||||
|
async def api_gerty_get_statistics(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("statistics", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/difficulty-adjustment/{gerty_id}")
|
||||||
|
async def api_gerty_get_difficulty_adjustment(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("difficulty_adjustment", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/tip-height/{gerty_id}")
|
||||||
|
async def api_gerty_get_tip_height(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("tip_height", gerty)
|
||||||
|
|
||||||
|
|
||||||
|
@gerty_ext.get("/api/v1/gerty/mempool/{gerty_id}")
|
||||||
|
async def api_gerty_get_mempool(gerty_id):
|
||||||
|
gerty = await get_gerty(gerty_id)
|
||||||
|
return await get_mempool_info("mempool", gerty)
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<h1>Hivemind</h1>
|
<h1>Hivemind</h1>
|
||||||
|
|
||||||
Placeholder for a future <a href="https://bitcoinhivemind.com/">Bitcoin Hivemind</a> extension.
|
Placeholder for a future <a class="text-secondary" href="https://bitcoinhivemind.com/">Bitcoin Hivemind</a> extension.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
|
|
@ -12,4 +13,12 @@ def hivemind_renderer():
|
||||||
return template_renderer(["lnbits/extensions/hivemind/templates"])
|
return template_renderer(["lnbits/extensions/hivemind/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
hivemind_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/hivemind/static",
|
||||||
|
"app": StaticFiles(packages=[("lnbits", "extensions/hivemind/static")]),
|
||||||
|
"name": "hivemind_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Hivemind",
|
"name": "Hivemind",
|
||||||
"short_description": "Make cheap talk expensive!",
|
"short_description": "Make cheap talk expensive!",
|
||||||
"icon": "batch_prediction",
|
"tile": "/hivemind/static/image/hivemind.png",
|
||||||
"contributors": ["fiatjaf"]
|
"contributors": ["fiatjaf"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/hivemind/static/image/hivemind.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -6,9 +6,10 @@
|
||||||
This extension is just a placeholder for now.
|
This extension is just a placeholder for now.
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://bitcoinhivemind.com/">Hivemind</a> is a Bitcoin sidechain
|
<a class="text-secondary" href="https://bitcoinhivemind.com/">Hivemind</a>
|
||||||
project for a peer-to-peer oracle protocol that absorbs accurate data into
|
is a Bitcoin sidechain project for a peer-to-peer oracle protocol that
|
||||||
a blockchain so that Bitcoin users can speculate in prediction markets.
|
absorbs accurate data into a blockchain so that Bitcoin users can
|
||||||
|
speculate in prediction markets.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
These markets have the potential to revolutionize the emergence of
|
These markets have the potential to revolutionize the emergence of
|
||||||
|
|
@ -17,8 +18,8 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This extension will become fully operative when the
|
This extension will become fully operative when the
|
||||||
<a href="https://drivechain.xyz/">BIP300</a> soft-fork gets activated and
|
<a class="text-secondary" href="https://drivechain.xyz/">BIP300</a>
|
||||||
Bitcoin Hivemind is launched.
|
soft-fork gets activated and Bitcoin Hivemind is launched.
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Invoices",
|
"name": "Invoices",
|
||||||
"short_description": "Create invoices for your clients.",
|
"short_description": "Create invoices for your clients.",
|
||||||
"icon": "request_quote",
|
"tile": "/invoices/static/image/invoices.png",
|
||||||
"contributors": ["leesalminen"]
|
"contributors": ["leesalminen"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from . import db
|
||||||
from .models import (
|
from .models import (
|
||||||
CreateInvoiceData,
|
CreateInvoiceData,
|
||||||
CreateInvoiceItemData,
|
CreateInvoiceItemData,
|
||||||
CreatePaymentData,
|
|
||||||
Invoice,
|
Invoice,
|
||||||
InvoiceItem,
|
InvoiceItem,
|
||||||
Payment,
|
Payment,
|
||||||
|
|
@ -30,7 +29,7 @@ async def get_invoice_items(invoice_id: str) -> List[InvoiceItem]:
|
||||||
return [InvoiceItem.from_row(row) for row in rows]
|
return [InvoiceItem.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice_item(item_id: str) -> InvoiceItem:
|
async def get_invoice_item(item_id: str) -> Optional[InvoiceItem]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM invoices.invoice_items WHERE id = ?", (item_id,)
|
"SELECT * FROM invoices.invoice_items WHERE id = ?", (item_id,)
|
||||||
)
|
)
|
||||||
|
|
@ -61,7 +60,7 @@ async def get_invoice_payments(invoice_id: str) -> List[Payment]:
|
||||||
return [Payment.from_row(row) for row in rows]
|
return [Payment.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_invoice_payment(payment_id: str) -> Payment:
|
async def get_invoice_payment(payment_id: str) -> Optional[Payment]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM invoices.payments WHERE id = ?", (payment_id,)
|
"SELECT * FROM invoices.payments WHERE id = ?", (payment_id,)
|
||||||
)
|
)
|
||||||
|
|
@ -120,7 +119,9 @@ async def create_invoice_items(
|
||||||
return invoice_items
|
return invoice_items
|
||||||
|
|
||||||
|
|
||||||
async def update_invoice_internal(wallet_id: str, data: UpdateInvoiceData) -> Invoice:
|
async def update_invoice_internal(
|
||||||
|
wallet_id: str, data: Union[UpdateInvoiceData, Invoice]
|
||||||
|
) -> Invoice:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE invoices.invoices
|
UPDATE invoices.invoices
|
||||||
|
|
@ -155,21 +156,21 @@ async def update_invoice_items(
|
||||||
updated_items.append(item.id)
|
updated_items.append(item.id)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE invoices.invoice_items
|
UPDATE invoices.invoice_items
|
||||||
SET description = ?, amount = ?
|
SET description = ?, amount = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(item.description, int(item.amount * 100), item.id),
|
(item.description, int(item.amount * 100), item.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
placeholders = ",".join("?" for i in range(len(updated_items)))
|
placeholders = ",".join("?" for _ in range(len(updated_items)))
|
||||||
if not placeholders:
|
if not placeholders:
|
||||||
placeholders = "?"
|
placeholders = "?"
|
||||||
updated_items = ("skip",)
|
updated_items = ["skip"]
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
DELETE FROM invoices.invoice_items
|
DELETE FROM invoices.invoice_items
|
||||||
WHERE invoice_id = ?
|
WHERE invoice_id = ?
|
||||||
AND id NOT IN ({placeholders})
|
AND id NOT IN ({placeholders})
|
||||||
""",
|
""",
|
||||||
|
|
@ -180,8 +181,11 @@ async def update_invoice_items(
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in data:
|
for item in data:
|
||||||
if not item.id:
|
if not item:
|
||||||
await create_invoice_items(invoice_id=invoice_id, data=[item])
|
await create_invoice_items(
|
||||||
|
invoice_id=invoice_id,
|
||||||
|
data=[CreateInvoiceItemData(description=item.description)],
|
||||||
|
)
|
||||||
|
|
||||||
invoice_items = await get_invoice_items(invoice_id)
|
invoice_items = await get_invoice_items(invoice_id)
|
||||||
return invoice_items
|
return invoice_items
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from enum import Enum
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||