Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41b08b3d84 | ||
|
|
e38cde98ae | ||
|
|
f6a9cdc578 | ||
|
|
f3b6b1949d | ||
|
|
e387ae3271 | ||
|
|
a7bce3a636 | ||
|
|
0532e8a733 | ||
|
|
2582b4ca17 | ||
|
|
ee7201dc58 | ||
|
|
57c64b104f | ||
|
|
d33735faa0 |
@@ -1,16 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
@@ -1,46 +0,0 @@
|
||||
FLASK_APP=lnbits
|
||||
FLASK_ENV=development
|
||||
|
||||
LNBITS_SITE_TITLE=LNbits
|
||||
LNBITS_ALLOWED_USERS="all"
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
LNBITS_DATA_FOLDER="/your_custom_data_folder"
|
||||
LNBITS_DISABLED_EXTENSIONS="amilk,events"
|
||||
LNBITS_FORCE_HTTPS=1
|
||||
LNBITS_SERVICE_FEE="0.0"
|
||||
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), LndRestWallet, CLightningWallet, LnbitsWallet
|
||||
LNBITS_BACKEND_WALLET_CLASS=LntxbotWallet
|
||||
|
||||
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||
|
||||
LNBITS_ENDPOINT=127.0.0.1:5000
|
||||
LNBITS_INVOICE_MACAROON=LNBITS_INVOICE_MACAROON
|
||||
LNBITS_ADMIN_MACAROON=LNBITS_ADMIN_MACAROON
|
||||
|
||||
LND_GRPC_ENDPOINT=127.0.0.1
|
||||
LND_GRPC_PORT=11009
|
||||
LND_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_ADMIN_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon"
|
||||
LND_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon"
|
||||
LND_READ_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/read.macaroon"
|
||||
|
||||
LND_REST_ENDPOINT=https://localhost:8080/
|
||||
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_REST_ADMIN_MACAROON="HEXSTRING"
|
||||
LND_REST_INVOICE_MACAROON="HEXSTRING"
|
||||
LND_REST_READ_MACAROON="HEXSTRING"
|
||||
|
||||
LNPAY_API_ENDPOINT=https://lnpay.co/v1/
|
||||
LNPAY_API_KEY=LNPAY_API_KEY
|
||||
LNPAY_ADMIN_KEY=LNPAY_ADMIN_KEY
|
||||
LNPAY_INVOICE_KEY=LNPAY_INVOICE_KEY
|
||||
LNPAY_READ_KEY=LNPAY_READ_KEY
|
||||
|
||||
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/
|
||||
LNTXBOT_ADMIN_KEY=LNTXBOT_ADMIN_KEY
|
||||
LNTXBOT_INVOICE_KEY=LNTXBOT_INVOICE_KEY
|
||||
|
||||
OPENNODE_API_ENDPOINT=https://api.opennode.com/
|
||||
OPENNODE_ADMIN_KEY=OPENNODE_ADMIN_KEY
|
||||
OPENNODE_INVOICE_KEY=OPENNODE_INVOICE_KEY
|
||||
@@ -1 +0,0 @@
|
||||
**/static/vendor/** linguist-vendored
|
||||
@@ -1 +0,0 @@
|
||||
custom: https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK
|
||||
@@ -1,54 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 12 * * 5'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Run Linters
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
mypy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Run MyPy python type checker
|
||||
uses: jpetrucciani/mypy-check@master
|
||||
with:
|
||||
path: 'lnbits'
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Check JS code formatting convention
|
||||
uses: creyD/prettier_action@v2.2
|
||||
with:
|
||||
dry: True
|
||||
prettier_options: --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**
|
||||
@@ -1,38 +0,0 @@
|
||||
name: test suite
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Test with pytest
|
||||
env:
|
||||
LNBITS_BACKEND_WALLET_CLASS: LNPayWallet
|
||||
LNBITS_FORCE_HTTPS: 0
|
||||
LNPAY_API_ENDPOINT: https://lnpay.co/v1/
|
||||
LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
|
||||
LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
|
||||
LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
|
||||
LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
|
||||
run: |
|
||||
pip install pytest pytest-cov
|
||||
pytest --cov=lnbits --cov-report=xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
@@ -1,31 +0,0 @@
|
||||
.DS_Store
|
||||
._*
|
||||
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.mypy_cache
|
||||
.vscode
|
||||
|
||||
*.egg
|
||||
*.egg-info
|
||||
.coverage
|
||||
.pytest_cache
|
||||
.webassets-cache
|
||||
htmlcov
|
||||
test-reports
|
||||
|
||||
*.swo
|
||||
*.swp
|
||||
*.pyo
|
||||
*.pyc
|
||||
*.env
|
||||
.env
|
||||
venv
|
||||
|
||||
*.sqlite3
|
||||
.pyre*
|
||||
|
||||
__bundle__
|
||||
|
||||
node_modules
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSpacing": false
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
FROM python:3.7
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir -q -r requirements.txt
|
||||
COPY . /app
|
||||
|
||||
EXPOSE 5000
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Arc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,2 +0,0 @@
|
||||
prettier:
|
||||
./node_modules/.bin/prettier --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**
|
||||
@@ -1,29 +0,0 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[packages]
|
||||
bitstring = "*"
|
||||
cerberus = "*"
|
||||
lnurl = "*"
|
||||
flask = "*"
|
||||
flask-assets = "*"
|
||||
flask-compress = "*"
|
||||
flask-cors = "*"
|
||||
flask-talisman = "*"
|
||||
gevent = "==20.5.0"
|
||||
gunicorn = "*"
|
||||
pyscss = "*"
|
||||
requests = "*"
|
||||
shortuuid = "*"
|
||||
|
||||
[dev-packages]
|
||||
black = "==19.10b0"
|
||||
flake8 = "*"
|
||||
flake8-mypy = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
@@ -1,657 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "76587c919e4a0d49f3a4ff297353ec8b8c92521effdb5eaf985cb32213ebc5c5"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"bech32": {
|
||||
"hashes": [
|
||||
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
|
||||
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"bitstring": {
|
||||
"hashes": [
|
||||
"sha256:fdf3eb72b229d2864fb507f8f42b1b2c57af7ce5fec035972f9566de440a864a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.7"
|
||||
},
|
||||
"brotli": {
|
||||
"hashes": [
|
||||
"sha256:0538dc1744fd17c314d2adc409ea7d1b779783b89fd95bcfb0c2acc93a6ea5a7",
|
||||
"sha256:0970a47f471782912d7705160b2b0a9306e68e6fadf9cffcaeb42d8f0951e26c",
|
||||
"sha256:113f51658e6fe548dce4b3749f6ef6c24de4184ba9c10a909cbee4261c2a5da0",
|
||||
"sha256:1e1aa9c4d1558889f42749c8baf846007953bfd32c8209230cf1cd1f5ef33495",
|
||||
"sha256:2f2f4f78f29ac4a45d15b3d9fc3fd9705e0ad313a44b129f6e1d0c6916bad0e2",
|
||||
"sha256:3269f6de1dd150fd0cce1c158b61ff5ac06d627fd3ae9c6ea03aed26fbbff7ea",
|
||||
"sha256:3f4a1f6240916c7984c7f2542786710f622992508dafee0b1714e6d340fb9ffd",
|
||||
"sha256:50dd9ad2a2bb12da4e9002a438672d182f98e546e99952de80280a1e1729664f",
|
||||
"sha256:5519a4b01b1a4f965083cbfa2ef2b9774c5a5f352341c47b50776ad109423d72",
|
||||
"sha256:5eb27722d320370315971c427eb8aa7cc0791f2a458840d357ac653bd0ad3a14",
|
||||
"sha256:5f06b4d5b6f58e5b5c220c2f23cad034dc5efa51b01fde2351ced1605bd980e2",
|
||||
"sha256:71ceee286ea7ec613f1c36f1c6181864a6ca24ebb55e371276f33d6af8742834",
|
||||
"sha256:72848d25a5f9e736db4af4512e0c3feecc094d57d241f8f1ae959115a2c39756",
|
||||
"sha256:743001bca75f4a6b4454be3510feca46f9d61a0c782a9bc2bc684bdb245e279e",
|
||||
"sha256:7ac98c71a15648fd11bc1f32608b6110e396121280790082e32b9a3109048bc6",
|
||||
"sha256:9d1c2dd27a1083fefd05b1b2f8df4a6bc2aaa6c21dd82cd41c8ae5e7c23a87f8",
|
||||
"sha256:a13ce9b419fe9f277c63f700efb0e444331509d1881b5610d2ba7e9080606967",
|
||||
"sha256:a19ef0952b9d2803df88dff07f45a6c92d5676afb9b8d69cf32232d684036d11",
|
||||
"sha256:ad766ca8b8c1419b71a22756b45264f45725c86133dc80a7cbe30b6b78c75620",
|
||||
"sha256:ad7963f261988ee0883816b6b9f206f11461c9b3cb5cfbca0c9ab5adc406d395",
|
||||
"sha256:af0451e23016631a2f52925a10d738ac4a0f794ac315c30380b22efc0c90cbc6",
|
||||
"sha256:c16201060c5a3f8742e3deae759014251ac92f382f82bc2a41dc079ff18c3f24",
|
||||
"sha256:c43b202f65891861a9a336984a103de25de235f756de69e32db893156f767013",
|
||||
"sha256:c675c6cce4295cb1a692f3de7416aacace7314e064b94bc86e93aceefce7fd3e",
|
||||
"sha256:d17cec0b992b1434f5f9df9986563605a4d1b1acd5574c87fc2ac014bcbd3316",
|
||||
"sha256:dc91f6129953861a73d9a65c52a8dd682b561a9ebaf65283541645cab6489917",
|
||||
"sha256:e2f4cbd1760d2bf2f30e396c2301999aab0191aec031a6a8a04950b2f575a536",
|
||||
"sha256:f192e6d3556714105c10486bbd6d045e38a0c04d9da3cef21e0a8dfd8e162df4",
|
||||
"sha256:f775b07026af2b1b0b5a8b05e41571cdcf3a315a67df265d60af301656a5425b",
|
||||
"sha256:f969ec7f56ba9636679e69ca07fba548312ccaca37412ee823c7f413541ad7e0",
|
||||
"sha256:f9dc52cd70907aafb99a773b66b156f2f995c7a0d284397c487c8b71ddbef2f9",
|
||||
"sha256:f9ee88bb52352588ceb811d045b5c9bb1dc38927bc150fd156244f60ff3f59f1",
|
||||
"sha256:fc7212e36ebeb81aebf7949c92897b622490d7c0e333a479c0395591e7994600"
|
||||
],
|
||||
"version": "==1.0.7"
|
||||
},
|
||||
"cerberus": {
|
||||
"hashes": [
|
||||
"sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
|
||||
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"flask-assets": {
|
||||
"hashes": [
|
||||
"sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2",
|
||||
"sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0"
|
||||
},
|
||||
"flask-compress": {
|
||||
"hashes": [
|
||||
"sha256:f367b2b46003dd62be34f7fb1379938032656dca56377a9bc90e7188e4289a7c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"flask-cors": {
|
||||
"hashes": [
|
||||
"sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
|
||||
"sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.8"
|
||||
},
|
||||
"flask-talisman": {
|
||||
"hashes": [
|
||||
"sha256:468131464a249274ed226efc21b372518f442487e58918ccab8357eaa638fd1f",
|
||||
"sha256:eaa754f4b771dfbe473843391d69643b79e3a38c865790011ac5e4179c68e3ec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"gevent": {
|
||||
"hashes": [
|
||||
"sha256:00b03601b8dd1ee2aa07811cb60a4befe36173b15d91c6e207e37f8d77dd6fac",
|
||||
"sha256:0acc15ba2ac2a555529ad82d5a28fc85dbb6b2ff947657d67bebfd352e2b5c14",
|
||||
"sha256:15eae3cd450dac7dae7f4ac59e01db1378965c9ef565c39c5ae78c5a888f9ac9",
|
||||
"sha256:1dc7f1f6bc1f67d625e4272b01e717eba0b4fa024d2ff7934c8d320674d6f7fa",
|
||||
"sha256:1dd95433be45e1115053878366e3f5332ae99c39cb345be23851327c062b9f4a",
|
||||
"sha256:28b7d83b4327ceb79668eca2049bf4b9ce66d5ace18a88335e3035b573f889fd",
|
||||
"sha256:31dc5d4ab8172cc00c4ff17cb18edee633babd961f64bf54214244d769bc3a74",
|
||||
"sha256:38db524ea88d81d596b2cbb6948fced26654a15fec40ea4529224e239a6f45e8",
|
||||
"sha256:3ff477b6d275396123faf8ce2d5b82f96d85ba264e0b9d4b56a2bac49d1b9adc",
|
||||
"sha256:4d2729dd4bf9c4d0f29482f53cdf9fc90a498aebb5cd7ae8b45d35657437d2ac",
|
||||
"sha256:52e5cd607749ed3b8aa0272cacf2c11deec61fca4c3bec57a9fea8c49316627d",
|
||||
"sha256:5c604179cebcc57f10505d8db177b92a715907815a464b066e7eba322d1c33ac",
|
||||
"sha256:88c76df4967c5229f853aa67ad1b394d9e4f985b0359c9bc9879416bba3e7c68",
|
||||
"sha256:929c33df8e9bcbe31906024fcd21580bd018196dbd3249eb5b2f19d63e11092d",
|
||||
"sha256:92edc18a357473e01a4e4a82c073ed3c99ceca6e3ce93c23668dd4a2401f07dc",
|
||||
"sha256:937d36730f2b0dee3387712074b1f15b802e2e074a3d7c6dcaf70521236d607c",
|
||||
"sha256:9b4e940fc6071afebb86ba5f48dbb5f1fc3cb96ebeb8cf145eb5b499e9c6ee33",
|
||||
"sha256:a7805934e8ce81610b61f806572c3d504cedd698cc8c9460d78d2893ba598c4a",
|
||||
"sha256:d07a2afe4215731eb57d5b257a2e7e7e170d8a7ae1f02f6d0682cd3403debea9",
|
||||
"sha256:e01d5373528e4ebdde66dc47a608d225fa3c4408ccd828d26c49b7ff75d82bd9",
|
||||
"sha256:efd9546468502a30ddd4699c3124ccb9d3099130f9b5ae1e2a54ad5b46e86120",
|
||||
"sha256:fcb64f3a28420d1b872b7ef41b12e8a1a4dcadfc8eff3c09993ab0cdf52584a1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.5.0"
|
||||
},
|
||||
"greenlet": {
|
||||
"hashes": [
|
||||
"sha256:1000038ba0ea9032948e2156a9c15f5686f36945e8f9906e6b8db49f358e7b52",
|
||||
"sha256:133ba06bad4e5f2f8bf6a0ac434e0fd686df749a86b3478903b92ec3a9c0c90b",
|
||||
"sha256:1429dc183b36ec972055e13250d96e174491559433eb3061691b446899b87384",
|
||||
"sha256:1b805231bfb7b2900a16638c3c8b45c694334c811f84463e52451e00c9412691",
|
||||
"sha256:3a35e33902b2e6079949feed7a2dafa5ac6f019da97bd255842bb22de3c11bf5",
|
||||
"sha256:5ea034d040e6ab1d2ae04ab05a3f37dbd719c4dee3804b13903d4cc794b1336e",
|
||||
"sha256:682328aa576ec393c1872615bcb877cf32d800d4a2f150e1a5dc7e56644010b1",
|
||||
"sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c",
|
||||
"sha256:7eed31f4efc8356e200568ba05ad645525f1fbd8674f1e5be61a493e715e3873",
|
||||
"sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872",
|
||||
"sha256:b0b2a984bbfc543d144d88caad6cc7ff4a71be77102014bd617bd88cfb038727",
|
||||
"sha256:c196a5394c56352e21cb7224739c6dd0075b69dd56f758505951d1d8d68cf8a9",
|
||||
"sha256:d83c1d38658b0f81c282b41238092ed89d8f93c6e342224ab73fb39e16848721",
|
||||
"sha256:df7de669cbf21de4b04a3ffc9920bc8426cab4c61365fa84d79bf97401a8bef7",
|
||||
"sha256:e5db19d4a7d41bbeb3dd89b49fc1bc7e6e515b51bbf32589c618655a0ebe0bf0",
|
||||
"sha256:e695ac8c3efe124d998230b219eb51afb6ef10524a50b3c45109c4b77a8a3a92",
|
||||
"sha256:eac2a3f659d5f41d6bbfb6a97733bc7800ea5e906dc873732e00cebb98cec9e4"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython'",
|
||||
"version": "==0.4.16"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
|
||||
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.0.4"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"lnurl": {
|
||||
"hashes": [
|
||||
"sha256:398fcb58b8112575bb24a445c61dda0e180df232717ccdbcb6a6932221cc6d16"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.4"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c",
|
||||
"sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e",
|
||||
"sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e",
|
||||
"sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1",
|
||||
"sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d",
|
||||
"sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9",
|
||||
"sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614",
|
||||
"sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99",
|
||||
"sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633",
|
||||
"sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73",
|
||||
"sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7",
|
||||
"sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b",
|
||||
"sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d",
|
||||
"sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df",
|
||||
"sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20",
|
||||
"sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1",
|
||||
"sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.6.1"
|
||||
},
|
||||
"pyscss": {
|
||||
"hashes": [
|
||||
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.7"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"shortuuid": {
|
||||
"hashes": [
|
||||
"sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f",
|
||||
"sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
|
||||
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
|
||||
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.7.4.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.25.10"
|
||||
},
|
||||
"webassets": {
|
||||
"hashes": [
|
||||
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
|
||||
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
|
||||
],
|
||||
"version": "==2.0"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||
],
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
|
||||
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.10b0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
|
||||
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
|
||||
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
|
||||
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
|
||||
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
|
||||
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
|
||||
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
|
||||
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
|
||||
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
|
||||
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
|
||||
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
|
||||
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
|
||||
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
|
||||
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
|
||||
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
|
||||
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
|
||||
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
|
||||
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
|
||||
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
|
||||
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
|
||||
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
|
||||
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
|
||||
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
|
||||
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
|
||||
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
|
||||
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
|
||||
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
|
||||
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
|
||||
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
|
||||
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
|
||||
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
|
||||
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
|
||||
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
|
||||
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==5.2.1"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
|
||||
"sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.8.3"
|
||||
},
|
||||
"flake8-mypy": {
|
||||
"hashes": [
|
||||
"sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18",
|
||||
"sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==17.8.0"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
|
||||
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||
"sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"
|
||||
],
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5",
|
||||
"sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==8.4.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c",
|
||||
"sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86",
|
||||
"sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b",
|
||||
"sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd",
|
||||
"sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc",
|
||||
"sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea",
|
||||
"sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e",
|
||||
"sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308",
|
||||
"sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406",
|
||||
"sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d",
|
||||
"sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707",
|
||||
"sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d",
|
||||
"sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c",
|
||||
"sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.782"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.4"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
|
||||
"sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
|
||||
],
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
||||
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4",
|
||||
"sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.0.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191",
|
||||
"sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.10.1"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
|
||||
"sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
|
||||
"sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
|
||||
"sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
|
||||
"sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
|
||||
"sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
|
||||
"sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
|
||||
"sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
|
||||
"sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
|
||||
"sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
|
||||
"sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
|
||||
"sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
|
||||
"sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
|
||||
"sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
|
||||
"sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
|
||||
"sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
|
||||
"sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
|
||||
"sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
|
||||
"sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
|
||||
"sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
|
||||
"sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
|
||||
],
|
||||
"version": "==2020.7.14"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
|
||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
|
||||
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
|
||||
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
|
||||
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
|
||||
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
|
||||
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
|
||||
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
|
||||
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
|
||||
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
|
||||
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
|
||||
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
|
||||
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
|
||||
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
|
||||
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
|
||||
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
|
||||
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
|
||||
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
|
||||
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
|
||||
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
|
||||
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
|
||||
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
|
||||
],
|
||||
"version": "==1.4.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
|
||||
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
|
||||
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.7.4.2"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,11 @@
|
||||
LNbits
|
||||
======
|
||||
# Cashu
|
||||
|
||||
[![github-actions-badge]][github-actions]
|
||||
[![codecov-badge]][codecov]
|
||||
[![license-badge]](LICENSE)
|
||||
## Create ecash mint for pegging in/out of ecash
|
||||
|
||||

|
||||
|
||||
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
|
||||
|
||||
Use [lnbits.com](https://lnbits.com), or run your own LNbits server!
|
||||
### Usage
|
||||
|
||||
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
|
||||
|
||||
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
|
||||
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
|
||||
* Part of a development stack via LNbits API
|
||||
* Fallback wallet for the LNURL scheme
|
||||
* Instant wallet for LN demonstrations
|
||||
|
||||
The wallet can run on top of any lightning-network funding source, currently there is support for LND, CLightning, Lntxbot, LNpay, OpenNode, with more being added regularily.
|
||||
|
||||
See [lnbits.org](https://lnbits.org) for more detailed documentation.
|
||||
|
||||
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
||||
|
||||
LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits!
|
||||
|
||||
## LNbits as an account system
|
||||
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come..
|
||||
|
||||

|
||||
|
||||
Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||
|
||||
(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats)
|
||||
|
||||

|
||||
|
||||
## LNbits as an LNURL-withdraw fallback
|
||||
|
||||
LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the [LNURL-withdraw](https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-withdraw.md).
|
||||
|
||||

|
||||
|
||||
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
|
||||
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds.
|
||||
|
||||

|
||||
|
||||
## LNbits as an insta-wallet
|
||||
|
||||
Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon).
|
||||
"Go to this website", has a lot less friction than "Download this app".
|
||||
|
||||

|
||||
|
||||
## Running LNbits locally
|
||||
|
||||
See the [development docs](docs/devs/installation.md) for details on installation and setup.
|
||||
|
||||
## Tip me
|
||||
|
||||
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||
|
||||
[github-actions]: https://github.com/lnbits/lnbits/actions
|
||||
[github-actions-badge]: https://github.com/lnbits/lnbits/workflows/test%20suite/badge.svg
|
||||
[codecov]: https://codecov.io/gh/lnbits/lnbits
|
||||
[codecov-badge]: https://codecov.io/gh/lnbits/lnbits/branch/master/graph/badge.svg
|
||||
[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
1. Enable extension
|
||||
2. Create a Mint
|
||||
3. Share wallet
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import asyncio
|
||||
|
||||
from environs import Env
|
||||
from fastapi import APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import catch_everything_and_restart
|
||||
|
||||
db = Database("ext_cashu")
|
||||
|
||||
|
||||
cashu_static_files = [
|
||||
{
|
||||
"path": "/cashu/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/cashu/static"),
|
||||
"name": "cashu_static",
|
||||
}
|
||||
]
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
|
||||
ledger = Ledger(
|
||||
db=db,
|
||||
seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"),
|
||||
derivation_path="0/0/0/1",
|
||||
)
|
||||
|
||||
cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"])
|
||||
|
||||
|
||||
def cashu_renderer():
|
||||
return template_renderer(["lnbits/extensions/cashu/templates"])
|
||||
|
||||
|
||||
from .tasks import startup_cashu_mint, wait_for_paid_invoices
|
||||
from .views import * # noqa: F401,F403
|
||||
from .views_api import * # noqa: F401,F403
|
||||
|
||||
|
||||
def cashu_start():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(catch_everything_and_restart(startup_cashu_mint))
|
||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Cashu",
|
||||
"short_description": "Ecash mint and wallet",
|
||||
"tile": "/cashu/static/image/cashu.png",
|
||||
"contributors": ["calle", "vlad", "arcbtc"],
|
||||
"hidden": false
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from . import db
|
||||
from .models import Cashu
|
||||
|
||||
|
||||
async def create_cashu(
|
||||
cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
|
||||
) -> Cashu:
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, keyset_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
cashu_id,
|
||||
wallet_id,
|
||||
data.name,
|
||||
data.tickershort,
|
||||
data.fraction,
|
||||
data.maxsats,
|
||||
data.coins,
|
||||
keyset_id,
|
||||
),
|
||||
)
|
||||
|
||||
cashu = await get_cashu(cashu_id)
|
||||
assert cashu, "Newly created cashu couldn't be retrieved"
|
||||
return cashu
|
||||
|
||||
|
||||
async def get_cashu(cashu_id) -> Optional[Cashu]:
|
||||
row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
||||
return Cashu(**row) if row else None
|
||||
|
||||
|
||||
async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM cashu.cashu WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
|
||||
return [Cashu(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_cashu(cashu_id) -> None:
|
||||
await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
||||
@@ -1,5 +0,0 @@
|
||||
_site
|
||||
.sass-cache
|
||||
.jekyll-cache
|
||||
.jekyll-metadata
|
||||
vendor
|
||||
@@ -1 +0,0 @@
|
||||
lnbits.org
|
||||
@@ -1,11 +0,0 @@
|
||||
title: "LNbits docs"
|
||||
|
||||
remote_theme: pmarsceill/just-the-docs
|
||||
logo: "/logos/lnbits-full.png"
|
||||
search_enabled: true
|
||||
url: https://lnbits.org
|
||||
aux_links:
|
||||
"LNbits on GitHub":
|
||||
- "//github.com/lnbits/lnbits"
|
||||
"lnbits.com":
|
||||
- "//lnbits.com"
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
parent: For developers
|
||||
title: API reference
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
|
||||
API reference
|
||||
=============
|
||||
|
||||
Coming soon...
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
title: For developers
|
||||
nav_order: 3
|
||||
has_children: true
|
||||
---
|
||||
|
||||
|
||||
For developers
|
||||
==============
|
||||
|
||||
Thanks for contributing :)
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
parent: For developers
|
||||
title: Making extensions
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
Making extensions
|
||||
=================
|
||||
|
||||
Start off by copying the example extension in `lnbits/extensions/example` into your own:
|
||||
```sh
|
||||
cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use dashes or anything; it doesn't like those.
|
||||
cd lnbits/extensions/mysuperplugin
|
||||
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
|
||||
```
|
||||
|
||||
Going over the example extension's structure:
|
||||
* views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.
|
||||
* views.py: The `/` path will show up as your plugin's home page in lnbits' UI. Other pages you can define yourself. The `templates` folder should explain itself in relation to this.
|
||||
* migrations.py: Create database tables for your plugin. They'll be created when you run `pipenv run flask migrate`.
|
||||
|
||||
... This document is a work-in-progress. Send pull requests if you get stuck, so others don't.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
parent: For developers
|
||||
title: Installation
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Download this repo and install the dependencies.
|
||||
|
||||
Application dependencies
|
||||
------------------------
|
||||
|
||||
The application uses [Pipenv][pipenv] to manage Python packages.
|
||||
While in development, you will need to install all dependencies:
|
||||
|
||||
```sh
|
||||
$ pipenv shell
|
||||
$ pipenv install --dev
|
||||
```
|
||||
If you wish to use a version of Python higher than 3.7:
|
||||
```sh
|
||||
$ pipenv --python 3.8 install --dev
|
||||
```
|
||||
You will need to set the variables in `.env.example`, and rename the file to `.env`.
|
||||
|
||||

|
||||
|
||||
You might also need to install additional packages, depending on the [backend wallets](../guide/wallets.md) you configured.
|
||||
E.g. when you want to use LND you have to `pipenv install lnd-grpc`.
|
||||
|
||||
Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
|
||||
|
||||
Running the server
|
||||
------------------
|
||||
|
||||
LNbits uses [Flask](http://flask.pocoo.org/) as an application server.
|
||||
|
||||
```sh
|
||||
$ pipenv run flask migrate
|
||||
$ pipenv run flask run
|
||||
```
|
||||
|
||||
There is an environment variable called `FLASK_ENV` that has to be set to `development`
|
||||
if you want to run Flask in debug mode with autoreload
|
||||
|
||||
[pipenv]: https://pipenv.pypa.io/
|
||||
|
||||
Frontend
|
||||
--------
|
||||
|
||||
The views are build using [Vue.js and Quasar](https://quasar.dev/start/how-to-use-vue).
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
title: Backend wallets
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
Backend wallets
|
||||
===============
|
||||
|
||||
LNbits can run on top of many lightning-network funding sources. Currently there is support for
|
||||
CLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularily.
|
||||
|
||||
A backend wallet can be configured using the following LNbits environment variables:
|
||||
|
||||
|
||||
### CLightning
|
||||
|
||||
Using this wallet requires the installation of the `pylightning` Python package.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
|
||||
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
|
||||
|
||||
### LND (gRPC)
|
||||
|
||||
Using this wallet requires the installation of the `lnd-grpc` Python package.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
|
||||
- `LND_GRPC_ENDPOINT`: ip_address
|
||||
- `LND_GRPC_PORT`: port
|
||||
- `LND_CERT`: /file/path/tls.cert
|
||||
- `LND_ADMIN_MACAROON`: /file/path/admin.macaroon
|
||||
- `LND_INVOICE_MACAROON`: /file/path/invoice.macaroon
|
||||
- `LND_READ_MACAROON`: /file/path/read.macaroon
|
||||
|
||||
|
||||
### LND (REST)
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
|
||||
- `LND_REST_ENDPOINT`: ip_address
|
||||
- `LND_CERT`: /file/path/tls.cert
|
||||
- `LND_ADMIN_MACAROON`: /file/path/admin.macaroon
|
||||
- `LND_INVOICE_MACAROON`: /file/path/invoice.macaroon
|
||||
- `LND_READ_MACAROON`: /file/path/read.macaroon
|
||||
|
||||
|
||||
### LNbits
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
|
||||
- `LNBITS_ENDPOINT`: ex. https://lnbits.com
|
||||
- `LNBITS_ADMIN_KEY`: apiKey
|
||||
- `LNBITS_INVOICE_KEY`: apiKey
|
||||
|
||||
|
||||
### LNPay
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
|
||||
- `LNPAY_API_ENDPOINT`: ex. https://lnpay.co/v1/
|
||||
- `LNPAY_API_KEY`: apiKey
|
||||
- `LNPAY_ADMIN_KEY`: apiKey
|
||||
- `LNPAY_INVOICE_KEY`: apiKey
|
||||
- `LNPAY_READ_KEY`: apiKey
|
||||
|
||||
|
||||
### lntxbot
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LntxbotWallet**
|
||||
- `LNTXBOT_API_ENDPOINT`: ex. https://lntxbot.bigsun.xyz/
|
||||
- `LNTXBOT_ADMIN_KEY`: apiKey
|
||||
- `LNTXBOT_INVOICE_KEY`: apiKey
|
||||
|
||||
|
||||
### OpenNode
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
||||
- `OPENNODE_API_ENDPOINT`: ex. https://api.opennode.com/
|
||||
- `OPENNODE_ADMIN_KEY`: apiKey
|
||||
- `OPENNODE_INVOICE_KEY`: apiKey
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
title: User’s Guide
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
|
||||
LNbits, free and open-source lightning-network wallet/accounts system
|
||||
=====================================================================
|
||||
|
||||
LNbits is a very simple Python application that sits on top of any funding source, and can be used as:
|
||||
|
||||
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
|
||||
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
|
||||
* Part of a development stack via LNbits API
|
||||
* Fallback wallet for the LNURL scheme
|
||||
* Instant wallet for LN demonstrations
|
||||
|
||||
|
||||
LNbits as an account system
|
||||
---------------------------
|
||||
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending,
|
||||
export to csv + more to come...
|
||||
|
Before Width: | Height: | Size: 69 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 101.041 30.0001"><defs><path id="a" d="M33.2619 148.1667h154.2143v68.7917H33.2619z"/></defs><g fill="#1f2234" aria-label="LNbits" font-family="sans-serif" font-size=".3095" font-weight="400" letter-spacing=".0031" style="line-height:1.25;white-space:pre;shape-inside:url(#a)" transform="matrix(72.4607 0 0 72.4607 -2399.2814 -10741.3589)"><g transform="matrix(.00244 0 0 .00244 33.0708 148.1594)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g><g fill="#eee" font-family="roboto"><path d="M33.6755 148.518h.0962v.0364h-.1416v-.22h.0454zM33.9796 148.5544h-.0453l-.0883-.1448v.1448h-.0453v-.22h.0453l.0884.145v-.145h.0452z" font-weight="700" style="-inkscape-font-specification:'roboto Bold'"/><path d="M34.1625 148.4744q0 .0375-.0172.0603-.0172.0227-.0462.0227-.031 0-.048-.0219l-.0014.019h-.0256v-.2322h.028v.0866q.0168-.021.0466-.021.0298 0 .0467.0225.017.0225.017.0617zm-.028-.003q0-.0286-.011-.044-.011-.0157-.0317-.0157-.0277 0-.0398.0257v.0707q.0129.0257.04.0257.0202 0 .0314-.0156.0112-.0156.0112-.0469zM34.231 148.5544h-.028v-.1635h.028zm-.0302-.2069q0-.007.0041-.0115.0042-.005.0124-.005.0082 0 .0124.005.0042.005.0042.0115 0 .007-.0042.0113-.0042.005-.0124.005-.0082 0-.0124-.005-.004-.005-.004-.0113zM34.3167 148.3513v.0396h.0305v.0216h-.0305v.1014q0 .01.004.0148.0042.005.014.005.0048 0 .0133-.002v.0227q-.011.003-.0215.003-.0187 0-.0283-.0113-.0095-.0113-.0095-.0322v-.1014h-.0297v-.0216h.0297v-.0396zM34.478 148.511q0-.0113-.0086-.0175-.0085-.006-.0298-.0109-.0211-.005-.0337-.0109-.0124-.006-.0184-.015t-.006-.0209q0-.02.017-.034.017-.0139.0435-.0139.0278 0 .045.0144.0174.0144.0174.0367h-.028q0-.0115-.0099-.0198-.0097-.008-.0245-.008-.0152 0-.0238.007-.0086.007-.0086.0174 0 .0101.008.0153.008.005.0288.01.021.005.034.0112.013.007.0192.0157.0063.009.0063.0222 0 .022-.0175.0352-.0175.0131-.0455.0131-.0197 0-.0348-.007-.015-.007-.0237-.0193-.0085-.0125-.0085-.027h.028q.0007.014.0111.0223.0106.008.0279.008.0158 0 .0253-.006.0097-.006.0097-.0172z" style="-inkscape-font-specification:roboto"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 71 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 101.041 30.0001"><defs><path id="a" d="M33.2619 148.1667h154.2143v68.7917H33.2619z"/></defs><g fill="#1f2234" aria-label="LNbits" font-family="sans-serif" font-size=".3095" font-weight="400" letter-spacing=".0031" style="line-height:1.25;white-space:pre;shape-inside:url(#a)" transform="matrix(72.4607 0 0 72.4607 -2399.2814 -10741.3589)"><g transform="matrix(.00244 0 0 .00244 33.0708 148.1594)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g><g font-family="roboto"><path d="M33.6755 148.518h.0962v.0364h-.1416v-.22h.0454zM33.9796 148.5544h-.0453l-.0883-.1448v.1448h-.0453v-.22h.0453l.0884.145v-.145h.0452z" font-weight="700" style="-inkscape-font-specification:'roboto Bold'"/><path d="M34.1625 148.4744q0 .0375-.0172.0603-.0172.0227-.0462.0227-.031 0-.048-.0219l-.0014.019h-.0256v-.2322h.028v.0866q.0168-.021.0466-.021.0298 0 .0467.0225.017.0225.017.0617zm-.028-.003q0-.0286-.011-.044-.011-.0157-.0317-.0157-.0277 0-.0398.0257v.0707q.0129.0257.04.0257.0202 0 .0314-.0156.0112-.0156.0112-.0469zM34.231 148.5544h-.028v-.1635h.028zm-.0302-.2069q0-.007.0041-.0115.0042-.005.0124-.005.0082 0 .0124.005.0042.005.0042.0115 0 .007-.0042.0113-.0042.005-.0124.005-.0082 0-.0124-.005-.004-.005-.004-.0113zM34.3167 148.3513v.0396h.0305v.0216h-.0305v.1014q0 .01.004.0148.0042.005.014.005.0048 0 .0133-.002v.0227q-.011.003-.0215.003-.0187 0-.0283-.0113-.0095-.0113-.0095-.0322v-.1014h-.0297v-.0216h.0297v-.0396zM34.478 148.511q0-.0113-.0086-.0175-.0085-.006-.0298-.0109-.0211-.005-.0337-.0109-.0124-.006-.0184-.015t-.006-.0209q0-.02.017-.034.017-.0139.0435-.0139.0278 0 .045.0144.0174.0144.0174.0367h-.028q0-.0115-.0099-.0198-.0097-.008-.0245-.008-.0152 0-.0238.007-.0086.007-.0086.0174 0 .0101.008.0153.008.005.0288.01.021.005.034.0112.013.007.0192.0157.0063.009.0063.0222 0 .022-.0175.0352-.0175.0131-.0455.0131-.0197 0-.0348-.007-.015-.007-.0237-.0193-.0085-.0125-.0085-.027h.028q.0007.014.0111.0223.0106.008.0279.008.0158 0 .0253-.006.0097-.006.0097-.0172z" style="-inkscape-font-specification:roboto"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 28 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><g transform="translate(-2.9464 -5.625) scale(.17717)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 314 B |
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCqdYhs1zok/3Es
|
||||
uSycCn0YKGXaXO9Vbhb1Aol1vzYc0UkQseDrGCiEF/0D5Vol6n3aqsZabKfQC46e
|
||||
ylEs+HWUYStsDApkSsyrlT+38nNYFtbCGNzf3lFqJxaOKrelCmhiYc3vzJc4yHpp
|
||||
OaWtuWDlx3bWnJSMi8VF6UkqAtZrlLTrx45Wd/X2lPh5V3hHbwQy+BJ0kGOJuNWO
|
||||
iMg4PUtaCVa8QQQSSdbXzLeMnkDXJRgGr6tEaFbZWCmQBOYIrmYcXTa4xSg+SaLa
|
||||
fBqGGY5CFa2mnGarhQ9I6/Or7KES4CgyIewuJSC4VZQ9WNFTeWRJmwF59LQIB1B+
|
||||
Su61Ds5vAgMBAAECggEAUTUJN4EZzEJ9smbNn9FHY1jrnSolQzfdbAA3aIcdyoDS
|
||||
JEf59UZfFv8upMqBNgsOxjwRn0agc/btjSypm5kKuxPH0gOyd7FC7IOABaxV0yOe
|
||||
UH3zzBtoAgB5OGWVc+UPcb9SkumPKZgo6TRzf94PEakDLp8ODh8QbowDNSaA7GL2
|
||||
FXvSgcxtLFv24/wMj7MPtUJDPPzhzRafe097RAJKi9ig33GpiohVg7HWxBEt/m2q
|
||||
WPJqV1izIYVuyUV1NN2qV/7KP35vqGu3sMyuKjmz33+7G5OUzxoixFp1hx1iQwNM
|
||||
QmzrNjKgGKPOvI169K73LtT6GfcAcVyvudPioJi2oQKBgQDlvEg79bXuV72OMkDI
|
||||
LINTk0Pncs5fJMhsPrbhvAbi8C9qH3j/e3uh4/f/RZVy8PZFe2YsmJo6cE60t445
|
||||
oejGpj0L9gB+ptWQa+6tJ0vO2DFrGz7siBDQDK08bBfE0FRAYYH1oy81oZihAwh/
|
||||
ypUWgxAAkFIDG1b8IYozUwlgaQKBgQC98mWG1ypNFDKU5ffFmxzCermXhe4sIOny
|
||||
E9Na51lMuzp1HVOHqYio27KGWB9NhFnhCRdzq2csJrdL5GqNBzF3mVc7Wp1QA9Iz
|
||||
GAN1BWYsObtffmePI73UOtdOf3vWRWt9xFDw07CuPUJoZLxCO8DE2R8TfBEZpMVx
|
||||
L99xZ4ldFwKBgCCCZk5l1Nrne6F1pwksk2DuQ/uqmV2JXPtEXKjQ7MCMcIk57uto
|
||||
U/RyyBNNi3p6cr8eWTUSWzDN17VnDC9HpCMIPzdEFonVnf8T5vCF0QpyOXwInPzn
|
||||
vElUoecc/LgTKvj5U2SpNPMH7QtL3JYI2tQWs9sAkTpzUfnX3TCqHXeRAoGAGeEJ
|
||||
phpwn/nNfm3qmRiuDW0tFhDgR1DgruO2mx8EEqk7qLMrK0OU4/Sy2QzujuAmYrub
|
||||
X14N+3z24jcnsTk2qyXQNbz9y+YJYH64Edx/dC0Vc0hEkcL73Qt7RC2oUKQ6pfL2
|
||||
WClvW/3qq1B2OcvsUqWGT2BK8CR75hTwq4HAvFcCgYB0Cuv/cHo0iLzvCX2LwIiq
|
||||
N38udO9oYa2VV/Ny4lf/oWFyLuL2syNJnW6fhqZk3ACVbIxQ4t85IJ+kKSKgSBXV
|
||||
9p3kAo93/sP+Qd3yWK/2kc8fwQXPQWhEynmS0c2LmfmA6h+JlPUG+HHhXwEMcplc
|
||||
iuvTCsY3KzPtBZgm8hFIKg==
|
||||
-----END PRIVATE KEY-----
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 828 B |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
background: #350a60;
|
||||
margin: 0;
|
||||
width: 380px;
|
||||
height: 595px;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe
|
||||
id="iframe"
|
||||
src="https://cashu.calle.wtf:5001/cashu/wallet?mint_id=W358Nzvmb7iuQpt6mXArKx"
|
||||
></iframe>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"description": "Cashu ecash wallet",
|
||||
"icons": {"128": "128.png", "32": "32.png", "16": "16.png"},
|
||||
"manifest_version": 3,
|
||||
"name": "Cashu",
|
||||
"version": "0.1",
|
||||
"action": {
|
||||
"default_icon": "32.png",
|
||||
"default_popup": "embedder.html",
|
||||
"default_title": "Cashu"
|
||||
},
|
||||
"permissions": [
|
||||
"unlimitedStorage",
|
||||
"storage"
|
||||
],
|
||||
"content_security_policy": {
|
||||
"extension_pages": "default-src 'self'"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import importlib
|
||||
|
||||
from flask import Flask
|
||||
from flask_assets import Environment, Bundle # type: ignore
|
||||
from flask_compress import Compress # type: ignore
|
||||
from flask_cors import CORS # type: ignore
|
||||
from flask_talisman import Talisman # type: ignore
|
||||
from os import getenv
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from .core import core_app, migrations as core_migrations
|
||||
from .helpers import ExtensionManager
|
||||
from .settings import FORCE_HTTPS
|
||||
|
||||
|
||||
disabled_extensions = getenv("LNBITS_DISABLED_EXTENSIONS", "").split(",")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) # type: ignore
|
||||
valid_extensions = [ext for ext in ExtensionManager(disabled=disabled_extensions).extensions if ext.is_valid]
|
||||
|
||||
|
||||
# optimization & security
|
||||
# -----------------------
|
||||
|
||||
Compress(app)
|
||||
CORS(app)
|
||||
Talisman(
|
||||
app,
|
||||
force_https=FORCE_HTTPS,
|
||||
content_security_policy={
|
||||
"default-src": [
|
||||
"'self'",
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
"blob:",
|
||||
"api.opennode.co",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# blueprints / extensions
|
||||
# -----------------------
|
||||
|
||||
app.register_blueprint(core_app)
|
||||
|
||||
for ext in valid_extensions:
|
||||
try:
|
||||
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
|
||||
app.register_blueprint(getattr(ext_module, f"{ext.code}_ext"), url_prefix=f"/{ext.code}")
|
||||
except Exception:
|
||||
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.")
|
||||
|
||||
|
||||
# filters
|
||||
# -------
|
||||
|
||||
app.jinja_env.globals["DEBUG"] = app.config["DEBUG"]
|
||||
app.jinja_env.globals["EXTENSIONS"] = valid_extensions
|
||||
app.jinja_env.globals["SITE_TITLE"] = getenv("LNBITS_SITE_TITLE", "LNbits")
|
||||
|
||||
|
||||
# assets
|
||||
# ------
|
||||
|
||||
assets = Environment(app)
|
||||
assets.url = app.static_url_path
|
||||
assets.register("base_css", Bundle("scss/base.scss", filters="pyscss", output="css/base.css"))
|
||||
|
||||
|
||||
# commands
|
||||
# --------
|
||||
|
||||
@app.cli.command("migrate")
|
||||
def migrate_databases():
|
||||
"""Creates the necessary databases if they don't exist already; or migrates them."""
|
||||
core_migrations.migrate()
|
||||
|
||||
for ext in valid_extensions:
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations")
|
||||
ext_migrations.migrate()
|
||||
except Exception:
|
||||
raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.")
|
||||
|
||||
|
||||
# init
|
||||
# ----
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dokku": {
|
||||
"predeploy": "flask migrate"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
# type: ignore
|
||||
|
||||
import bitstring
|
||||
import re
|
||||
from binascii import hexlify
|
||||
from bech32 import bech32_decode, CHARSET
|
||||
|
||||
|
||||
class Invoice(object):
|
||||
def __init__(self):
|
||||
self.payment_hash: str = None
|
||||
self.amount_msat: int = 0
|
||||
self.description: str = None
|
||||
|
||||
|
||||
def decode(pr: str) -> Invoice:
|
||||
""" Super naïve bolt11 decoder,
|
||||
only gets payment_hash, description/description_hash and amount in msatoshi.
|
||||
based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py
|
||||
"""
|
||||
hrp, data = bech32_decode(pr)
|
||||
if not hrp:
|
||||
raise ValueError("Bad bech32 checksum")
|
||||
|
||||
if not hrp.startswith("ln"):
|
||||
raise ValueError("Does not start with ln")
|
||||
|
||||
data = u5_to_bitarray(data)
|
||||
|
||||
# Final signature 65 bytes, split it off.
|
||||
if len(data) < 65 * 8:
|
||||
raise ValueError("Too short to contain signature")
|
||||
data = bitstring.ConstBitStream(data[: -65 * 8])
|
||||
|
||||
invoice = Invoice()
|
||||
|
||||
m = re.search("[^\d]+", hrp[2:])
|
||||
if m:
|
||||
amountstr = hrp[2 + m.end() :]
|
||||
if amountstr != "":
|
||||
invoice.amount_msat = unshorten_amount(amountstr)
|
||||
|
||||
# pull out date
|
||||
data.read(35).uint
|
||||
|
||||
while data.pos != data.len:
|
||||
tag, tagdata, data = pull_tagged(data)
|
||||
|
||||
data_length = len(tagdata) / 5
|
||||
|
||||
if tag == "d":
|
||||
invoice.description = trim_to_bytes(tagdata).decode("utf-8")
|
||||
elif tag == "h" and data_length == 52:
|
||||
invoice.description = hexlify(trim_to_bytes(tagdata)).decode("ascii")
|
||||
elif tag == "p" and data_length == 52:
|
||||
invoice.payment_hash = hexlify(trim_to_bytes(tagdata)).decode("ascii")
|
||||
|
||||
return invoice
|
||||
|
||||
|
||||
def unshorten_amount(amount: str) -> int:
|
||||
""" Given a shortened amount, return millisatoshis
|
||||
"""
|
||||
# BOLT #11:
|
||||
# The following `multiplier` letters are defined:
|
||||
#
|
||||
# * `m` (milli): multiply by 0.001
|
||||
# * `u` (micro): multiply by 0.000001
|
||||
# * `n` (nano): multiply by 0.000000001
|
||||
# * `p` (pico): multiply by 0.000000000001
|
||||
units = {
|
||||
"p": 10 ** 12,
|
||||
"n": 10 ** 9,
|
||||
"u": 10 ** 6,
|
||||
"m": 10 ** 3,
|
||||
}
|
||||
unit = str(amount)[-1]
|
||||
|
||||
# BOLT #11:
|
||||
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
|
||||
# anything except a `multiplier` in the table above.
|
||||
if not re.fullmatch("\d+[pnum]?", str(amount)):
|
||||
raise ValueError("Invalid amount '{}'".format(amount))
|
||||
|
||||
if unit in units:
|
||||
return int(amount[:-1]) * 100_000_000_000 / units[unit]
|
||||
else:
|
||||
return int(amount) * 100_000_000_000
|
||||
|
||||
|
||||
def pull_tagged(stream):
|
||||
tag = stream.read(5).uint
|
||||
length = stream.read(5).uint * 32 + stream.read(5).uint
|
||||
return (CHARSET[tag], stream.read(length * 5), stream)
|
||||
|
||||
|
||||
def trim_to_bytes(barr):
|
||||
# Adds a byte if necessary.
|
||||
b = barr.tobytes()
|
||||
if barr.len % 8 != 0:
|
||||
return b[:-1]
|
||||
return b
|
||||
|
||||
|
||||
def u5_to_bitarray(arr):
|
||||
ret = bitstring.BitArray()
|
||||
for a in arr:
|
||||
ret += bitstring.pack("uint:5", a)
|
||||
return ret
|
||||
@@ -1,9 +0,0 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
core_app: Blueprint = Blueprint("core", __name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
from .views.api import * # noqa
|
||||
from .views.generic import * # noqa
|
||||
from .views.lnurl import * # noqa
|
||||
@@ -1,213 +0,0 @@
|
||||
from typing import List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.db import open_db
|
||||
from lnbits.settings import DEFAULT_WALLET_NAME
|
||||
|
||||
from .models import User, Wallet, Payment
|
||||
|
||||
|
||||
# accounts
|
||||
# --------
|
||||
|
||||
|
||||
def create_account() -> User:
|
||||
with open_db() as db:
|
||||
user_id = uuid4().hex
|
||||
db.execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
|
||||
|
||||
new_account = get_account(user_id=user_id)
|
||||
assert new_account, "Newly created account couldn't be retrieved"
|
||||
|
||||
return new_account
|
||||
|
||||
|
||||
def get_account(user_id: str) -> Optional[User]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone("SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,))
|
||||
|
||||
return User(**row) if row else None
|
||||
|
||||
|
||||
def get_user(user_id: str) -> Optional[User]:
|
||||
with open_db() as db:
|
||||
user = db.fetchone("SELECT id, email FROM accounts WHERE id = ?", (user_id,))
|
||||
|
||||
if user:
|
||||
extensions = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,))
|
||||
wallets = db.fetchall(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE user = ?
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
return (
|
||||
User(**{**user, **{"extensions": [e[0] for e in extensions], "wallets": [Wallet(**w) for w in wallets]}})
|
||||
if user
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
def update_user_extension(*, user_id: str, extension: str, active: int) -> None:
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO extensions (user, extension, active)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(user_id, extension, active),
|
||||
)
|
||||
|
||||
|
||||
# wallets
|
||||
# -------
|
||||
|
||||
|
||||
def create_wallet(*, user_id: str, wallet_name: Optional[str] = None) -> Wallet:
|
||||
with open_db() as db:
|
||||
wallet_id = uuid4().hex
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO wallets (id, name, user, adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet_id, wallet_name or DEFAULT_WALLET_NAME, user_id, uuid4().hex, uuid4().hex),
|
||||
)
|
||||
|
||||
new_wallet = get_wallet(wallet_id=wallet_id)
|
||||
assert new_wallet, "Newly created wallet couldn't be retrieved"
|
||||
|
||||
return new_wallet
|
||||
|
||||
|
||||
def delete_wallet(*, user_id: str, wallet_id: str) -> None:
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE wallets AS w
|
||||
SET
|
||||
user = 'del:' || w.user,
|
||||
adminkey = 'del:' || w.adminkey,
|
||||
inkey = 'del:' || w.inkey
|
||||
WHERE id = ? AND user = ?
|
||||
""",
|
||||
(wallet_id, user_id),
|
||||
)
|
||||
|
||||
|
||||
def get_wallet(wallet_id: str) -> Optional[Wallet]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE id = ?
|
||||
""",
|
||||
(wallet_id,),
|
||||
)
|
||||
|
||||
return Wallet(**row) if row else None
|
||||
|
||||
|
||||
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone(
|
||||
f"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE adminkey = ? OR inkey = ?
|
||||
""",
|
||||
(key, key),
|
||||
)
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
if key_type == "admin" and row["adminkey"] != key:
|
||||
return None
|
||||
|
||||
return Wallet(**row)
|
||||
|
||||
|
||||
# wallet payments
|
||||
# ---------------
|
||||
|
||||
|
||||
def get_wallet_payment(wallet_id: str, checking_id: str) -> Optional[Payment]:
|
||||
with open_db() as db:
|
||||
row = db.fetchone(
|
||||
"""
|
||||
SELECT payhash as checking_id, amount, fee, pending, memo, time
|
||||
FROM apipayments
|
||||
WHERE wallet = ? AND payhash = ?
|
||||
""",
|
||||
(wallet_id, checking_id),
|
||||
)
|
||||
|
||||
return Payment(**row) if row else None
|
||||
|
||||
|
||||
def get_wallet_payments(wallet_id: str, *, include_all_pending: bool = False) -> List[Payment]:
|
||||
with open_db() as db:
|
||||
if include_all_pending:
|
||||
clause = "pending = 1"
|
||||
else:
|
||||
clause = "((amount > 0 AND pending = 0) OR amount < 0)"
|
||||
|
||||
rows = db.fetchall(
|
||||
f"""
|
||||
SELECT payhash as checking_id, amount, fee, pending, memo, time
|
||||
FROM apipayments
|
||||
WHERE wallet = ? AND {clause}
|
||||
ORDER BY time DESC
|
||||
""",
|
||||
(wallet_id,),
|
||||
)
|
||||
|
||||
return [Payment(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> None:
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
DELETE
|
||||
FROM apipayments WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ?
|
||||
""",
|
||||
(wallet_id, seconds),
|
||||
)
|
||||
|
||||
|
||||
# payments
|
||||
# --------
|
||||
|
||||
|
||||
def create_payment(
|
||||
*, wallet_id: str, checking_id: str, amount: int, memo: str, fee: int = 0, pending: bool = True
|
||||
) -> Payment:
|
||||
with open_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet_id, checking_id, amount, int(pending), memo, fee),
|
||||
)
|
||||
|
||||
new_payment = get_wallet_payment(wallet_id, checking_id)
|
||||
assert new_payment, "Newly created payment couldn't be retrieved"
|
||||
|
||||
return new_payment
|
||||
|
||||
|
||||
def update_payment_status(checking_id: str, pending: bool) -> None:
|
||||
with open_db() as db:
|
||||
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), checking_id,))
|
||||
|
||||
|
||||
def delete_payment(checking_id: str) -> None:
|
||||
with open_db() as db:
|
||||
db.execute("DELETE FROM apipayments WHERE payhash = ?", (checking_id,))
|
||||
@@ -1,75 +0,0 @@
|
||||
from lnbits.db import open_db
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
"""
|
||||
Initial LNbits tables.
|
||||
"""
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT,
|
||||
pass TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS extensions (
|
||||
user TEXT NOT NULL,
|
||||
extension TEXT NOT NULL,
|
||||
active BOOLEAN DEFAULT 0,
|
||||
|
||||
UNIQUE (user, extension)
|
||||
);
|
||||
"""
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
adminkey TEXT NOT NULL,
|
||||
inkey TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS apipayments (
|
||||
payhash TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
fee INTEGER NOT NULL DEFAULT 0,
|
||||
wallet TEXT NOT NULL,
|
||||
pending BOOLEAN NOT NULL,
|
||||
memo TEXT,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
|
||||
UNIQUE (wallet, payhash)
|
||||
);
|
||||
"""
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
CREATE VIEW IF NOT EXISTS balances AS
|
||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT wallet, SUM(amount) AS s -- incoming
|
||||
FROM apipayments
|
||||
WHERE amount > 0 AND pending = 0 -- don't sum pending
|
||||
GROUP BY wallet
|
||||
UNION ALL
|
||||
SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees
|
||||
FROM apipayments
|
||||
WHERE amount < 0 -- do sum pending
|
||||
GROUP BY wallet
|
||||
)
|
||||
GROUP BY wallet;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_db() as db:
|
||||
m001_initial(db)
|
||||
@@ -1,80 +0,0 @@
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
|
||||
class User(NamedTuple):
|
||||
id: str
|
||||
email: str
|
||||
extensions: List[str] = []
|
||||
wallets: List["Wallet"] = []
|
||||
password: Optional[str] = None
|
||||
|
||||
@property
|
||||
def wallet_ids(self) -> List[str]:
|
||||
return [wallet.id for wallet in self.wallets]
|
||||
|
||||
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
|
||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||
return w[0] if w else None
|
||||
|
||||
|
||||
class Wallet(NamedTuple):
|
||||
id: str
|
||||
name: str
|
||||
user: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
balance_msat: int
|
||||
|
||||
@property
|
||||
def balance(self) -> int:
|
||||
return self.balance // 1000
|
||||
|
||||
def get_payment(self, checking_id: str) -> Optional["Payment"]:
|
||||
from .crud import get_wallet_payment
|
||||
|
||||
return get_wallet_payment(self.id, checking_id)
|
||||
|
||||
def get_payments(self, *, include_all_pending: bool = False) -> List["Payment"]:
|
||||
from .crud import get_wallet_payments
|
||||
|
||||
return get_wallet_payments(self.id, include_all_pending=include_all_pending)
|
||||
|
||||
def delete_expired_payments(self, seconds: int = 86400) -> None:
|
||||
from .crud import delete_wallet_payments_expired
|
||||
|
||||
delete_wallet_payments_expired(self.id, seconds=seconds)
|
||||
|
||||
|
||||
class Payment(NamedTuple):
|
||||
checking_id: str
|
||||
pending: bool
|
||||
amount: int
|
||||
fee: int
|
||||
memo: str
|
||||
time: int
|
||||
|
||||
@property
|
||||
def msat(self) -> int:
|
||||
return self.amount
|
||||
|
||||
@property
|
||||
def sat(self) -> int:
|
||||
return self.amount // 1000
|
||||
|
||||
@property
|
||||
def is_in(self) -> bool:
|
||||
return self.amount > 0
|
||||
|
||||
@property
|
||||
def is_out(self) -> bool:
|
||||
return self.amount < 0
|
||||
|
||||
def set_pending(self, pending: bool) -> None:
|
||||
from .crud import update_payment_status
|
||||
|
||||
update_payment_status(self.checking_id, pending)
|
||||
|
||||
def delete(self) -> None:
|
||||
from .crud import delete_payment
|
||||
|
||||
delete_payment(self.checking_id)
|
||||
@@ -1,73 +0,0 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from lnbits.bolt11 import decode as bolt11_decode # type: ignore
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from .crud import get_wallet, create_payment, delete_payment
|
||||
|
||||
|
||||
def create_invoice(*, wallet_id: str, amount: int, memo: str) -> Tuple[str, str]:
|
||||
try:
|
||||
ok, checking_id, payment_request, error_message = WALLET.create_invoice(amount=amount, memo=memo)
|
||||
except Exception as e:
|
||||
ok, error_message = False, str(e)
|
||||
|
||||
if not ok:
|
||||
raise Exception(error_message or "Unexpected backend error.")
|
||||
|
||||
amount_msat = amount * 1000
|
||||
create_payment(wallet_id=wallet_id, checking_id=checking_id, amount=amount_msat, memo=memo)
|
||||
|
||||
return checking_id, payment_request
|
||||
|
||||
|
||||
def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) -> str:
|
||||
temp_id = f"temp_{urlsafe_short_hash()}"
|
||||
try:
|
||||
invoice = bolt11_decode(bolt11)
|
||||
|
||||
if invoice.amount_msat == 0:
|
||||
raise ValueError("Amountless invoices not supported.")
|
||||
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise ValueError("Amount in invoice is too high.")
|
||||
|
||||
fee_reserve = max(1000, int(invoice.amount_msat * 0.01))
|
||||
create_payment(
|
||||
wallet_id=wallet_id,
|
||||
checking_id=temp_id,
|
||||
amount=-invoice.amount_msat,
|
||||
fee=-fee_reserve,
|
||||
memo=temp_id,
|
||||
)
|
||||
|
||||
wallet = get_wallet(wallet_id)
|
||||
assert wallet, "invalid wallet id"
|
||||
if wallet.balance_msat < 0:
|
||||
raise PermissionError("Insufficient balance.")
|
||||
|
||||
ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(bolt11)
|
||||
|
||||
if ok:
|
||||
create_payment(
|
||||
wallet_id=wallet_id,
|
||||
checking_id=checking_id,
|
||||
amount=-invoice.amount_msat,
|
||||
fee=fee_msat,
|
||||
memo=invoice.description,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
ok, error_message = False, str(e)
|
||||
|
||||
delete_payment(temp_id)
|
||||
|
||||
if not ok:
|
||||
raise Exception(error_message or "Unexpected backend error.")
|
||||
|
||||
return checking_id
|
||||
|
||||
|
||||
def check_payment(*, checking_id: str) -> str:
|
||||
pass
|
||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,4 +0,0 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin]
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
disclaimerDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
walletName: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createWallet: function () {
|
||||
LNbits.href.createWallet(this.walletName)
|
||||
},
|
||||
processing: function () {
|
||||
this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Processing...',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,415 +0,0 @@
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
|
||||
function generateChart(canvas, payments) {
|
||||
var txs = []
|
||||
var n = 0
|
||||
var data = {
|
||||
labels: [],
|
||||
income: [],
|
||||
outcome: [],
|
||||
cumulative: []
|
||||
}
|
||||
|
||||
_.each(
|
||||
payments.slice(0).sort(function (a, b) {
|
||||
return a.time - b.time
|
||||
}),
|
||||
function (tx) {
|
||||
txs.push({
|
||||
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
|
||||
sat: tx.sat
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
_.each(_.groupBy(txs, 'hour'), function (value, day) {
|
||||
var income = _.reduce(
|
||||
value,
|
||||
function (memo, tx) {
|
||||
return tx.sat >= 0 ? memo + tx.sat : memo
|
||||
},
|
||||
0
|
||||
)
|
||||
var outcome = _.reduce(
|
||||
value,
|
||||
function (memo, tx) {
|
||||
return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo
|
||||
},
|
||||
0
|
||||
)
|
||||
n = n + income - outcome
|
||||
data.labels.push(day)
|
||||
data.income.push(income)
|
||||
data.outcome.push(outcome)
|
||||
data.cumulative.push(n)
|
||||
})
|
||||
|
||||
new Chart(canvas.getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data.cumulative,
|
||||
type: 'line',
|
||||
label: 'balance',
|
||||
backgroundColor: '#673ab7', // deep-purple
|
||||
borderColor: '#673ab7',
|
||||
borderWidth: 4,
|
||||
pointRadius: 3,
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
data: data.income,
|
||||
type: 'bar',
|
||||
label: 'in',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
|
||||
},
|
||||
{
|
||||
data: data.outcome,
|
||||
type: 'bar',
|
||||
label: 'out',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
text: 'Chart.js Combo Time Scale'
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
type: 'time',
|
||||
display: true,
|
||||
offset: true,
|
||||
time: {
|
||||
minUnit: 'hour',
|
||||
stepSize: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// performance tweaks
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
receive: {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null,
|
||||
data: {
|
||||
amount: null,
|
||||
memo: ''
|
||||
}
|
||||
},
|
||||
send: {
|
||||
show: false,
|
||||
invoice: null,
|
||||
data: {
|
||||
bolt11: ''
|
||||
}
|
||||
},
|
||||
sendCamera: {
|
||||
show: false,
|
||||
camera: 'auto'
|
||||
},
|
||||
payments: [],
|
||||
paymentsTable: {
|
||||
columns: [
|
||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: 'Date',
|
||||
field: 'date',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'sat',
|
||||
align: 'right',
|
||||
label: 'Amount (sat)',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
},
|
||||
filter: null
|
||||
},
|
||||
paymentsChart: {
|
||||
show: false
|
||||
},
|
||||
disclaimerDialog: {
|
||||
show: false,
|
||||
location: window.location
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPayments: function () {
|
||||
var q = this.paymentsTable.filter
|
||||
if (!q || q == '') return this.payments
|
||||
|
||||
return LNbits.utils.search(this.payments, q)
|
||||
},
|
||||
balance: function () {
|
||||
if (this.payments.length) {
|
||||
return (
|
||||
_.pluck(this.payments, 'amount').reduce(function (a, b) {
|
||||
return a + b
|
||||
}, 0) / 1000
|
||||
)
|
||||
}
|
||||
return this.g.wallet.sat
|
||||
},
|
||||
fbalance: function () {
|
||||
return LNbits.utils.formatSat(this.balance)
|
||||
},
|
||||
canPay: function () {
|
||||
if (!this.send.invoice) return false
|
||||
return this.send.invoice.sat <= this.balance
|
||||
},
|
||||
pendingPaymentsExist: function () {
|
||||
return this.payments
|
||||
? _.where(this.payments, {pending: 1}).length > 0
|
||||
: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeCamera: function () {
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
showCamera: function () {
|
||||
this.sendCamera.show = true
|
||||
},
|
||||
showChart: function () {
|
||||
this.paymentsChart.show = true
|
||||
this.$nextTick(function () {
|
||||
generateChart(this.$refs.canvas, this.payments)
|
||||
})
|
||||
},
|
||||
showReceiveDialog: function () {
|
||||
this.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: null,
|
||||
data: {
|
||||
amount: null,
|
||||
memo: ''
|
||||
},
|
||||
paymentChecker: null
|
||||
}
|
||||
},
|
||||
showSendDialog: function () {
|
||||
this.send = {
|
||||
show: true,
|
||||
invoice: null,
|
||||
data: {
|
||||
bolt11: ''
|
||||
},
|
||||
paymentChecker: null
|
||||
}
|
||||
},
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker
|
||||
setTimeout(function () {
|
||||
clearInterval(checker)
|
||||
}, 10000)
|
||||
},
|
||||
closeSendDialog: function () {
|
||||
this.sendCamera.show = false
|
||||
var checker = this.send.paymentChecker
|
||||
setTimeout(function () {
|
||||
clearInterval(checker)
|
||||
}, 1000)
|
||||
},
|
||||
createInvoice: function () {
|
||||
var self = this
|
||||
this.receive.status = 'loading'
|
||||
LNbits.api
|
||||
.createInvoice(
|
||||
this.g.wallet,
|
||||
this.receive.data.amount,
|
||||
this.receive.data.memo
|
||||
)
|
||||
.then(function (response) {
|
||||
self.receive.status = 'success'
|
||||
self.receive.paymentReq = response.data.payment_request
|
||||
|
||||
self.receive.paymentChecker = setInterval(function () {
|
||||
LNbits.api
|
||||
.getPayment(self.g.wallet, response.data.checking_id)
|
||||
.then(function (response) {
|
||||
if (response.data.paid) {
|
||||
self.fetchPayments()
|
||||
self.receive.show = false
|
||||
clearInterval(self.receive.paymentChecker)
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
self.receive.status = 'pending'
|
||||
})
|
||||
},
|
||||
decodeQR: function (res) {
|
||||
this.send.data.bolt11 = res
|
||||
this.decodeInvoice()
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
decodeInvoice: function () {
|
||||
if (this.send.data.bolt11.startsWith('lightning:')) {
|
||||
this.send.data.bolt11 = this.send.data.bolt11.slice(10)
|
||||
}
|
||||
|
||||
let invoice
|
||||
try {
|
||||
invoice = decode(this.send.data.bolt11)
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 3000,
|
||||
type: 'warning',
|
||||
message: error + '.',
|
||||
caption: '400 BAD REQUEST',
|
||||
icon: null
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let cleanInvoice = {
|
||||
msat: invoice.human_readable_part.amount,
|
||||
sat: invoice.human_readable_part.amount / 1000,
|
||||
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
|
||||
}
|
||||
|
||||
_.each(invoice.data.tags, function (tag) {
|
||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
if (tag.description == 'payment_hash') {
|
||||
cleanInvoice.hash = tag.value
|
||||
} else if (tag.description == 'description') {
|
||||
cleanInvoice.description = tag.value
|
||||
} else if (tag.description == 'expiry') {
|
||||
var expireDate = new Date(
|
||||
(invoice.data.time_stamp + tag.value) * 1000
|
||||
)
|
||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
expireDate,
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
)
|
||||
cleanInvoice.expired = false // TODO
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.send.invoice = Object.freeze(cleanInvoice)
|
||||
},
|
||||
payInvoice: function () {
|
||||
var self = this
|
||||
|
||||
dismissPaymentMsg = this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Processing payment...',
|
||||
icon: null
|
||||
})
|
||||
|
||||
LNbits.api
|
||||
.payInvoice(this.g.wallet, this.send.data.bolt11)
|
||||
.then(function (response) {
|
||||
self.send.paymentChecker = setInterval(function () {
|
||||
LNbits.api
|
||||
.getPayment(self.g.wallet, response.data.checking_id)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
self.send.show = false
|
||||
clearInterval(self.send.paymentChecker)
|
||||
dismissPaymentMsg()
|
||||
self.fetchPayments()
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
dismissPaymentMsg()
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteWallet: function (walletId, user) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||
.onOk(function () {
|
||||
LNbits.href.deleteWallet(walletId, user)
|
||||
})
|
||||
},
|
||||
fetchPayments: function (checkPending) {
|
||||
var self = this
|
||||
|
||||
return LNbits.api
|
||||
.getPayments(this.g.wallet, checkPending)
|
||||
.then(function (response) {
|
||||
self.payments = response.data
|
||||
.map(function (obj) {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return b.time - a.time
|
||||
})
|
||||
})
|
||||
},
|
||||
checkPendingPayments: function () {
|
||||
var dismissMsg = this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Checking pending transactions...',
|
||||
icon: null
|
||||
})
|
||||
|
||||
this.fetchPayments(true).then(function () {
|
||||
dismissMsg()
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
payments: function () {
|
||||
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance])
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.fetchPayments()
|
||||
setTimeout(this.checkPendingPayments(), 1200)
|
||||
},
|
||||
mounted: function () {
|
||||
if (
|
||||
this.$refs.disclaimer &&
|
||||
!this.$q.localStorage.getItem('lnbits.disclaimerShown')
|
||||
) {
|
||||
this.disclaimerDialog.show = true
|
||||
this.$q.localStorage.set('lnbits.disclaimerShown', true)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create an invoice (incoming)">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"out": false, "amount": <int>, "memo": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
|
||||
<code>{"checking_id": <string>, "payment_request": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false, "amount": <int>, "memo": <string>}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Pay an invoice (outgoing)">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": "{{ wallet.adminkey }}"}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"out": true, "bolt11": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
|
||||
<code>{"checking_id": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": true, "bolt11": <string>}' -H "X-Api-Key: {{ wallet.adminkey }}"</i> -H "Content-type: application/json"</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Check an invoice (incoming or outgoing)"
|
||||
class="q-pb-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-blue">GET</span> /api/v1/payments/<checking_id></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": "{{ wallet.inkey }}"}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200 OK (application/json)</h5>
|
||||
<code>{"paid": <bool>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X GET {{ request.url_root }}api/v1/payments/<checking_id> -H "X-Api-Key: {{ wallet.inkey }}"</i> -H "Content-type: application/json"</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
@@ -1,42 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
{% assets filters='rjsmin', output='__bundle__/core/extensions.js',
|
||||
'core/js/extensions.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-6 col-md-4 col-lg-3" v-for="extension in g.extensions" :key="extension.code">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-icon :name="extension.icon" color="grey-5" style="font-size: 4rem;"></q-icon>
|
||||
{% raw %}
|
||||
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
|
||||
{{ extension.shortDescription }}
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-actions>
|
||||
<div v-if="extension.isEnabled">
|
||||
<q-btn flat color="deep-purple"
|
||||
type="a" :href="[extension.url, '?usr=', g.user.id].join('')">Open</q-btn>
|
||||
<q-btn flat color="grey-5"
|
||||
type="a"
|
||||
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&disable=', extension.code].join('')"> Disable</q-btn>
|
||||
</div>
|
||||
<q-btn v-else flat color="deep-purple"
|
||||
type="a"
|
||||
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&enable=', extension.code].join('')">
|
||||
Enable</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,72 +0,0 @@
|
||||
{% extends "public.html" %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{% assets filters='rjsmin', output='__bundle__/core/index.js',
|
||||
'core/js/index.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
{% if lnurl %}
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
@click="processing"
|
||||
type="a" href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}">
|
||||
Press to claim bitcoin
|
||||
</q-btn>
|
||||
{% else %}
|
||||
<q-form @submit="createWallet" class="q-gutter-md">
|
||||
<q-input filled dense
|
||||
v-model="walletName"
|
||||
label="Name your LNbits wallet *"
|
||||
></q-input>
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="walletName == ''"
|
||||
type="submit">Add a new wallet</q-btn>
|
||||
</q-form>
|
||||
{% endif %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none"><strong>LN</strong>bits</h3>
|
||||
<h5 class="q-my-md">Free and open-source lightning wallet</h5>
|
||||
<p>Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself!</p>
|
||||
<p>You can run LNbits for yourself, or easily offer a custodian solution for others.</p>
|
||||
<p>Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool.</p>
|
||||
<p>Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.</p>
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn outline
|
||||
color="grey"
|
||||
type="a" href="https://github.com/lnbits/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
|
||||
<q-btn outline
|
||||
color="grey"
|
||||
type="a" href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK" target="_blank" rel="noopener">Donate</q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Ads -->
|
||||
<div class="col-12 col-md-3 col-lg-3">
|
||||
<div class="row q-col-gutter-lg justify-center">
|
||||
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn flat color="purple" label="Supporters" type="a" href="mailto:lnbits@pm.me" class="full-width"></q-btn>
|
||||
<a href="https://github.com/rootzoll/raspiblitz">
|
||||
<q-img contain :src="($q.dark.isActive) ? '{{ url_for('static', filename='images/raspiblitz-negative.svg') }}' : '{{ url_for('static', filename='images/raspiblitz.svg') }}'" style="height: 90px;"></q-img>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,259 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user, wallet) }}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
|
||||
'vendor/moment@2.25.1/moment.min.js',
|
||||
'vendor/chart.js@2.9.3/chart.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% assets filters='rjsmin', output='__bundle__/core/wallet.js',
|
||||
'vendor/bolt11/utils.js',
|
||||
'vendor/bolt11/decoder.js',
|
||||
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js',
|
||||
'core/js/wallet.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none"><strong>{% raw %}{{ fbalance }}{% endraw %}</strong> sat</h3>
|
||||
</q-card-section>
|
||||
<div class="row q-pb-md q-px-md q-col-gutter-md">
|
||||
<div class="col">
|
||||
<q-btn unelevated
|
||||
color="purple"
|
||||
class="full-width"
|
||||
@click="showSendDialog">Send</q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
class="full-width"
|
||||
@click="showReceiveDialog">Receive</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-sm">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Transactions</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
<!--<q-btn v-if="pendingPaymentsExist" dense flat round icon="update" color="grey" @click="checkPendingPayments">
|
||||
<q-tooltip>Check pending</q-tooltip>
|
||||
</q-btn>-->
|
||||
<q-btn dense flat round icon="show_chart" color="grey" @click="showChart">
|
||||
<q-tooltip>Show chart</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-input v-if="payments.length > 10" filled dense clearable
|
||||
v-model="paymentsTable.filter"
|
||||
debounce="300" placeholder="Search by memo, amount" class="q-mb-md">
|
||||
</q-input>
|
||||
<q-table dense flat
|
||||
:data="filteredPayments"
|
||||
row-key="payhash"
|
||||
:columns="paymentsTable.columns"
|
||||
:pagination.sync="paymentsTable.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">{{ col.label }}</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width class="lnbits__q-table__icon-td">
|
||||
<q-icon v-if="props.row.isPaid" size="14px"
|
||||
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'"
|
||||
:color="(props.row.sat < 0) ? 'pink' : 'green'"></q-icon>
|
||||
<q-icon v-else name="settings_ethernet" color="grey">
|
||||
<q-tooltip>Pending</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td key="memo" :props="props">
|
||||
{{ props.row.memo }}
|
||||
</q-td>
|
||||
<q-td auto-width key="date" :props="props">
|
||||
{{ props.row.date }}
|
||||
</q-td>
|
||||
<q-td auto-width key="sat" :props="props">
|
||||
{{ props.row.fsat }}
|
||||
</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>
|
||||
<q-btn flat color="grey" @click="exportCSV" class="float-right">Renew keys</q-btn>
|
||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">LNbits wallet</h6>
|
||||
<strong>Wallet name: </strong><em>{{ wallet.name }}</em><br>
|
||||
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br>
|
||||
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br>
|
||||
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "core/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="remove_circle"
|
||||
label="Delete wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>This whole wallet will be deleted, the funds will be <strong>UNRECOVERABLE</strong>.</p>
|
||||
<q-btn unelevated
|
||||
color="red-10"
|
||||
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')">Delete wallet</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||
<q-card v-if="!receive.paymentReq" class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="createInvoice" class="q-gutter-md">
|
||||
<q-input filled dense
|
||||
v-model.number="receive.data.amount"
|
||||
type="number"
|
||||
label="Amount (sat) *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.trim="receive.data.memo"
|
||||
label="Memo"
|
||||
placeholder="LNbits invoice"></q-input>
|
||||
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
||||
type="submit">Create invoice</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
<q-spinner v-if="receive.status == 'loading'" color="deep-purple" size="2.55em"></q-spinner>
|
||||
</q-form>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode :value="receive.paymentReq" :options="{width: 340}" class="rounded-borders"></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)">Copy invoice</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="send.show" position="top" @hide="closeSendDialog">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div v-if="!send.invoice">
|
||||
<q-form v-if="!sendCamera.show" @submit="decodeInvoice" class="q-gutter-md">
|
||||
<q-input filled dense
|
||||
v-model.trim="send.data.bolt11"
|
||||
type="textarea"
|
||||
label="Paste an invoice *">
|
||||
<template v-slot:after>
|
||||
<q-btn round dense flat icon="photo_camera" @click="showCamera">
|
||||
<q-tooltip>Use camera to scan an invoice</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="send.data.bolt11 == ''"
|
||||
type="submit">Read invoice</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
<div v-else>
|
||||
<q-responsive :ratio="1">
|
||||
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream>
|
||||
</q-responsive>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
{% raw %}
|
||||
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<p style="word-break: break-all">
|
||||
<strong>Memo:</strong> {{ send.invoice.description }}<br>
|
||||
<strong>Expire date:</strong> {{ send.invoice.expireDate }}<br>
|
||||
<strong>Hash:</strong> {{ send.invoice.hash }}
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div v-if="canPay" class="row q-mt-lg">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
@click="payInvoice">Send satoshis</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
<div v-else class="row q-mt-lg">
|
||||
<q-btn unelevated disabled color="yellow" text-color="black">Not enough funds!</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="paymentsChart.show" position="top">
|
||||
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||
<q-card-section>
|
||||
<canvas ref="canvas" width="600" height="400"></canvas>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% if service_fee > 0 %}
|
||||
<div ref="disclaimer"></div>
|
||||
<q-dialog v-model="disclaimerDialog.show">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-deep-purple">Warning</h6>
|
||||
<p>Login functionality to be released in v0.2, for now, <strong>make sure you bookmark this page for future access to your wallet</strong>!</p>
|
||||
<p>This service is in BETA, and we hold no responsibility for people losing access to funds. To encourage you to run your own LNbits installation, any balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will incur a charge of <strong>{{ service_fee }}% service fee</strong> per week.</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(disclaimerDialog.location.href)">Copy wallet URL</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">I understand</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,89 +0,0 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from ..services import create_invoice, pay_invoice
|
||||
|
||||
|
||||
@core_app.route("/api/v1/payments", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_payments():
|
||||
if "check_pending" in request.args:
|
||||
g.wallet.delete_expired_payments()
|
||||
|
||||
for payment in g.wallet.get_payments(include_all_pending=True):
|
||||
if payment.is_out:
|
||||
payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending)
|
||||
else:
|
||||
payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending)
|
||||
|
||||
return jsonify(g.wallet.get_payments()), HTTPStatus.OK
|
||||
|
||||
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"amount": {"type": "integer", "min": 1, "required": True},
|
||||
"memo": {"type": "string", "empty": False, "required": True},
|
||||
}
|
||||
)
|
||||
def api_payments_create_invoice():
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=g.wallet.id, amount=g.data["amount"], memo=g.data["memo"]
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}})
|
||||
def api_payments_pay_invoice():
|
||||
try:
|
||||
checking_id = pay_invoice(wallet_id=g.wallet.id, bolt11=g.data["bolt11"])
|
||||
except ValueError as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
|
||||
except PermissionError as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.FORBIDDEN
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id}), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@core_app.route("/api/v1/payments", methods=["POST"])
|
||||
@api_validate_post_request(schema={"out": {"type": "boolean", "required": True}})
|
||||
def api_payments_create():
|
||||
if g.data["out"] is True:
|
||||
return api_payments_pay_invoice()
|
||||
return api_payments_create_invoice()
|
||||
|
||||
|
||||
@core_app.route("/api/v1/payments/<checking_id>", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_payment(checking_id):
|
||||
payment = g.wallet.get_payment(checking_id)
|
||||
|
||||
if not payment:
|
||||
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
|
||||
elif not payment.pending:
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
try:
|
||||
if payment.is_out:
|
||||
is_paid = not WALLET.get_payment_status(checking_id).pending
|
||||
elif payment.is_in:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
if is_paid:
|
||||
payment.set_pending(False)
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
@@ -1,99 +0,0 @@
|
||||
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
|
||||
from http import HTTPStatus
|
||||
from os import getenv, path
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.settings import SERVICE_FEE
|
||||
|
||||
from ..crud import (
|
||||
create_account,
|
||||
get_user,
|
||||
update_user_extension,
|
||||
create_wallet,
|
||||
delete_wallet,
|
||||
)
|
||||
|
||||
|
||||
@core_app.route("/favicon.ico")
|
||||
def favicon():
|
||||
return send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico")
|
||||
|
||||
|
||||
@core_app.route("/")
|
||||
def home():
|
||||
return render_template("core/index.html", lnurl=request.args.get("lightning", None))
|
||||
|
||||
|
||||
@core_app.route("/extensions")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def extensions():
|
||||
extension_to_enable = request.args.get("enable", type=str)
|
||||
extension_to_disable = request.args.get("disable", type=str)
|
||||
|
||||
if extension_to_enable and extension_to_disable:
|
||||
abort(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
|
||||
|
||||
if extension_to_enable:
|
||||
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
|
||||
elif extension_to_disable:
|
||||
update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0)
|
||||
|
||||
return render_template("core/extensions.html", user=get_user(g.user.id))
|
||||
|
||||
|
||||
@core_app.route("/wallet")
|
||||
@validate_uuids(["usr", "wal"])
|
||||
def wallet():
|
||||
user_id = request.args.get("usr", type=str)
|
||||
wallet_id = request.args.get("wal", type=str)
|
||||
wallet_name = request.args.get("nme", type=str)
|
||||
service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE
|
||||
|
||||
# just wallet_name: create a new user, then create a new wallet for user with wallet_name
|
||||
# just user_id: return the first user wallet or create one if none found (with default wallet_name)
|
||||
# user_id and wallet_name: create a new wallet for user with wallet_name
|
||||
# user_id and wallet_id: return that wallet if user is the owner
|
||||
# nothing: create everything
|
||||
|
||||
if not user_id:
|
||||
user = get_user(create_account().id)
|
||||
else:
|
||||
user = get_user(user_id) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
|
||||
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
|
||||
|
||||
if allowed_users != "all" and user_id not in allowed_users.split(","):
|
||||
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.")
|
||||
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name:
|
||||
wallet = user.wallets[0]
|
||||
else:
|
||||
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
|
||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||
|
||||
if wallet_id not in user.wallet_ids:
|
||||
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
|
||||
return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee)
|
||||
|
||||
|
||||
@core_app.route("/deletewallet")
|
||||
@validate_uuids(["usr", "wal"], required=True)
|
||||
@check_user_exists()
|
||||
def deletewallet():
|
||||
wallet_id = request.args.get("wal", type=str)
|
||||
user_wallet_ids = g.user.wallet_ids
|
||||
|
||||
if wallet_id not in user_wallet_ids:
|
||||
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
else:
|
||||
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
|
||||
user_wallet_ids.remove(wallet_id)
|
||||
|
||||
if user_wallet_ids:
|
||||
return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0]))
|
||||
|
||||
return redirect(url_for("core.home"))
|
||||
@@ -1,61 +0,0 @@
|
||||
import requests
|
||||
|
||||
from flask import abort, redirect, request, url_for
|
||||
from http import HTTPStatus
|
||||
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore
|
||||
from lnurl.exceptions import LnurlException # type: ignore
|
||||
from time import sleep
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from ..crud import create_account, get_user, create_wallet, create_payment
|
||||
|
||||
|
||||
@core_app.route("/lnurlwallet")
|
||||
def lnurlwallet():
|
||||
memo = "LNbits LNURL funding"
|
||||
|
||||
try:
|
||||
withdraw_res = handle_lnurl(request.args.get("lightning"))
|
||||
if not withdraw_res.ok:
|
||||
abort(HTTPStatus.BAD_REQUEST, f"Could not process LNURL-withdraw: {withdraw_res.error_msg}")
|
||||
if not isinstance(withdraw_res, LnurlWithdrawResponse):
|
||||
abort(HTTPStatus.BAD_REQUEST, "Not a valid LNURL-withdraw.")
|
||||
except LnurlException:
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.")
|
||||
|
||||
try:
|
||||
ok, checking_id, payment_request, error_message = WALLET.create_invoice(withdraw_res.max_sats, memo)
|
||||
except Exception as e:
|
||||
ok, error_message = False, str(e)
|
||||
|
||||
if not ok:
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, error_message)
|
||||
|
||||
r = requests.get(
|
||||
withdraw_res.callback.base,
|
||||
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}},
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.")
|
||||
|
||||
for i in range(10):
|
||||
invoice_status = WALLET.get_invoice_status(checking_id)
|
||||
sleep(i)
|
||||
if not invoice_status.paid:
|
||||
continue
|
||||
break
|
||||
|
||||
user = get_user(create_account().id)
|
||||
wallet = create_wallet(user_id=user.id)
|
||||
create_payment(
|
||||
wallet_id=wallet.id,
|
||||
checking_id=checking_id,
|
||||
amount=withdraw_res.max_sats * 1000,
|
||||
memo=memo,
|
||||
pending=invoice_status.pending,
|
||||
)
|
||||
|
||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,42 +0,0 @@
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from .settings import LNBITS_DATA_FOLDER
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path: str):
|
||||
self.path = db_path
|
||||
self.connection = sqlite3.connect(db_path)
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
self.cursor = self.connection.cursor()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.cursor.close()
|
||||
self.connection.close()
|
||||
|
||||
def fetchall(self, query: str, values: tuple = ()) -> list:
|
||||
"""Given a query, return cursor.fetchall() rows."""
|
||||
self.cursor.execute(query, values)
|
||||
return self.cursor.fetchall()
|
||||
|
||||
def fetchone(self, query: str, values: tuple = ()):
|
||||
self.cursor.execute(query, values)
|
||||
return self.cursor.fetchone()
|
||||
|
||||
def execute(self, query: str, values: tuple = ()) -> None:
|
||||
"""Given a query, cursor.execute() it."""
|
||||
self.cursor.execute(query, values)
|
||||
self.connection.commit()
|
||||
|
||||
|
||||
def open_db(db_name: str = "database") -> Database:
|
||||
db_path = os.path.join(LNBITS_DATA_FOLDER, f"{db_name}.sqlite3")
|
||||
return Database(db_path=db_path)
|
||||
|
||||
|
||||
def open_ext_db(extension_name: str) -> Database:
|
||||
return open_db(f"ext_{extension_name}")
|
||||
@@ -1,88 +0,0 @@
|
||||
from cerberus import Validator # type: ignore
|
||||
from flask import g, abort, jsonify, request
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
from os import getenv
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
|
||||
|
||||
def api_check_wallet_key(key_type: str = "invoice"):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
try:
|
||||
g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type)
|
||||
except KeyError:
|
||||
return jsonify({"message": "`X-Api-Key` header missing."}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
if not g.wallet:
|
||||
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def api_validate_post_request(*, schema: dict):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
if "application/json" not in request.headers["Content-Type"]:
|
||||
return jsonify({"message": "Content-Type must be `application/json`."}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
v = Validator(schema)
|
||||
g.data = {key: (request.json[key] if key in request.json else None) for key in schema.keys()}
|
||||
|
||||
if not v.validate(g.data):
|
||||
return jsonify({"message": f"Errors in request data: {v.errors}"}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def check_user_exists(param: str = "usr"):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
|
||||
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
|
||||
|
||||
if allowed_users != "all" and g.user.id not in allowed_users.split(","):
|
||||
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
query_params = {param: request.args.get(param, type=str) for param in params}
|
||||
|
||||
for param, value in query_params.items():
|
||||
if not value and (required is True or (required and param in required)):
|
||||
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.")
|
||||
|
||||
if value:
|
||||
try:
|
||||
UUID(value, version=version)
|
||||
except ValueError:
|
||||
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
@@ -1,11 +0,0 @@
|
||||
<h1>Example Extension</h1>
|
||||
<h2>*tagline*</h2>
|
||||
This is an example extension to help you organise and build you own.
|
||||
|
||||
Try to include an image
|
||||
<img src="https://i.imgur.com/9i4xcQB.png">
|
||||
|
||||
|
||||
<h2>If your extension has API endpoints, include useful ones here</h2>
|
||||
|
||||
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
||||
@@ -1,8 +0,0 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
amilk_ext: Blueprint = Blueprint("amilk", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "AMilk",
|
||||
"short_description": "Assistant Faucet Milker",
|
||||
"icon": "room_service",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
from base64 import urlsafe_b64encode
|
||||
from uuid import uuid4
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
from .models import AMilk
|
||||
|
||||
|
||||
def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) -> AMilk:
|
||||
with open_ext_db("amilk") as db:
|
||||
amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO amilks (id, wallet, lnurl, atime, amount)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(amilk_id, wallet_id, lnurl, atime, amount),
|
||||
)
|
||||
|
||||
return get_amilk(amilk_id)
|
||||
|
||||
|
||||
def get_amilk(amilk_id: str) -> Optional[AMilk]:
|
||||
with open_ext_db("amilk") as db:
|
||||
row = db.fetchone("SELECT * FROM amilks WHERE id = ?", (amilk_id,))
|
||||
|
||||
return AMilk(**row) if row else None
|
||||
|
||||
|
||||
def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("amilk") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM amilks WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [AMilk(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_amilk(amilk_id: str) -> None:
|
||||
with open_ext_db("amilk") as db:
|
||||
db.execute("DELETE FROM amilks WHERE id = ?", (amilk_id,))
|
||||
@@ -1,21 +0,0 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
"""
|
||||
Initial amilks table.
|
||||
"""
|
||||
db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS amilks (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
lnurl TEXT NOT NULL,
|
||||
atime INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("amilk") as db:
|
||||
m001_initial(db)
|
||||
@@ -1,9 +0,0 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class AMilk(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
lnurl: str
|
||||
atime: int
|
||||
amount: int
|
||||
@@ -1,24 +0,0 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
|
||||
<p>
|
||||
Milking faucets with software, known as "assmilking", seems at first to
|
||||
be black-hat, although in fact there might be some unexplored use cases.
|
||||
An LNURL withdraw gives someone the right to pull funds, which can be
|
||||
done over time. An LNURL withdraw could be used outside of just faucets,
|
||||
to provide money streaming and repeat payments.<br />Paste or scan an
|
||||
LNURL withdraw, enter the amount for the AMilk to pull and the frequency
|
||||
for it to be pulled.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
@@ -1,252 +0,0 @@
|
||||
{% 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="deep-purple" @click="amilkDialog.show = true"
|
||||
>New AMilk</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">AMilks</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="amilks"
|
||||
row-key="id"
|
||||
:columns="amilksTable.columns"
|
||||
:pagination.sync="amilksTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteAMilk(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-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
LNbits Assistant Faucet Milker Extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "amilk/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="amilkDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="createAMilk" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="amilkDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="amilkDialog.data.lnurl"
|
||||
type="url"
|
||||
label="LNURL Withdraw"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="amilkDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount *"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="amilkDialog.data.atime"
|
||||
type="number"
|
||||
label="Hit frequency (secs)"
|
||||
placeholder="Frequency to be hit"
|
||||
></q-input>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
|
||||
type="submit"
|
||||
>Create amilk</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapAMilk = 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.wall = ['/amilk/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
amilks: [],
|
||||
amilksTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
|
||||
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
|
||||
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
amilkDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAMilks: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/amilk/api/v1/amilk?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks = response.data.map(function (obj) {
|
||||
response.data.forEach(MILK)
|
||||
function MILK(item) {
|
||||
window.setInterval(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/amilk/api/v1/amilk/milk/' + item.id,
|
||||
'Lorem'
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks = response.data.map(function (obj) {
|
||||
return mapAMilk(obj)
|
||||
})
|
||||
})
|
||||
}, item.atime * 1000)
|
||||
}
|
||||
return mapAMilk(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
createAMilk: function () {
|
||||
var data = {
|
||||
lnurl: this.amilkDialog.data.lnurl,
|
||||
atime: parseInt(this.amilkDialog.data.atime),
|
||||
amount: this.amilkDialog.data.amount
|
||||
}
|
||||
var self = this
|
||||
|
||||
console.log(this.amilkDialog.data.wallet)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/amilk/api/v1/amilk',
|
||||
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet})
|
||||
.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks.push(mapAMilk(response.data))
|
||||
self.amilkDialog.show = false
|
||||
self.amilkDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteAMilk: function (amilkId) {
|
||||
var self = this
|
||||
var amilk = _.findWhere(this.amilks, {id: amilkId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this AMilk link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/amilk/api/v1/amilks/' + amilkId,
|
||||
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.amilks = _.reject(self.amilks, function (obj) {
|
||||
return obj.id == amilkId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getAMilks()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,21 +0,0 @@
|
||||
from flask import g, abort, render_template
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.amilk import amilk_ext
|
||||
|
||||
from .crud import get_amilk
|
||||
|
||||
|
||||
@amilk_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
return render_template("amilk/index.html", user=g.user)
|
||||
|
||||
|
||||
@amilk_ext.route("/<amilk_id>")
|
||||
def wall(amilk_id):
|
||||
amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.")
|
||||
|
||||
return render_template("amilk/wall.html", amilk=amilk)
|
||||
@@ -1,94 +0,0 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from lnbits.extensions.amilk import amilk_ext
|
||||
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
|
||||
from lnbits.core.services import create_invoice
|
||||
|
||||
from flask import abort, redirect, request, url_for
|
||||
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl
|
||||
from lnurl.exceptions import LnurlException
|
||||
from time import sleep
|
||||
import requests
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
@amilk_ext.route("/api/v1/amilk", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_amilks():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
|
||||
def api_amilkit(amilk_id):
|
||||
milk = get_amilk(amilk_id)
|
||||
memo = milk.id
|
||||
|
||||
try:
|
||||
withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse)
|
||||
except LnurlException:
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
print(withdraw_res.max_sats)
|
||||
|
||||
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo)
|
||||
#print(payment_request)
|
||||
except Exception as e:
|
||||
error_message = False, str(e)
|
||||
|
||||
r = requests.get(
|
||||
withdraw_res.callback.base,
|
||||
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}},
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
|
||||
for i in range(10):
|
||||
invoice_status = WALLET.get_invoice_status(checking_id)
|
||||
sleep(i)
|
||||
if not invoice_status.paid:
|
||||
continue
|
||||
else:
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
break
|
||||
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
|
||||
@amilk_ext.route("/api/v1/amilk", methods=["POST"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(schema={
|
||||
"lnurl": {"type": "string", "empty": False, "required": True},
|
||||
"atime": {"type": "integer", "min": 0, "required": True},
|
||||
"amount": {"type": "integer", "min": 0, "required": True},
|
||||
})
|
||||
def api_amilk_create():
|
||||
amilk = create_amilk(wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"])
|
||||
|
||||
return jsonify(amilk._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_amilk_delete(amilk_id):
|
||||
amilk = get_amilk(amilk_id)
|
||||
|
||||
if not amilk:
|
||||
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if amilk.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_amilk(amilk_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
@@ -1,10 +0,0 @@
|
||||
<h1>Diagon Alley</h1>
|
||||
<h2>A movable market stand</h2>
|
||||
Make a list of products to sell, point the list to an indexer (or many), stack sats.
|
||||
Diagon Alley is a movable market stand, for anon transactions. You then give permission for an indexer to list those products. Delivery addresses are sent through the Lightning Network.
|
||||
<img src="https://i.imgur.com/P1tvBSG.png">
|
||||
|
||||
|
||||
<h2>API endpoints</h2>
|
||||
|
||||
<code>curl -X GET http://YOUR-TOR-ADDRESS</code>
|
||||
@@ -1,8 +0,0 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
diagonalley_ext: Blueprint = Blueprint("diagonalley", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Diagon Alley",
|
||||
"short_description": "Movable anonymous market stand",
|
||||
"icon": "add_shopping_cart",
|
||||
"contributors": ["benarc"]
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
from base64 import urlsafe_b64encode
|
||||
from uuid import uuid4
|
||||
from typing import List, Optional, Union
|
||||
import requests
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.settings import WALLET
|
||||
from .models import Products, Orders, Indexers
|
||||
import re
|
||||
regex = re.compile(
|
||||
r'^(?:http|ftp)s?://' # http:// or https://
|
||||
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
|
||||
r'localhost|'
|
||||
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
|
||||
r'(?::\d+)?'
|
||||
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
||||
|
||||
###Products
|
||||
|
||||
def create_diagonalleys_product(*, wallet_id: str, product: str, categories: str, description: str, image: str, price: int, quantity: int) -> Products:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
product_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO products (id, wallet, product, categories, description, image, price, quantity)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(product_id, wallet_id, product, categories, description, image, price, quantity),
|
||||
)
|
||||
|
||||
return get_diagonalleys_product(product_id)
|
||||
|
||||
|
||||
def update_diagonalleys_product(product_id: str, **kwargs) -> Optional[Indexers]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute(f"UPDATE products SET {q} WHERE id = ?", (*kwargs.values(), product_id))
|
||||
row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,))
|
||||
|
||||
return get_diagonalleys_indexer(product_id)
|
||||
|
||||
|
||||
def get_diagonalleys_product(product_id: str) -> Optional[Products]:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,))
|
||||
|
||||
return Products(**row) if row else None
|
||||
|
||||
|
||||
def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Products]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("diagonalley") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM products WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Products(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_diagonalleys_product(product_id: str) -> None:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("DELETE FROM products WHERE id = ?", (product_id,))
|
||||
|
||||
|
||||
|
||||
|
||||
###Indexers
|
||||
|
||||
def create_diagonalleys_indexer(wallet_id: str, shopname: str, indexeraddress: str, shippingzone1: str, shippingzone2: str, zone1cost: int, zone2cost: int, email: str) -> Indexers:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
indexer_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO indexers (id, wallet, shopname, indexeraddress, online, rating, shippingzone1, shippingzone2, zone1cost, zone2cost, email)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(indexer_id, wallet_id, shopname, indexeraddress, False, 0, shippingzone1, shippingzone2, zone1cost, zone2cost, email),
|
||||
)
|
||||
return get_diagonalleys_indexer(indexer_id)
|
||||
|
||||
|
||||
def update_diagonalleys_indexer(indexer_id: str, **kwargs) -> Optional[Indexers]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute(f"UPDATE indexers SET {q} WHERE id = ?", (*kwargs.values(), indexer_id))
|
||||
row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
|
||||
|
||||
return get_diagonalleys_indexer(indexer_id)
|
||||
|
||||
|
||||
def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
roww = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
|
||||
try:
|
||||
x = requests.get(roww["indexeraddress"] + "/" + roww["ratingkey"])
|
||||
if x.status_code == 200:
|
||||
print(x)
|
||||
print("poo")
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (True, indexer_id,))
|
||||
else:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (False, indexer_id,))
|
||||
except:
|
||||
print("An exception occurred")
|
||||
with open_ext_db("diagonalley") as db:
|
||||
row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
|
||||
return Indexers(**row) if row else None
|
||||
|
||||
|
||||
def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexers]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("diagonalley") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
for r in rows:
|
||||
try:
|
||||
x = requests.get(r["indexeraddress"] + "/" + r["ratingkey"])
|
||||
if x.status_code == 200:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (True, r["id"],))
|
||||
else:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (False, r["id"],))
|
||||
except:
|
||||
print("An exception occurred")
|
||||
with open_ext_db("diagonalley") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
return [Indexers(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_diagonalleys_indexer(indexer_id: str) -> None:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("DELETE FROM indexers WHERE id = ?", (indexer_id,))
|
||||
|
||||
|
||||
|
||||
###Orders
|
||||
|
||||
def create_diagonalleys_order(*, productid: str, wallet: str, product: str, quantity: int, shippingzone: str, address: str, email: str, invoiceid: str, paid: bool, shipped: bool) -> Indexers:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
order_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(order_id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, False, False),
|
||||
)
|
||||
|
||||
return get_diagonalleys_order(order_id)
|
||||
|
||||
|
||||
def get_diagonalleys_order(order_id: str) -> Optional[Orders]:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
row = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
|
||||
|
||||
return Orders(**row) if row else None
|
||||
|
||||
|
||||
def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("diagonalley") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
for r in rows:
|
||||
PAID = WALLET.get_invoice_status(r["invoiceid"]).paid
|
||||
if PAID:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE orders SET paid = ? WHERE id = ?", (True, r["id"],))
|
||||
rows = db.fetchall(f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
return [Orders(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_diagonalleys_order(order_id: str) -> None:
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("DELETE FROM orders WHERE id = ?", (order_id,))
|
||||
@@ -1,64 +0,0 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
"""
|
||||
Initial products table.
|
||||
"""
|
||||
db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
product TEXT NOT NULL,
|
||||
categories TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
image TEXT NOT NULL,
|
||||
price INTEGER NOT NULL,
|
||||
quantity INTEGER NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
"""
|
||||
Initial indexers table.
|
||||
"""
|
||||
db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS indexers (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
shopname TEXT NOT NULL,
|
||||
indexeraddress TEXT NOT NULL,
|
||||
online BOOLEAN NOT NULL,
|
||||
rating INTEGER NOT NULL,
|
||||
shippingzone1 TEXT NOT NULL,
|
||||
shippingzone2 TEXT NOT NULL,
|
||||
zone1cost INTEGER NOT NULL,
|
||||
zone2cost INTEGER NOT NULL,
|
||||
email TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
"""
|
||||
Initial orders table.
|
||||
"""
|
||||
db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id TEXT PRIMARY KEY,
|
||||
productid TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
product TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
shippingzone INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
invoiceid TEXT NOT NULL,
|
||||
paid BOOLEAN NOT NULL,
|
||||
shipped BOOLEAN NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("diagonalley") as db:
|
||||
m001_initial(db)
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
class Indexers(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
shopname: str
|
||||
indexeraddress: str
|
||||
online: bool
|
||||
rating: str
|
||||
shippingzone1: str
|
||||
shippingzone2: str
|
||||
zone1cost: int
|
||||
zone2cost: int
|
||||
email: str
|
||||
|
||||
class Products(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
product: str
|
||||
categories: str
|
||||
description: str
|
||||
image: str
|
||||
price: int
|
||||
quantity: int
|
||||
|
||||
class Orders(NamedTuple):
|
||||
id: str
|
||||
productid: str
|
||||
wallet: str
|
||||
product: str
|
||||
quantity: int
|
||||
shippingzone: int
|
||||
address: str
|
||||
email: str
|
||||
invoiceid: str
|
||||
paid: bool
|
||||
shipped: bool
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Diagon Alley: Decentralised Market-Stalls
|
||||
</h5>
|
||||
<p>
|
||||
Make a list of products to sell, point your list of products at a public
|
||||
indexer. Buyers browse your products on the indexer, and pay you
|
||||
directly. Ratings are managed by the indexer. Your stall can be listed
|
||||
in multiple indexers, even over TOR, if you wish to be anonymous.<br />
|
||||
More information on the
|
||||
<a href="https://github.com/lnbits/Diagon-Alley"
|
||||
>Diagon Alley Protocol</a
|
||||
><br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get prodcuts, categorised by wallet"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/api/v1/diagonalley/stall/products/<indexer_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>Product JSON list</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}diagonalley/api/v1/diagonalley/stall/products/<indexer_id></code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get invoice for product"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-green">POST</span>
|
||||
/api/v1/diagonalley/stall/order/<indexer_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"id": <string>, "address": <string>, "shippingzone":
|
||||
<integer>, "email": <string>, "quantity":
|
||||
<integer>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"checking_id": <string>,"payment_request":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root
|
||||
}}diagonalley/api/v1/diagonalley/stall/order/<indexer_id> -d
|
||||
'{"id": <product_id&>, "email": <customer_email>,
|
||||
"address": <customer_address>, "quantity": 2, "shippingzone":
|
||||
1}' -H "Content-type: application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Check a product has been shipped"
|
||||
class="q-mb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>{"shipped": <boolean>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id>
|
||||
-H "Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
@@ -1,912 +0,0 @@
|
||||
{% 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="deep-purple" @click="productDialog.show = true"
|
||||
>New Product</q-btn
|
||||
>
|
||||
<q-btn unelevated color="deep-purple" @click="indexerDialog.show = true"
|
||||
>New Indexer
|
||||
<q-tooltip>
|
||||
Frontend shop your stall will list its products in
|
||||
</q-tooltip></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">Products</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportProductsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="products"
|
||||
row-key="id"
|
||||
:columns="productsTable.columns"
|
||||
:pagination.sync="productsTable.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">
|
||||
{{ col.label }}
|
||||
</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="add_shopping_cart"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.wallet"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-tooltip>
|
||||
Link to pass to stall indexer
|
||||
</q-tooltip>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="openProductUpdateDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteProduct(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</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">Indexers</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportIndexersCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="indexers"
|
||||
row-key="id"
|
||||
:columns="indexersTable.columns"
|
||||
:pagination.sync="indexersTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="openIndexerUpdateDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteIndexer(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</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">Orders</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportOrdersCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="orders"
|
||||
row-key="id"
|
||||
:columns="ordersTable.columns"
|
||||
:pagination.sync="ordersTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="shipOrder(props.row.id)"
|
||||
icon="add_shopping_cart"
|
||||
color="green"
|
||||
>
|
||||
<q-tooltip>
|
||||
Product shipped?
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteOrder(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-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits Diagon Alley Extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "diagonalley/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="productDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
|
||||
<q-form @submit="sendProductFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="productDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="productDialog.data.product"
|
||||
label="Product"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="productDialog.data.categories"
|
||||
placeholder="cakes, guns, wool, drugs"
|
||||
label="Categories seperated by comma"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="productDialog.data.description"
|
||||
label="Description"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="productDialog.data.image"
|
||||
label="Image"
|
||||
placeholder="Imagur link (max 500/500px)"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="productDialog.data.price"
|
||||
type="number"
|
||||
label="Price"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="productDialog.data.quantity"
|
||||
type="number"
|
||||
label="Quantity"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="productDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update Product</q-btn
|
||||
>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="productDialog.data.image == null
|
||||
|| productDialog.data.product == null
|
||||
|| productDialog.data.description == null
|
||||
|| productDialog.data.quantity == null"
|
||||
type="submit"
|
||||
>Create Product</q-btn
|
||||
>
|
||||
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="indexerDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
|
||||
<q-form @submit="sendIndexerFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="indexerDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="indexerDialog.data.shopname"
|
||||
label="Shop Name"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="indexerDialog.data.indexeraddress"
|
||||
label="Shop link (LNbits will point to)"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model.trim="indexerDialog.data.shippingzone1"
|
||||
multiple
|
||||
:options="shippingoptions"
|
||||
label="Shipping Zone 1"
|
||||
></q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="indexerDialog.data.zone1cost"
|
||||
type="number"
|
||||
label="Zone 1 Cost"
|
||||
></q-input>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model.trim="indexerDialog.data.shippingzone2"
|
||||
multiple
|
||||
:options="shippingoptions"
|
||||
label="Shipping Zone 2"
|
||||
></q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="indexerDialog.data.zone2cost"
|
||||
type="number"
|
||||
label="Zone 2 Cost"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="indexerDialog.data.email"
|
||||
label="Email to share with customers"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="indexerDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update Indexer</q-btn
|
||||
>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="indexerDialog.data.shopname == null
|
||||
|| indexerDialog.data.shippingzone1 == null
|
||||
|| indexerDialog.data.indexeraddress == null
|
||||
|| indexerDialog.data.zone1cost == null
|
||||
|| indexerDialog.data.shippingzone2 == null
|
||||
|| indexerDialog.data.zone2cost == null
|
||||
|| indexerDialog.data.email == null"
|
||||
type="submit"
|
||||
>Create Indexer</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 mapDiagonAlley = 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.wall = ['/diagonalley/', obj.id].join('')
|
||||
obj._data = _.clone(obj)
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
products: [],
|
||||
orders: [],
|
||||
indexers: [],
|
||||
shippedModel: false,
|
||||
shippingoptions: [
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Belgium',
|
||||
'Brazil',
|
||||
'Canada',
|
||||
'Denmark',
|
||||
'Finland',
|
||||
'France*',
|
||||
'Germany',
|
||||
'Greece',
|
||||
'Hong Kong',
|
||||
'Hungary',
|
||||
'Ireland',
|
||||
'Indonesia',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Japan',
|
||||
'Kazakhstan',
|
||||
'Korea',
|
||||
'Luxembourg',
|
||||
'Malaysia',
|
||||
'Mexico',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Norway',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Russia',
|
||||
'Saudi Arabia',
|
||||
'Singapore',
|
||||
'Spain',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Thailand',
|
||||
'Turkey',
|
||||
'Ukraine',
|
||||
'United Kingdom**',
|
||||
'United States***',
|
||||
'Vietnam',
|
||||
'China'
|
||||
],
|
||||
label: '',
|
||||
indexersTable: {
|
||||
columns: [
|
||||
{name: 'shopname', align: 'left', label: 'Shop', field: 'shopname'},
|
||||
{
|
||||
name: 'indexeraddress',
|
||||
align: 'left',
|
||||
label: 'Address',
|
||||
field: 'indexeraddress'
|
||||
},
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{
|
||||
name: 'rating',
|
||||
align: 'left',
|
||||
label: 'Your Rating',
|
||||
field: 'rating'
|
||||
},
|
||||
{name: 'email', align: 'left', label: 'Your email', field: 'email'},
|
||||
{name: 'online', align: 'left', label: 'Online', field: 'online'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
ordersTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'product',
|
||||
align: 'left',
|
||||
label: 'Product',
|
||||
field: 'product'
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
align: 'left',
|
||||
label: 'Quantity',
|
||||
field: 'quantity'
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
align: 'left',
|
||||
label: 'Address',
|
||||
field: 'address'
|
||||
},
|
||||
{
|
||||
name: 'invoiceid',
|
||||
align: 'left',
|
||||
label: 'InvoiceID',
|
||||
field: 'invoiceid'
|
||||
},
|
||||
{name: 'paid', align: 'left', label: 'Paid', field: 'paid'},
|
||||
{name: 'shipped', align: 'left', label: 'Shipped', field: 'shipped'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
productsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'product',
|
||||
align: 'left',
|
||||
label: 'Product',
|
||||
field: 'product'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Description',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
align: 'left',
|
||||
label: 'Categories',
|
||||
field: 'categories'
|
||||
},
|
||||
{name: 'image', align: 'left', label: 'Image', field: 'image'},
|
||||
{name: 'price', align: 'left', label: 'Price', field: 'price'},
|
||||
{
|
||||
name: 'quantity',
|
||||
align: 'left',
|
||||
label: 'Quantity',
|
||||
field: 'quantity'
|
||||
},
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
productDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
orderDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
indexerDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getIndexers: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/diagonalley/api/v1/diagonalley/indexers?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.indexers = response.data.map(function (obj) {
|
||||
return mapDiagonAlley(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
openIndexerUpdateDialog: function (linkId) {
|
||||
var link = _.findWhere(this.indexers, {id: linkId})
|
||||
|
||||
this.indexerDialog.data = _.clone(link._data)
|
||||
this.indexerDialog.show = true
|
||||
},
|
||||
sendIndexerFormData: function () {
|
||||
if (this.indexerDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
shopname: this.indexerDialog.data.shopname,
|
||||
indexeraddress: this.indexerDialog.data.indexeraddress,
|
||||
shippingzone1: this.indexerDialog.data.shippingzone1.join(', '),
|
||||
zone1cost: this.indexerDialog.data.zone1cost,
|
||||
shippingzone2: this.indexerDialog.data.shippingzone2.join(', '),
|
||||
zone2cost: this.indexerDialog.data.zone2cost,
|
||||
email: this.indexerDialog.data.email
|
||||
}
|
||||
}
|
||||
|
||||
if (this.indexerDialog.data.id) {
|
||||
this.updateIndexer(this.indexerDialog.data)
|
||||
} else {
|
||||
this.createIndexer(data)
|
||||
}
|
||||
},
|
||||
updateIndexer: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/diagonalley/api/v1/diagonalley/indexers' + data.id,
|
||||
_.findWhere(this.g.user.wallets, {
|
||||
id: this.indexerDialog.data.wallet
|
||||
}).inkey,
|
||||
_.pick(
|
||||
data,
|
||||
'shopname',
|
||||
'indexeraddress',
|
||||
'shippingzone1',
|
||||
'zone1cost',
|
||||
'shippingzone2',
|
||||
'zone2cost',
|
||||
'email'
|
||||
)
|
||||
)
|
||||
.then(function (response) {
|
||||
self.indexers = _.reject(self.indexers, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.indexers.push(mapDiagonAlley(response.data))
|
||||
self.indexerDialog.show = false
|
||||
self.indexerDialog.data = {}
|
||||
data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
createIndexer: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/diagonalley/api/v1/diagonalley/indexers',
|
||||
_.findWhere(this.g.user.wallets, {
|
||||
id: this.indexerDialog.data.wallet
|
||||
}).inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.indexers.push(mapDiagonAlley(response.data))
|
||||
self.indexerDialog.show = false
|
||||
self.indexerDialog.data = {}
|
||||
data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteIndexer: function (indexerId) {
|
||||
var self = this
|
||||
var indexer = _.findWhere(this.indexers, {id: indexerId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this Indexer link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/diagonalley/api/v1/diagonalley/indexers/' + indexerId,
|
||||
_.findWhere(self.g.user.wallets, {id: indexer.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.indexers = _.reject(self.indexers, function (obj) {
|
||||
return obj.id == indexerId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportIndexersCSV: function () {
|
||||
LNbits.utils.exportCSV(this.indexersTable.columns, this.indexers)
|
||||
},
|
||||
getOrders: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/diagonalley/api/v1/diagonalley/orders?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.orders = response.data.map(function (obj) {
|
||||
return mapDiagonAlley(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
createOrder: function () {
|
||||
var data = {
|
||||
address: this.orderDialog.data.address,
|
||||
email: this.orderDialog.data.email,
|
||||
quantity: this.orderDialog.data.quantity,
|
||||
shippingzone: this.orderDialog.data.shippingzone
|
||||
}
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/diagonalley/api/v1/diagonalley/orders',
|
||||
_.findWhere(this.g.user.wallets, {id: this.orderDialog.data.wallet})
|
||||
.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.orders.push(mapDiagonAlley(response.data))
|
||||
self.orderDialog.show = false
|
||||
self.orderDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteOrder: function (orderId) {
|
||||
var self = this
|
||||
var order = _.findWhere(this.orders, {id: orderId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this order link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/diagonalley/api/v1/diagonalley/orders/' + orderId,
|
||||
_.findWhere(self.g.user.wallets, {id: order.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.orders = _.reject(self.orders, function (obj) {
|
||||
return obj.id == orderId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
shipOrder: function (order_id) {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/diagonalley/api/v1/diagonalley/orders/shipped/' + order_id,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.orders = response.data.map(function (obj) {
|
||||
return mapDiagonAlley(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportOrdersCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ordersTable.columns, this.orders)
|
||||
},
|
||||
getProducts: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/diagonalley/api/v1/diagonalley/products?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.products = response.data.map(function (obj) {
|
||||
return mapDiagonAlley(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
openProductUpdateDialog: function (linkId) {
|
||||
var link = _.findWhere(this.products, {id: linkId})
|
||||
|
||||
this.productDialog.data = _.clone(link._data)
|
||||
this.productDialog.show = true
|
||||
},
|
||||
sendProductFormData: function () {
|
||||
if (this.productDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
product: this.productDialog.data.product,
|
||||
categories: this.productDialog.data.categories,
|
||||
description: this.productDialog.data.description,
|
||||
image: this.productDialog.data.image,
|
||||
price: this.productDialog.data.price,
|
||||
quantity: this.productDialog.data.quantity
|
||||
}
|
||||
}
|
||||
if (this.productDialog.data.id) {
|
||||
this.updateProduct(this.productDialog.data)
|
||||
} else {
|
||||
this.createProduct(data)
|
||||
}
|
||||
},
|
||||
updateProduct: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/diagonalley/api/v1/diagonalley/products' + data.id,
|
||||
_.findWhere(this.g.user.wallets, {
|
||||
id: this.productDialog.data.wallet
|
||||
}).inkey,
|
||||
_.pick(
|
||||
data,
|
||||
'shopname',
|
||||
'indexeraddress',
|
||||
'shippingzone1',
|
||||
'zone1cost',
|
||||
'shippingzone2',
|
||||
'zone2cost',
|
||||
'email'
|
||||
)
|
||||
)
|
||||
.then(function (response) {
|
||||
self.products = _.reject(self.products, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.products.push(mapDiagonAlley(response.data))
|
||||
self.productDialog.show = false
|
||||
self.productDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
createProduct: function (data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/diagonalley/api/v1/diagonalley/products',
|
||||
_.findWhere(this.g.user.wallets, {
|
||||
id: this.productDialog.data.wallet
|
||||
}).inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.products.push(mapDiagonAlley(response.data))
|
||||
self.productDialog.show = false
|
||||
self.productDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteProduct: function (productId) {
|
||||
var self = this
|
||||
var product = _.findWhere(this.products, {id: productId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this products link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/diagonalley/api/v1/diagonalley/products/' + productId,
|
||||
_.findWhere(self.g.user.wallets, {id: product.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.products = _.reject(self.products, function (obj) {
|
||||
return obj.id == productId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportProductsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.productsTable.columns, this.products)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getProducts()
|
||||
this.getOrders()
|
||||
this.getIndexers()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,3 +0,0 @@
|
||||
<script>
|
||||
console.log('{{ stall }}')
|
||||
</script>
|
||||
@@ -1,15 +0,0 @@
|
||||
import json
|
||||
|
||||
from flask import g, abort, render_template, jsonify
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.diagonalley import diagonalley_ext
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
@diagonalley_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
|
||||
return render_template("diagonalley/index.html", user=g.user)
|
||||
@@ -1,248 +0,0 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from lnbits.extensions.diagonalley import diagonalley_ext
|
||||
from .crud import create_diagonalleys_product,get_diagonalleys_product,get_diagonalleys_products,delete_diagonalleys_product,create_diagonalleys_indexer,update_diagonalleys_indexer,get_diagonalleys_indexer,get_diagonalleys_indexers,delete_diagonalleys_indexer,create_diagonalleys_order,get_diagonalleys_order,get_diagonalleys_orders,delete_diagonalleys_order
|
||||
from lnbits.core.services import create_invoice
|
||||
from base64 import urlsafe_b64encode
|
||||
from uuid import uuid4
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
###Products
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalley_products():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["POST"])
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/products<product_id>", methods=["PUT"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
@api_validate_post_request(schema={
|
||||
"product": {"type": "string", "empty": False, "required": True},
|
||||
"categories": {"type": "string", "empty": False, "required": True},
|
||||
"description": {"type": "string", "empty": False, "required": True},
|
||||
"image": {"type": "string", "empty": False, "required": True},
|
||||
"price": {"type": "integer", "min": 0, "required": True},
|
||||
"quantity": {"type": "integer", "min": 0, "required": True}
|
||||
})
|
||||
def api_diagonalley_product_create(product_id=None):
|
||||
|
||||
if product_id:
|
||||
product = get_diagonalleys_indexer(product_id)
|
||||
|
||||
if not product:
|
||||
return jsonify({"message": "Withdraw product does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if product.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw product."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
product = update_diagonalleys_product(product_id, **g.data)
|
||||
else:
|
||||
product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify(product._asdict()), HTTPStatus.OK if product_id else HTTPStatus.CREATED
|
||||
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/products/<product_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalley_products_delete(product_id):
|
||||
product = get_diagonalleys_product(product_id)
|
||||
|
||||
if not product:
|
||||
return jsonify({"message": "Product does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if product.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Diagon Alley."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_product(product_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
|
||||
###Indexers
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalley_indexers():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["POST"])
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/indexers<indexer_id>", methods=["PUT"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
@api_validate_post_request(schema={
|
||||
"shopname": {"type": "string", "empty": False, "required": True},
|
||||
"indexeraddress": {"type": "string", "empty": False, "required": True},
|
||||
"shippingzone1": {"type": "string", "empty": False, "required": True},
|
||||
"shippingzone2": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True},
|
||||
"zone1cost": {"type": "integer", "min": 0, "required": True},
|
||||
"zone2cost": {"type": "integer", "min": 0, "required": True}
|
||||
})
|
||||
def api_diagonalley_indexer_create(indexer_id=None):
|
||||
|
||||
if indexer_id:
|
||||
indexer = get_diagonalleys_indexer(indexer_id)
|
||||
|
||||
if not indexer:
|
||||
return jsonify({"message": "Withdraw indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if indexer.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw indexer."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
indexer = update_diagonalleys_indexer(indexer_id, **g.data)
|
||||
else:
|
||||
indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify(indexer._asdict()), HTTPStatus.OK if indexer_id else HTTPStatus.CREATED
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalley_indexer_delete(indexer_id):
|
||||
indexer = get_diagonalleys_indexer(indexer_id)
|
||||
|
||||
if not indexer:
|
||||
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if indexer.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_indexer(indexer_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
###Orders
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalley_orders():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["POST"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
@api_validate_post_request(schema={
|
||||
"id": {"type": "string", "empty": False, "required": True},
|
||||
"address": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True},
|
||||
"quantity": {"type": "integer", "empty": False, "required": True},
|
||||
"shippingzone": {"type": "integer", "empty": False, "required": True},
|
||||
})
|
||||
def api_diagonalley_order_create():
|
||||
order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data)
|
||||
return jsonify(order._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders/<order_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalley_order_delete(order_id):
|
||||
order = get_diagonalleys_order(order_id)
|
||||
|
||||
if not order:
|
||||
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if order.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_indexer(order_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders/paid/<order_id>", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalleys_order_paid(order_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE orders SET paid = ? WHERE id = ?", (True, order_id,))
|
||||
return "", HTTPStatus.OK
|
||||
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/<order_id>", methods=["GET"])
|
||||
@api_check_wallet_key(key_type="invoice")
|
||||
def api_diagonalleys_order_shipped(order_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute("UPDATE orders SET shipped = ? WHERE id = ?", (True, order_id,))
|
||||
order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
|
||||
|
||||
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), HTTPStatus.OK
|
||||
|
||||
|
||||
###List products based on indexer id
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/stall/products/<indexer_id>", methods=["GET"])
|
||||
def api_diagonalleys_stall_products(indexer_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
|
||||
print(rows[1])
|
||||
if not rows:
|
||||
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
products = db.fetchone("SELECT * FROM products WHERE wallet = ?", (rows[1],))
|
||||
if not products:
|
||||
return jsonify({"message": "No products"}), HTTPStatus.NOT_FOUND
|
||||
|
||||
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), HTTPStatus.OK
|
||||
|
||||
###Check a product has been shipped
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/stall/checkshipped/<checking_id>", methods=["GET"])
|
||||
def api_diagonalleys_stall_checkshipped(checking_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
|
||||
|
||||
return jsonify({"shipped": rows["shipped"]}), HTTPStatus.OK
|
||||
|
||||
###Place order
|
||||
|
||||
@diagonalley_ext.route("/api/v1/diagonalley/stall/order/<indexer_id>", methods=["POST"])
|
||||
@api_validate_post_request(schema={
|
||||
"id": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True},
|
||||
"address": {"type": "string", "empty": False, "required": True},
|
||||
"quantity": {"type": "integer", "empty": False, "required": True},
|
||||
"shippingzone": {"type": "integer", "empty": False, "required": True},
|
||||
})
|
||||
def api_diagonalley_stall_order(indexer_id):
|
||||
product = get_diagonalleys_product(g.data["id"])
|
||||
shipping = get_diagonalleys_indexer(indexer_id)
|
||||
|
||||
if g.data["shippingzone"] == 1:
|
||||
shippingcost = shipping.zone1cost
|
||||
else:
|
||||
shippingcost = shipping.zone2cost
|
||||
|
||||
checking_id, payment_request = create_invoice(wallet_id=product.wallet, amount=shippingcost + (g.data["quantity"] * product.price), memo=g.data["id"])
|
||||
selling_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(selling_id ,g.data["id"] , product.wallet, product.product, g.data["quantity"], g.data["shippingzone"], g.data["address"], g.data["email"], checking_id, False, False),
|
||||
)
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
||||
@@ -1,3 +0,0 @@
|
||||
<h1>Events</h1>
|
||||
<h2>Events: Sell and register event tickets</h2>
|
||||
Events alows you to make a wave of tickets for an event, each ticket is in the form of a unqiue QRcode, which the user presents at registration. Events comes with a shareable ticket scanner, which can be used to register attendees.
|
||||
@@ -1,8 +0,0 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Events",
|
||||
"short_description": "Sell and register event tickets",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["benarc"]
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import Tickets, Events
|
||||
|
||||
|
||||
#######TICKETS########
|
||||
|
||||
|
||||
def create_ticket(checking_id: str, wallet: str, event: str, name: str, email: str) -> Tickets:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (id, wallet, event, name, email, registered, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(checking_id, wallet, event, name, email, False, False),
|
||||
)
|
||||
|
||||
return get_ticket(checking_id)
|
||||
|
||||
def update_ticket(paid: bool, checking_id: str) -> Tickets:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
|
||||
if row[6] == True:
|
||||
return get_ticket(checking_id)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE ticket
|
||||
SET paid = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(paid, checking_id),
|
||||
)
|
||||
|
||||
eventdata = get_event(row[2])
|
||||
sold = eventdata.sold + 1
|
||||
amount_tickets = eventdata.amount_tickets - 1
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE events
|
||||
SET sold = ?, amount_tickets = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(sold, amount_tickets, row[2]),
|
||||
)
|
||||
return get_ticket(checking_id)
|
||||
|
||||
|
||||
def get_ticket(checking_id: str) -> Optional[Tickets]:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
|
||||
|
||||
return Tickets(**row) if row else None
|
||||
|
||||
|
||||
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
print("scrum")
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_ticket(checking_id: str) -> None:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("DELETE FROM ticket WHERE id = ?", (checking_id,))
|
||||
|
||||
|
||||
|
||||
########EVENTS#########
|
||||
|
||||
|
||||
def create_event(*, wallet: str, name: str, info: str, closing_date: str, event_start_date: str, event_end_date: str, amount_tickets: int, price_per_ticket: int) -> Events:
|
||||
with open_ext_db("events") as db:
|
||||
event_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(event_id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, 0),
|
||||
)
|
||||
print(event_id)
|
||||
|
||||
return get_event(event_id)
|
||||
|
||||
def update_event(event_id: str, **kwargs) -> Events:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
with open_ext_db("events") as db:
|
||||
db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id))
|
||||
|
||||
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
return Events(**row) if row else None
|
||||
|
||||
|
||||
def get_event(event_id: str) -> Optional[Events]:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
return Events(**row) if row else None
|
||||
|
||||
|
||||
def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Events(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_event(event_id: str) -> None:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("DELETE FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
########EVENTTICKETS#########
|
||||
|
||||
def get_event_tickets(event_id: str, wallet_id: str) -> Tickets:
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
rows = db.fetchall("SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id))
|
||||
print(rows)
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
def reg_ticket(ticket_id: str) -> Tickets:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("UPDATE ticket SET registered = ? WHERE id = ?", (True, ticket_id))
|
||||
ticket = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
|
||||
print(ticket[1])
|
||||
rows = db.fetchall("SELECT * FROM ticket WHERE event = ?", (ticket[1],))
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
||||
@@ -1,93 +0,0 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
def m001_initial(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
info TEXT NOT NULL,
|
||||
closing_date TEXT NOT NULL,
|
||||
event_start_date TEXT NOT NULL,
|
||||
event_end_date TEXT NOT NULL,
|
||||
amount_tickets INTEGER NOT NULL,
|
||||
price_per_ticket INTEGER NOT NULL,
|
||||
sold INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
registered BOOLEAN NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def m002_changed(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS ticket (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
registered BOOLEAN NOT NULL,
|
||||
paid BOOLEAN NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]:
|
||||
usescsv = ""
|
||||
|
||||
for i in range(row[5]):
|
||||
if row[7]:
|
||||
usescsv += "," + str(i + 1)
|
||||
else:
|
||||
usescsv += "," + str(1)
|
||||
usescsv = usescsv[1:]
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (
|
||||
id,
|
||||
wallet,
|
||||
event,
|
||||
name,
|
||||
email,
|
||||
registered,
|
||||
paid
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
row[0],
|
||||
row[1],
|
||||
row[2],
|
||||
row[3],
|
||||
row[4],
|
||||
row[5],
|
||||
True,
|
||||
),
|
||||
)
|
||||
db.execute("DROP TABLE tickets")
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("events") as db:
|
||||
m001_initial(db)
|
||||
m002_changed(db)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Events(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
info: str
|
||||
closing_date: str
|
||||
event_start_date: str
|
||||
event_end_date: str
|
||||
amount_tickets: int
|
||||
price_per_ticket: int
|
||||
sold: int
|
||||
time: int
|
||||
|
||||
class Tickets(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
event: str
|
||||
name: str
|
||||
email: str
|
||||
registered: bool
|
||||
paid: bool
|
||||
time: int
|
||||
@@ -1,23 +0,0 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Events: Sell and register ticket waves for an event
|
||||
</h5>
|
||||
<p>
|
||||
Events alows you to make a wave of tickets for an event, each ticket is
|
||||
in the form of a unqiue QRcode, which the user presents at registration.
|
||||
Events comes with a shareable ticket scanner, which can be used to
|
||||
register attendees.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>
|
||||
</small>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
@@ -1,205 +0,0 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h3 class="q-my-none">{{ event_name }}</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">{{ event_info }}</h5>
|
||||
<br />
|
||||
<q-form @submit="Invoice()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Your name "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.email"
|
||||
type="email"
|
||||
label="Your email "
|
||||
></q-input>
|
||||
|
||||
<p>{% raw %}{{amountWords}}{% endraw %}</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card v-show="ticketLink.show" class="q-pa-lg">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="ticketLink.data.link" target="_blank">
|
||||
<q-btn unelevated size="xl" color="deep-purple"
|
||||
>Link to your ticket!</q-btn
|
||||
></a
|
||||
>
|
||||
<br /><br />
|
||||
<p>You'll be redirected in a few moments...</p>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
console.log('{{ form_costpword }}')
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
name: '',
|
||||
email: ''
|
||||
}
|
||||
},
|
||||
ticketLink: {
|
||||
show: false,
|
||||
data: {
|
||||
link: ''
|
||||
}
|
||||
},
|
||||
receive: {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetForm: function (e) {
|
||||
e.preventDefault()
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
},
|
||||
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker
|
||||
dismissMsg()
|
||||
|
||||
clearInterval(paymentChecker)
|
||||
setTimeout(function () {}, 10000)
|
||||
},
|
||||
Invoice: function () {
|
||||
var self = this
|
||||
axios
|
||||
|
||||
.post(
|
||||
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
|
||||
{
|
||||
event: '{{ event_id }}',
|
||||
event_name: '{{ event_name }}',
|
||||
name: self.formDialog.data.name,
|
||||
email: self.formDialog.data.email
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.checking_id
|
||||
|
||||
dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
|
||||
self.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: self.paymentReq
|
||||
}
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios
|
||||
.get('/events/api/v1/tickets/' + self.paymentCheck)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
dismissMsg()
|
||||
self.formDialog.data.name = ''
|
||||
self.formDialog.data.email = ''
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: null
|
||||
})
|
||||
self.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
|
||||
self.ticketLink = {
|
||||
show: true,
|
||||
data: {
|
||||
link: '/events/ticket/' + res.data.ticket_id
|
||||
}
|
||||
}
|
||||
setTimeout(function () {
|
||||
window.location.href =
|
||||
'/events/ticket/' + res.data.ticket_id
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,35 +0,0 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ event_name }} error</h3>
|
||||
<br />
|
||||
<q-icon
|
||||
name="warning"
|
||||
class="text-grey"
|
||||
style="font-size: 20rem;"
|
||||
></q-icon>
|
||||
|
||||
<h5 class="q-my-none">{{ event_error }}</h5>
|
||||
<br />
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
@@ -1,509 +0,0 @@
|
||||
{% 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="deep-purple" @click="formDialog.show = true"
|
||||
>New Event</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">Events</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exporteventsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="events"
|
||||
row-key="id"
|
||||
:columns="eventsTable.columns"
|
||||
:pagination.sync="eventsTable.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">
|
||||
{{ col.label }}
|
||||
</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="link"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="how_to_reg"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/register/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateformDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteEvent(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</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">Tickets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportticketsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.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">
|
||||
{{ col.label }}
|
||||
</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="local_activity"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/ticket/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTicket(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-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits Events extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "events/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendEventData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Name of event "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.info"
|
||||
type="textarea"
|
||||
label="Info about the event "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.closing_date"
|
||||
type="date"
|
||||
label="Closing for tickets "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_start_date"
|
||||
type="date"
|
||||
label="Event begins "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_end_date"
|
||||
type="date"
|
||||
label="Event ends "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.amount_tickets"
|
||||
type="number"
|
||||
label="Amount of tickets "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.price_per_ticket"
|
||||
type="number"
|
||||
label="Price per ticket "
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update Event</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.wallet == null || formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || formDialog.data.price_per_ticket == null"
|
||||
type="submit"
|
||||
>Create Event</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 mapEvents = 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.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
events: [],
|
||||
tickets: [],
|
||||
eventsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'info', align: 'left', label: 'Info', field: 'info'},
|
||||
{
|
||||
name: 'event_start_date',
|
||||
align: 'left',
|
||||
label: 'Start date',
|
||||
field: 'event_start_date'
|
||||
},
|
||||
{
|
||||
name: 'event_end_date',
|
||||
align: 'left',
|
||||
label: 'End date',
|
||||
field: 'event_end_date'
|
||||
},
|
||||
{
|
||||
name: 'closing_date',
|
||||
align: 'left',
|
||||
label: 'Ticket close',
|
||||
field: 'closing_date'
|
||||
},
|
||||
{
|
||||
name: 'price_per_ticket',
|
||||
align: 'left',
|
||||
label: 'Price',
|
||||
field: 'price_per_ticket'
|
||||
},
|
||||
{
|
||||
name: 'amount_tickets',
|
||||
align: 'left',
|
||||
label: 'No tickets',
|
||||
field: 'amount_tickets'
|
||||
},
|
||||
{
|
||||
name: 'sold',
|
||||
align: 'left',
|
||||
label: 'Sold',
|
||||
field: 'sold'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'event', align: 'left', label: 'Event', field: 'event'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
||||
{
|
||||
name: 'registered',
|
||||
align: 'left',
|
||||
label: 'Registered',
|
||||
field: 'registered'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTickets: function () {
|
||||
var self = this
|
||||
console.log('obj')
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/tickets?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
console.log(obj)
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteTicket: function (ticketId) {
|
||||
var self = this
|
||||
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this ticket')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/events/api/v1/tickets/' + ticketId,
|
||||
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = _.reject(self.tickets, function (obj) {
|
||||
return obj.id == ticketId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportticketsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||
},
|
||||
|
||||
getEvents: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/events?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = response.data.map(function (obj) {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
sendEventData: function () {
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
var data = this.formDialog.data
|
||||
|
||||
if (data.id) {
|
||||
this.updateEvent(wallet, data)
|
||||
} else {
|
||||
this.createEvent(wallet, data)
|
||||
}
|
||||
},
|
||||
|
||||
createEvent: function (wallet, data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request('POST', '/events/api/v1/events', wallet.inkey, data)
|
||||
.then(function (response) {
|
||||
self.events.push(mapEvents(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.events, {id: formId})
|
||||
console.log(link.id)
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
this.formDialog.data.name = link.name
|
||||
this.formDialog.data.info = link.info
|
||||
this.formDialog.data.closing_date = link.closing_date
|
||||
this.formDialog.data.event_start_date = link.event_start_date
|
||||
this.formDialog.data.event_end_date = link.event_end_date
|
||||
this.formDialog.data.amount_tickets = link.amount_tickets
|
||||
this.formDialog.data.price_per_ticket = link.price_per_ticket
|
||||
this.formDialog.show = true
|
||||
},
|
||||
updateEvent: function (wallet, data) {
|
||||
var self = this
|
||||
console.log(data)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/events/api/v1/events/' + data.id,
|
||||
wallet.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = _.reject(self.events, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.events.push(mapEvents(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteEvent: function (eventsId) {
|
||||
var self = this
|
||||
var events = _.findWhere(this.events, {id: eventsId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this form link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/events/api/v1/events/' + eventsId,
|
||||
_.findWhere(self.g.user.wallets, {id: events.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = _.reject(self.events, function (obj) {
|
||||
return obj.id == eventsId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exporteventsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.eventsTable.columns, this.events)
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTickets()
|
||||
this.getEvents()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,190 +0,0 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ event_name }} Registration</h3>
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
<q-btn unelevated color="deep-purple" @click="showCamera" size="xl"
|
||||
>Scan ticket</q-btn
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.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">
|
||||
{{ col.label }}
|
||||
</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="local_activity"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/ticket/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="sendCamera.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl">
|
||||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream
|
||||
@decode="decodeQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block styles %}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}"
|
||||
/>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
|
||||
'vendor/moment@2.25.1/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js',
|
||||
'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js',
|
||||
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
var mapEvents = 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.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tickets: [],
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{
|
||||
name: 'registered',
|
||||
align: 'left',
|
||||
label: 'Registered',
|
||||
field: 'registered'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
sendCamera: {
|
||||
show: false,
|
||||
camera: 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hoverEmail: function (tmp) {
|
||||
this.tickets.data.emailtemp = tmp
|
||||
},
|
||||
closeCamera: function () {
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
showCamera: function () {
|
||||
this.sendCamera.show = true
|
||||
},
|
||||
decodeQR: function (res) {
|
||||
this.sendCamera.show = false
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request('GET', '/events/api/v1/register/ticket/' + res)
|
||||
.then(function (response) {
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Registered!'
|
||||
})
|
||||
setTimeout(function () {
|
||||
window.location.reload()
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getEventTickets: function () {
|
||||
var self = this
|
||||
console.log('obj')
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.getEventTickets()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,46 +0,0 @@
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ ticket_name }} Ticket</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">
|
||||
Bookmark, print or screenshot this page,<br />
|
||||
and present it for registration!
|
||||
</h5>
|
||||
<br />
|
||||
|
||||
<qrcode
|
||||
:value="'{{ ticket_id }}'"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
<br />
|
||||
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
|
||||
<q-icon left size="3em" name="print"></q-icon> Print</q-btn
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
printWindow: function () {
|
||||
window.print()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,45 +0,0 @@
|
||||
from flask import g, abort, render_template
|
||||
from datetime import date, datetime
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.extensions.events import events_ext
|
||||
from .crud import get_ticket, get_event
|
||||
|
||||
|
||||
@events_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
return render_template("events/index.html", user=g.user)
|
||||
|
||||
|
||||
@events_ext.route("/<event_id>")
|
||||
def display(event_id):
|
||||
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
if event.amount_tickets < 1:
|
||||
return render_template("events/error.html", event_name=event.name, event_error="Sorry, tickets are sold out :(")
|
||||
datetime_object = datetime.strptime(event.closing_date, '%Y-%m-%d').date()
|
||||
if date.today() > datetime_object:
|
||||
return render_template("events/error.html", event_name=event.name, event_error="Sorry, ticket closing date has passed :(")
|
||||
|
||||
|
||||
return render_template("events/display.html", event_id=event_id, event_name=event.name, event_info=event.info, event_price=event.price_per_ticket)
|
||||
|
||||
|
||||
@events_ext.route("/ticket/<ticket_id>")
|
||||
def ticket(ticket_id):
|
||||
ticket = get_ticket(ticket_id) or abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.")
|
||||
event = get_event(ticket.event) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
return render_template("events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info)
|
||||
|
||||
|
||||
@events_ext.route("/register/<event_id>")
|
||||
def register(event_id):
|
||||
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
|
||||
return render_template("events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet)
|
||||
|
||||
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.extensions.events import events_ext
|
||||
from .crud import create_ticket, update_ticket, get_ticket, get_tickets, delete_ticket, create_event, update_event, get_event, get_events, delete_event, get_event_tickets, reg_ticket
|
||||
|
||||
|
||||
#########Events##########
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/events", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_events():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([event._asdict() for event in get_events(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/events", methods=["POST"])
|
||||
@events_ext.route("/api/v1/events/<event_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"wallet": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"info": {"type": "string", "min": 0, "required": True},
|
||||
"closing_date": {"type": "string", "empty": False, "required": True},
|
||||
"event_start_date": {"type": "string", "empty": False, "required": True},
|
||||
"event_end_date": {"type": "string", "empty": False, "required": True},
|
||||
"amount_tickets": {"type": "integer", "min": 0, "required": True},
|
||||
"price_per_ticket": {"type": "integer", "min": 0, "required": True}
|
||||
}
|
||||
)
|
||||
def api_event_create(event_id=None):
|
||||
if event_id:
|
||||
event = get_event(event_id)
|
||||
print(g.data)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if event.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
event = update_event(event_id, **g.data)
|
||||
else:
|
||||
event = create_event(**g.data)
|
||||
print(event)
|
||||
return jsonify(event._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_form_delete(event_id):
|
||||
event = get_event(event_id)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if event.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_event(event_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
|
||||
@events_ext.route("/api/v1/tickets", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_tickets():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in get_tickets(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["POST"])
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True}
|
||||
})
|
||||
def api_ticket_make_ticket(event_id, sats):
|
||||
|
||||
event = get_event(event_id)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, event=event_id, **g.data)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/tickets/<checking_id>", methods=["GET"])
|
||||
def api_ticket_send_ticket(checking_id):
|
||||
theticket = get_ticket(checking_id)
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if is_paid:
|
||||
wallet = get_wallet(theticket.wallet)
|
||||
payment = wallet.get_payment(checking_id)
|
||||
payment.set_pending(False)
|
||||
ticket = update_ticket(paid=True, checking_id=checking_id)
|
||||
|
||||
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_ticket_delete(ticket_id):
|
||||
ticket = get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if ticket.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_ticket(ticket_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########EventTickets##########
|
||||
|
||||
@events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"])
|
||||
def api_event_tickets(wallet_id, event_id):
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in get_event_tickets(wallet_id=wallet_id, event_id=event_id)]), HTTPStatus.OK
|
||||
|
||||
@events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"])
|
||||
def api_event_register_ticket(ticket_id):
|
||||
|
||||
ticket = get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
if ticket.registered == True:
|
||||
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
|
||||
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in reg_ticket(ticket_id)]), HTTPStatus.OK
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<h1>Example Extension</h1>
|
||||
<h2>*tagline*</h2>
|
||||
This is an example extension to help you organise and build you own.
|
||||
|
||||
Try to include an image
|
||||
<img src="https://i.imgur.com/9i4xcQB.png">
|
||||
|
||||
|
||||
<h2>If your extension has API endpoints, include useful ones here</h2>
|
||||
|
||||
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
||||