Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0197c6c44a | ||
|
|
c3e337a319 | ||
|
|
976a3d4e5c | ||
|
|
4b4a297c3f | ||
|
|
5d128523c7 | ||
|
|
b34a0a9835 | ||
|
|
99cfdd5531 | ||
|
|
435f9c9773 | ||
|
|
ab190454f4 | ||
|
|
5fe64d324d | ||
|
|
19fa6c2bf9 | ||
|
|
efd2cb7170 | ||
|
|
4995ab5aaf | ||
|
|
7acbe9f93c | ||
|
|
122281ba81 | ||
|
|
97f4fabea1 | ||
|
|
2c30209cba | ||
|
|
9f22c0395e | ||
|
|
2cf5b13b93 | ||
|
|
bedf01b1b6 | ||
|
|
eff41ddf8d | ||
|
|
19e439c9a2 | ||
|
|
fb86aad6a9 | ||
|
|
e226df2894 | ||
|
|
d15ba01f94 | ||
|
|
897644956a | ||
|
|
f02526585f | ||
|
|
4bf93d36c6 | ||
|
|
e461e34fc0 | ||
|
|
34e6d2a7bb | ||
|
|
1978cafe44 | ||
|
|
cd22aaded9 | ||
|
|
498eceeb46 | ||
|
|
39041ffb5a | ||
|
|
0c88aa8fcf | ||
|
|
eb938c11a8 | ||
|
|
b1da103080 | ||
|
|
d90ae26062 | ||
|
|
403385c205 | ||
|
|
fd4dc6c48f | ||
|
|
e1c5e970c4 | ||
|
|
f12f52db2d | ||
|
|
8d328c5465 | ||
|
|
efb9deb028 | ||
|
|
5806beaeb7 | ||
|
|
d917feeb02 | ||
|
|
efbba20872 | ||
|
|
93e51a7398 | ||
|
|
94e524bf4c | ||
|
|
364b6dc857 | ||
|
|
bb2d1ea140 | ||
|
|
409122271a | ||
|
|
07b4d52432 | ||
|
|
ae778c19a1 | ||
|
|
65c17488e3 | ||
|
|
98f7dc8eff | ||
|
|
6bdf4e22e4 | ||
|
|
549976b9d0 | ||
|
|
f0cbaea0b1 | ||
|
|
ca08dc2302 | ||
|
|
7a1477c056 | ||
|
|
27b02e85db | ||
|
|
d82e06a4f9 | ||
|
|
39098b2432 | ||
|
|
5aab51b78c | ||
|
|
5f9c9c9df6 | ||
|
|
40fc7ad08f | ||
|
|
51587d7de2 | ||
|
|
1154d632a2 | ||
|
|
1862cccb2a | ||
|
|
c33a45d20e | ||
|
|
0178097ee6 | ||
|
|
57c26ed2bb | ||
|
|
9e98dae317 | ||
|
|
aa13551bd3 | ||
|
|
b52618f6f5 | ||
|
|
5c34c23617 | ||
|
|
5b16bd0224 | ||
|
|
316b2256e4 | ||
|
|
10b8f90fcc | ||
|
|
b38e6bf566 | ||
|
|
d4c9043278 | ||
|
|
d03e472cc2 | ||
|
|
de75057a15 | ||
|
|
a455379ee1 | ||
|
|
2d694c6b8a | ||
|
|
3dadf5b54f | ||
|
|
e330771b44 | ||
|
|
03a365256f | ||
|
|
83bb184ff3 | ||
|
|
49c1537b25 | ||
|
|
c9ff6c9a4c | ||
|
|
68fbe2dabf | ||
|
|
d5f4697251 | ||
|
|
fd4dddda6e | ||
|
|
a834a64319 | ||
|
|
dd23b20090 | ||
|
|
490e166f75 | ||
|
|
6651122bb6 | ||
|
|
764fde012c | ||
|
|
abc694307a | ||
|
|
016d07bc4d | ||
|
|
2f9542bee6 | ||
|
|
a637a7f7e7 | ||
|
|
aff2ab1780 | ||
|
|
609f5e8470 | ||
|
|
ba9449db23 | ||
|
|
1b82df8094 | ||
|
|
708b2f2652 | ||
|
|
7a6c87bd8e | ||
|
|
a468904356 | ||
|
|
11c8e46b26 | ||
|
|
af5269e239 | ||
|
|
4397a6cab3 | ||
|
|
949f816a78 | ||
|
|
f40ead9701 | ||
|
|
ce46e8eb31 | ||
|
|
77d8851009 | ||
|
|
d282e6dea6 | ||
|
|
211b72648b | ||
|
|
e65c0a47bb | ||
|
|
768f0427e7 | ||
|
|
fb7dfb3a32 | ||
|
|
089793a486 | ||
|
|
c866d0572e | ||
|
|
7b962ffc4b | ||
|
|
8dc2143080 | ||
|
|
a81de95f73 | ||
|
|
d0b44eada9 | ||
|
|
434029966f | ||
|
|
d6cab311c5 | ||
|
|
676bd24d38 | ||
|
|
5fb17a9244 | ||
|
|
de187559a8 | ||
|
|
7fced84f57 | ||
|
|
1e6c6a8c3c | ||
|
|
426981fe1c | ||
|
|
46c11f3946 | ||
|
|
8fe9864e43 | ||
|
|
8db35ca1af | ||
|
|
77e849f056 | ||
|
|
377b0c9d47 | ||
|
|
0ca5586465 | ||
|
|
1248f85612 | ||
|
|
e57bebd01b | ||
|
|
908d1cbf24 | ||
|
|
97719623a3 | ||
|
|
ccb9b462cc | ||
|
|
127f1ee258 | ||
|
|
1db380d88e | ||
|
|
9ead85987f | ||
|
|
4562d23f6e | ||
|
|
f6f2304c5a | ||
|
|
d194ba5fe1 | ||
|
|
c2a116b713 | ||
|
|
c6f47fc589 | ||
|
|
4533363c49 | ||
|
|
efc847d381 | ||
|
|
b02bee01e5 | ||
|
|
f364ce51c0 | ||
|
|
73ec8deee1 | ||
|
|
649cc888ab | ||
|
|
d03785558b | ||
|
|
75d97ddfc1 | ||
|
|
49addb8d56 | ||
|
|
d862b16ee6 | ||
|
|
6e26e06aea | ||
|
|
7996c48b70 | ||
|
|
9382381144 | ||
|
|
f98a5040ac |
@@ -5,8 +5,6 @@ charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{html,js,json}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
FLASK_APP=lnbits
|
||||
FLASK_ENV=development
|
||||
|
||||
LND_API_ENDPOINT=https://mylnd.io/rest/
|
||||
LND_ADMIN_MACAROON=LND_ADMIN_MACAROON
|
||||
LND_INVOICE_MACAROON=LND_INVOICE_MACAROON
|
||||
LND_READ_MACAROON=LND_READ_MACAROON
|
||||
LNBITS_SITE_TITLE=LNbits
|
||||
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, 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
|
||||
@@ -13,11 +43,3 @@ 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
|
||||
|
||||
LNPAY_API_ENDPOINT=https://lnpay.co/v1/
|
||||
LNPAY_ADMIN_KEY=LNPAY_ADMIN_KEY
|
||||
LNPAY_INVOICE_KEY=LNPAY_INVOICE_KEY
|
||||
LNPAY_READ_KEY=LNPAY_READ_KEY
|
||||
LNPAY_API_KEY=LNPAY_API_KEY
|
||||
|
||||
FEE_RESERVE=0
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
lnbits/static/vendor/** linguist-vendored
|
||||
lnbits/extensions/**/static/vendor/** linguist-vendored
|
||||
@@ -0,0 +1,13 @@
|
||||
name: mypy type checker
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: jpetrucciani/mypy-check@master
|
||||
with:
|
||||
path: 'lnbits'
|
||||
@@ -11,8 +11,9 @@ __pycache__
|
||||
*.egg-info
|
||||
.coverage
|
||||
.pytest_cache
|
||||
.webassets-cache
|
||||
htmlcov
|
||||
Pipfile.lock
|
||||
test-reports
|
||||
|
||||
*.swo
|
||||
*.swp
|
||||
@@ -24,3 +25,5 @@ venv
|
||||
|
||||
*.sqlite3
|
||||
.pyre*
|
||||
|
||||
__bundle__
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM python:3.7
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir -q -r requirements.txt
|
||||
COPY . /app
|
||||
|
||||
EXPOSE 5000
|
||||
@@ -8,10 +8,19 @@ python_version = "3.7"
|
||||
|
||||
[packages]
|
||||
bitstring = "*"
|
||||
cerberus = "*"
|
||||
lnd-grpc = "*"
|
||||
lnurl = "*"
|
||||
flask = "*"
|
||||
flask-assets = "*"
|
||||
flask-compress = "*"
|
||||
flask-talisman = "*"
|
||||
gevent = "*"
|
||||
gunicorn = "*"
|
||||
pylightning = "*"
|
||||
pyscss = "*"
|
||||
requests = "*"
|
||||
shortuuid = "*"
|
||||
|
||||
[dev-packages]
|
||||
black = "==19.10b0"
|
||||
|
||||
@@ -0,0 +1,733 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "1c40bedbf1f2a9e9bccd443b57bd6dda496361df8712ddda5efcec0d00cab9f1"
|
||||
},
|
||||
"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"
|
||||
],
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"bitstring": {
|
||||
"hashes": [
|
||||
"sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443",
|
||||
"sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf",
|
||||
"sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.6"
|
||||
},
|
||||
"cerberus": {
|
||||
"hashes": [
|
||||
"sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
|
||||
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
|
||||
],
|
||||
"version": "==2020.4.5.1"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
|
||||
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
|
||||
],
|
||||
"version": "==7.1.1"
|
||||
},
|
||||
"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:468693f4ddd11ac6a41bca4eb5f94b071b763256d54136f77957cfee635badb3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"flask-talisman": {
|
||||
"hashes": [
|
||||
"sha256:468131464a249274ed226efc21b372518f442487e58918ccab8357eaa638fd1f",
|
||||
"sha256:eaa754f4b771dfbe473843391d69643b79e3a38c865790011ac5e4179c68e3ec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"gevent": {
|
||||
"hashes": [
|
||||
"sha256:0b84a8d6f088b29a74402728681c9f11864b95e49f5587a666e6fbf5c683e597",
|
||||
"sha256:1ef086264e846371beb5742ebaeb148dc96adf72da2ff350ae5603421cdc2ad9",
|
||||
"sha256:2070c65896f89a85b39f49427d6132f7abd047129fc4da88b3670f0ba13b0cf7",
|
||||
"sha256:2fbe0bc43d8c5540153f06eece6235dda14e5f99bdd9183838396313100815d7",
|
||||
"sha256:32813de352918fb652a3db805fd6e08e0a1666a1a9304eef95938c9c426f9573",
|
||||
"sha256:38c45d8a3b647f56f8a68769a8ac4953be84a84735c7c7a4d7ca62022bd54036",
|
||||
"sha256:3b4c4d99f87c0d04b825879c5a91fbfa2b66da7c25b8689e9bdd9f4741d5f80d",
|
||||
"sha256:42cae3be36b7458f411bd589c66aaba27e4e611ec3d3621e37fd732fe383f9b6",
|
||||
"sha256:4572dc7907a0ac3c39b9f0898dbdf390ae3250baaae5f7395661fb844e2e23be",
|
||||
"sha256:6088bedd8b6bcdb815be322304a5d1c028ffa837d84e93b349928dadac62f354",
|
||||
"sha256:8a9aba59a3268f20c7b584119215bdc589cb81500d93dad4dab428eb02f72944",
|
||||
"sha256:8cca7ffd58559f8d51e5605ad73afcc6f348f9747d2fa539b336e70851b69b79",
|
||||
"sha256:956e82a5d0e90f8d71efe4cecccde602cfb657cd866c58bb953c9c30ca1b3d77",
|
||||
"sha256:b0aea12de542f8fcd6882087bdd5b4d7dc8bb316d28181f6b012dd0b91583285",
|
||||
"sha256:b46399f6c9eccc2e6de1dc1057d362be840443e5439b06cce8b01d114ba1a7ec",
|
||||
"sha256:c0b38a654c8fde5b9d9bd27ea3261aeefe36bc9244b170b6d3b11d72a2163bdb",
|
||||
"sha256:c516cc5d70c3faf07f271d50930d144339c69fb80f3cac9b687aa964e518535e",
|
||||
"sha256:c7a62d51c6dca84f91a91b940037523c926a516f0568f47dc1386bd1682cf4e9",
|
||||
"sha256:cea28f958bc4206ae092043e0775cd7a2bb2536bcbece292732c6484c1076c01",
|
||||
"sha256:d56f36eb98532d2bccc51cb0964c31e9fbd9b2282074c297dc9b006b047e2966",
|
||||
"sha256:de6c0cbcb890d0a79323961d3b593a0f2f54dcb9fe38ee5167f2d514e69e3c8c",
|
||||
"sha256:e0990009e7c1624f9a0f3335df1ab8d45678241c852659ac645b70ed8229097c",
|
||||
"sha256:e7d23d5f32c9db6ae49c4b58585618dcafd6ad0babae251c9c8297afebc4744b",
|
||||
"sha256:ee39caf14d66e619709cdfe3962bc68a234518e43ea8c811c0d67a864bc7c196"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.4.0"
|
||||
},
|
||||
"googleapis-common-protos": {
|
||||
"hashes": [
|
||||
"sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e"
|
||||
],
|
||||
"version": "==1.51.0"
|
||||
},
|
||||
"greenlet": {
|
||||
"hashes": [
|
||||
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
|
||||
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
|
||||
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
|
||||
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
|
||||
"sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e",
|
||||
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
|
||||
"sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122",
|
||||
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
|
||||
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
|
||||
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
|
||||
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
|
||||
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
|
||||
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
|
||||
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
|
||||
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
|
||||
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
|
||||
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
|
||||
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
|
||||
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
|
||||
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
|
||||
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656",
|
||||
"sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython'",
|
||||
"version": "==0.4.15"
|
||||
},
|
||||
"grpcio": {
|
||||
"hashes": [
|
||||
"sha256:085bbf7fd0070b8d65e84aa32979f17cfe624d27b5ce23955ef770c19d2d9623",
|
||||
"sha256:0ae207a47ec0ad66eb1f53a27d566674d13a236c62ced409891335318ea9b8c5",
|
||||
"sha256:0c130204ff5de0b9f041bf3126db0d29369d69883592e4b0d3c19868ba0ced7e",
|
||||
"sha256:0ef6b380a588c2c6b29c6cfa0ba7f5d367beb33d5504bcc68658fa241ad498d2",
|
||||
"sha256:16e1edb367763ea08d0994d4635ec05f4f8db9db59c39304b061097e3b93df43",
|
||||
"sha256:16f5523dacae5aaeda4cf900da7e980747f663298c38c18eb4e5317704aa007a",
|
||||
"sha256:181b5078cf568f37915b8a118afcef5fc9f3128c59c38998ed93e7dd793e3928",
|
||||
"sha256:245564713cb4ac7bccb0f11be63781beb62299a44d8ab69031c859dbd9461728",
|
||||
"sha256:271abbe28eb99fa5c70b3f272c0c66b67dab7bb11e1d29d8e616b4e0e099d29a",
|
||||
"sha256:2e1b01cba26988c811c7fb91a0bca19c9afb776cc3d228993f08d324bdd0510a",
|
||||
"sha256:3366bd6412c1e73acb1ee27d7f0c7d7dbee118ad8d98c957c8173691b2effeec",
|
||||
"sha256:3893b39a0a17d857dc3a42fdb02a26aa53a59bfce49987187bcc0261647f1f55",
|
||||
"sha256:3c7864d5ae63b787001b01b376f6315aef1a015aa9c809535235ed0ead907919",
|
||||
"sha256:42c6716adf3ec1f608b2b56e885f26dd86e80d2fc1617f51fc92d1b0b649e28e",
|
||||
"sha256:4bef0756b9e0df78e8d67a5b1e0e89b7daf41525d575f74e1f14a993c55b680d",
|
||||
"sha256:4fe081862e58b8fbef0e479aefc9a64f8f17f53074df1085d8c1fe825a6e5df4",
|
||||
"sha256:505a8d1b4ac571a51f10c4c995d5d4714f03c886604dc3c097ef5fd57bcfcf0b",
|
||||
"sha256:5c2e81b6ab9768c43f2ca1c9a4c925823aad79ae95efb351007df4b92ebce592",
|
||||
"sha256:70ff2df0c1795c5cf585a72d95bb458838b40bad5653c314b9067ba819e918f9",
|
||||
"sha256:97b5612fc5d4bbf0490a2d80bed5eab5b59112ef1640440c1a9ac824bafa6968",
|
||||
"sha256:a35f8f4a0334ed8b05db90383aecef8e49923ab430689a4360a74052f3a89cf4",
|
||||
"sha256:aafe85a8210dfa1da3c46831b7f00c3735240b7b028eeba339eaea6ffdb593fb",
|
||||
"sha256:c2e53eb253840f05278a8410628419ba7060815f86d48c9d83b6047de21c9956",
|
||||
"sha256:c3645887db3309fc87c3db740b977d403fb265ebab292f1f6a926c4661231fd5",
|
||||
"sha256:c6565cc92853af13237b2233f331efdad07339d27fe1f5f74256bfde7dc2f587",
|
||||
"sha256:cbc322c5d5615e67c2a15be631f64e6c2bab8c12505bc7c150948abdaa0bdbac",
|
||||
"sha256:df749ee982ec35ab76d37a1e637b10a92b4573e2b4e1f86a5fa8a1273c40a850",
|
||||
"sha256:e9439d7b801c86df13c6cbb4c5a7e181c058f3c119d5e119a94a5f3090a8f060",
|
||||
"sha256:f493ac4754717f25ace3614a51dd408a32b8bff3c9c0c85e9356e7e0a120a8c8",
|
||||
"sha256:f80d10bdf1a306f7063046321fd4efc7732a606acdd4e6259b8a37349079b704",
|
||||
"sha256:f83b0c91796eb42865451a20e82246011078ba067ea0744f7301e12a94ae2e1b"
|
||||
],
|
||||
"version": "==1.28.1"
|
||||
},
|
||||
"grpcio-tools": {
|
||||
"hashes": [
|
||||
"sha256:095911bd21dd2dc029b8862b44e65b41341135fd16da60ad9d0df4b8e9a0618e",
|
||||
"sha256:17a6b63ee1650abbbc9306f3144d1d3bd3d088a270ca6569afcb68a8cc4a4a38",
|
||||
"sha256:27fcab84ea948d965f82c7994903e8e36a20ce4ad7ef55a22c8d7c639e6c8808",
|
||||
"sha256:2ffa5f6fbd26ea6c7626bf89d6ef4d64d111000a8bd4e9df69b070093de0f85d",
|
||||
"sha256:491dcd98d988c0a4ed9bbb1ec6310588fc0e16b334b64119b973363a1cf96cc1",
|
||||
"sha256:493b5a275ff2692942951926a259e396ff13acec72e6d93498638006b1a1bd71",
|
||||
"sha256:4c980208c1620e52e86c4037058097673ee8e3bbbec34582f2538e614d0c41f4",
|
||||
"sha256:55f3a86c3a808b8a5c2dc8112c45329dd912e43a6fb474da94a7e04d8bb36e1a",
|
||||
"sha256:69a60e1c020547668f313b7a81d653f8c820066ab0f6ed979af760905b61d9e1",
|
||||
"sha256:6f7806fe53e5979fa10ec49cbd5236cf0e57c8817ce54350bccc8ebceaeb54fb",
|
||||
"sha256:8f752fa03eb2d851aac5838aa46a5b831660e9800634fad8ccd46539e9cb4663",
|
||||
"sha256:9210b3575fee355a4056cff8ff7212b823371e9fa9ad1467e3c0c5cb006d0d7d",
|
||||
"sha256:9b900cf90b293acd48bf258d5e416eae9540b5aa0c75cc2fe68a7e2c5f4834ae",
|
||||
"sha256:addc40a47f496cafa108ba1a31838a9832223e0dcafaf424d38e6b8c79fe26ac",
|
||||
"sha256:adf089aaf6e21358b12e39d9fa7c28611340d8399a918c0b72ff122ce9b7e0af",
|
||||
"sha256:b22172e3ad5e849815f3e35a002fb67c52e83cd3e4299cf45760de4674c7cd04",
|
||||
"sha256:b5cf457dcd6499f8c2f2bdcc486d681ca3ee673064484a31c507df6139bf00fe",
|
||||
"sha256:b5ee6f089bb040d175e658911760584c7a6cb70fbdcc78bdbe6349475933483b",
|
||||
"sha256:bb9b1a05917011e98e5331d129a40a41f49c5652e1b86d5fea9b3f6c98454928",
|
||||
"sha256:c527db050fa619a2bd293a73db0e0af42f0bd5a4f41c043fc5da543d81f5fed6",
|
||||
"sha256:c9efe6420d2ffaf9aa32d292817ddfc939b9adf28a51cef30984bab02ed35a58",
|
||||
"sha256:cb7b54a96f4d9aae5d9c6b644a69ce48c12cfa3558c86ce0a62d9c8a08a83f2e",
|
||||
"sha256:d661e5c4f82f9a223b9d1a6c9ec68faea060324e0d25684a6f7fb0b9622ab561",
|
||||
"sha256:d6ee6f338e0099976161e78ecf91d2ab9c04be75a944ef29facc93a4aad28e1d",
|
||||
"sha256:d89dec894495c51ee0a8dd0c896e4320e95bab179c3faa6a439ef5719486c35e",
|
||||
"sha256:dc7ea3739aa6f5abe6e9a27f403b30c7996978176e5884c41ff3d8a153e7c35a",
|
||||
"sha256:de10f8b28ef3e8a7f336e02b7c2912713bd41e0931c4fad3a6993d25c8511159",
|
||||
"sha256:e05f7bfb63b844b7f9d8b50019b02168fd8982278dc0443986f3504b3cb87d2a",
|
||||
"sha256:e942dd349d5be0d5ebbe96ed9cbcfb5fdb0bc7b57357754c42db8083a0ec80aa",
|
||||
"sha256:edc01f022a878722ae69bff3faa8c6ec7e967d57b7e8a5f4b2f6100ad903a978",
|
||||
"sha256:f61d219d06057e015d662c0b40438f119f58e514a5bebf425058a34d05ff21db"
|
||||
],
|
||||
"version": "==1.28.1"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
|
||||
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.0.4"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
],
|
||||
"version": "==2.9"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"lnd-grpc": {
|
||||
"hashes": [
|
||||
"sha256:43114aead2dc2d008751231784fc94027d60569a5e4dc86ed68032a2203fe781",
|
||||
"sha256:c28208d912b28f5b0b11fa87559c6e564731549ecbc52eafe7d5dd2b2f469cf8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"lnurl": {
|
||||
"hashes": [
|
||||
"sha256:633ddedddcff4bd8b7790eba1649ec10c91fb28e690e3f6e984cb8591be3b626",
|
||||
"sha256:749e0d73a8e5d41b03c734df4f22c0e022f1da4f64e600a898b56e1bb75f5dda"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.3"
|
||||
},
|
||||
"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"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
|
||||
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
|
||||
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
|
||||
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
|
||||
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
|
||||
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
|
||||
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
|
||||
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
|
||||
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
|
||||
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
|
||||
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
|
||||
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
|
||||
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
|
||||
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
|
||||
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
|
||||
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
|
||||
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
|
||||
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
|
||||
],
|
||||
"version": "==3.11.3"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:0b7aadfa1de28057656064e04d9f018d1b186fe2a8e953a2fb41545873b7cf95",
|
||||
"sha256:0f61e67291b99a927816558a218a4e794db72a33621c836e63d12613a2202cd4",
|
||||
"sha256:20946280c750753b3e3177c748825ef189d7ab86c514f6a0b118621110d5f0d3",
|
||||
"sha256:22139ee446992c222977ac0a9269c4da2e9ecc1834f84804ebde008a4649b929",
|
||||
"sha256:3c0f39e884d7a3572d5cc8322b0fe9bf66114283e22e05a5c4b8961c19588945",
|
||||
"sha256:446ce773a552a2cb90065d4aa645e16fa7494369b5f0d199e4d41a992a98204d",
|
||||
"sha256:475e6606873e40717cc3b0eebc7d1101cbfc774e01dadeeea24c121eb5826b86",
|
||||
"sha256:66124752662de0479a9d0c17bdebdc8a889bccad8846626fb66d8669e8eafb63",
|
||||
"sha256:896637b7d8e4cdc0bcee1704fcadacdd167c35ac29f02a4395fce7a033925f26",
|
||||
"sha256:9af44d06db33896a2176603c9cb876df3a60297a292a24d3018956a910cc1402",
|
||||
"sha256:9e46fac8a4674db0777fd0133aa56817e1481beee50971bab39dded7639f9b2b",
|
||||
"sha256:ae206e103e976c40ec294cd6c8fcbfbdaced3ab9b736bc53d03fa11b5aaa1628",
|
||||
"sha256:b11d0bd7ecf41098894e8777ee623c29554dbaa37e862c51bcc5a2b950d1bf77",
|
||||
"sha256:d73070028f7b046a5b2e611a9799c238d7bd245f8fe30f4ad7ff29ddb63aac40",
|
||||
"sha256:ddedcdf9d5c24939578449a8e099ceeec3b3d76243fc143aff63ebf6d5aade10",
|
||||
"sha256:e08e21f4d5395ac17cde19de26be63fb16fb870f0cfde1481ddc22d5e2353548",
|
||||
"sha256:e6239199b363bc53262bcb57f1441206d4b2d46b392eccba2213d8358d6e284a"
|
||||
],
|
||||
"version": "==1.5"
|
||||
},
|
||||
"pylightning": {
|
||||
"hashes": [
|
||||
"sha256:5b7df2c6f0faf17450ea3024f29f901a114d4c68da116a1b130958df510a0557",
|
||||
"sha256:6a07b62b27e01baaf51165e2dfb8b7d1d47f61aef8304a9da3ca5081608a32c0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0.7.3"
|
||||
},
|
||||
"pyscss": {
|
||||
"hashes": [
|
||||
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.7"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
|
||||
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.23.0"
|
||||
},
|
||||
"shortuuid": {
|
||||
"hashes": [
|
||||
"sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f",
|
||||
"sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
|
||||
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
|
||||
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.7.4.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
|
||||
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
|
||||
],
|
||||
"version": "==1.25.9"
|
||||
},
|
||||
"webassets": {
|
||||
"hashes": [
|
||||
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
|
||||
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
|
||||
],
|
||||
"version": "==2.0"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
|
||||
],
|
||||
"version": "==1.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
|
||||
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.10b0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
|
||||
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
|
||||
],
|
||||
"version": "==7.1.1"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
|
||||
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
|
||||
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
|
||||
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
|
||||
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
|
||||
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
|
||||
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
|
||||
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
|
||||
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
|
||||
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
|
||||
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
|
||||
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
|
||||
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
|
||||
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
|
||||
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
|
||||
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
|
||||
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
|
||||
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
|
||||
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
|
||||
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
|
||||
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
|
||||
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
|
||||
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
|
||||
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
|
||||
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
|
||||
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
|
||||
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
|
||||
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
|
||||
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
|
||||
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
|
||||
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
|
||||
],
|
||||
"version": "==5.1"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
||||
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.9"
|
||||
},
|
||||
"flake8-mypy": {
|
||||
"hashes": [
|
||||
"sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18",
|
||||
"sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==17.8.0"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
|
||||
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
|
||||
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
|
||||
],
|
||||
"version": "==8.2.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2",
|
||||
"sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1",
|
||||
"sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164",
|
||||
"sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761",
|
||||
"sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce",
|
||||
"sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27",
|
||||
"sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754",
|
||||
"sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae",
|
||||
"sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9",
|
||||
"sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600",
|
||||
"sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65",
|
||||
"sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8",
|
||||
"sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913",
|
||||
"sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"
|
||||
],
|
||||
"version": "==0.770"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
|
||||
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
|
||||
],
|
||||
"version": "==20.3"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
|
||||
"sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
|
||||
],
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
||||
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
||||
],
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
||||
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
|
||||
],
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
|
||||
"sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.4.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
|
||||
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"pytest-sugar": {
|
||||
"hashes": [
|
||||
"sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283",
|
||||
"sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.2"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b",
|
||||
"sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8",
|
||||
"sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3",
|
||||
"sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e",
|
||||
"sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683",
|
||||
"sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1",
|
||||
"sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142",
|
||||
"sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3",
|
||||
"sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468",
|
||||
"sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e",
|
||||
"sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3",
|
||||
"sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a",
|
||||
"sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f",
|
||||
"sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6",
|
||||
"sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156",
|
||||
"sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b",
|
||||
"sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db",
|
||||
"sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd",
|
||||
"sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a",
|
||||
"sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948",
|
||||
"sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"
|
||||
],
|
||||
"version": "==2020.4.4"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"termcolor": {
|
||||
"hashes": [
|
||||
"sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
|
||||
"sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
|
||||
],
|
||||
"version": "==0.1.9"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
|
||||

|
||||
# LNbits
|
||||
Simple free and open-source Python lightning-network wallet/accounts system. Use https://lnbits.com, or run your own LNbits server!
|
||||

|
||||
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
|
||||
https://lnbits.com, or run your own LNbits server!
|
||||
|
||||
LNbits is a very simple server that sits on top of a 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!
|
||||
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 such as LND, lntxbot, paywall, opennode, etc. This first BETA release runs on top of lntxbot, other releases coming soon, although if you are impatient, it could be ported to other funding sources relatively easily. Contributors very welcome :)
|
||||
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.
|
||||
|
||||
LNbits is still in BETA. Please report any vulnerabilities responsibly
|
||||
## 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.
|
||||
@@ -29,7 +30,7 @@ LNURL has a fallback scheme, so if scanned by a regular QR code reader it can de
|
||||

|
||||
https://github.com/btcontract/lnurl-rfc/blob/master/spec.md
|
||||
|
||||
Adding **/lnurl?lightning="LNURL-WITHDRAW"** will trigger a withdraw that builds an LNbits wallet.
|
||||
Adding **/lnurlwallet?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.
|
||||
|
||||

|
||||
@@ -38,7 +39,7 @@ Example use would be an ATM, which utilises LNURL, if the user scans the QR with
|
||||
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
|
||||
Download this repo
|
||||
@@ -61,6 +62,7 @@ You will need to set the variables in .env.example, and rename the file to .env
|
||||
Running the server
|
||||
------------------
|
||||
|
||||
$ flask migrate
|
||||
$ flask run
|
||||
|
||||
There is an environment variable called `FLASK_ENV` that has to be set to `development`
|
||||
|
||||
|
After Width: | Height: | Size: 69 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 314 B |
@@ -1,39 +1,42 @@
|
||||
import importlib
|
||||
import json
|
||||
import requests
|
||||
import uuid
|
||||
|
||||
from flask import Flask, jsonify, redirect, render_template, request, url_for
|
||||
from flask_talisman import Talisman
|
||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
||||
from flask import Flask
|
||||
from flask_assets import Environment, Bundle # type: ignore
|
||||
from flask_compress import Compress # type: ignore
|
||||
from flask_talisman import Talisman # type: ignore
|
||||
from os import getenv
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from . import bolt11
|
||||
from .core import core_app
|
||||
from .db import init_databases, open_db
|
||||
from .helpers import ExtensionManager, megajson
|
||||
from .settings import WALLET, DEFAULT_USER_WALLET_NAME, FEE_RESERVE
|
||||
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__)
|
||||
valid_extensions = [ext for ext in ExtensionManager().extensions if ext.is_valid]
|
||||
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)
|
||||
Talisman(
|
||||
app,
|
||||
force_https=FORCE_HTTPS,
|
||||
content_security_policy={
|
||||
"default-src": [
|
||||
"'self'",
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
"cdnjs.cloudflare.com",
|
||||
"code.ionicframework.com",
|
||||
"code.jquery.com",
|
||||
"blob:",
|
||||
"api.opennode.co",
|
||||
"fonts.googleapis.com",
|
||||
"fonts.gstatic.com",
|
||||
"maxcdn.bootstrapcdn.com",
|
||||
"github.com",
|
||||
"avatars2.githubusercontent.com",
|
||||
]
|
||||
},
|
||||
)
|
||||
@@ -55,449 +58,37 @@ for ext in valid_extensions:
|
||||
# filters
|
||||
# -------
|
||||
|
||||
app.jinja_env.globals["DEBUG"] = app.config["DEBUG"]
|
||||
app.jinja_env.globals["EXTENSIONS"] = valid_extensions
|
||||
app.jinja_env.filters["megajson"] = megajson
|
||||
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
|
||||
# ----
|
||||
|
||||
@app.before_first_request
|
||||
def init():
|
||||
init_databases()
|
||||
|
||||
|
||||
# vvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
# move the rest to `core_app`
|
||||
# vvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
# vvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
|
||||
|
||||
@app.route("/deletewallet")
|
||||
def deletewallet():
|
||||
user_id = request.args.get("usr")
|
||||
wallet_id = request.args.get("wal")
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
next_wallet = db.fetchone("SELECT id FROM wallets WHERE user = ?", (user_id,))
|
||||
|
||||
if next_wallet:
|
||||
return redirect(url_for("wallet", usr=user_id, wal=next_wallet[0]))
|
||||
|
||||
return redirect(url_for("home"))
|
||||
|
||||
|
||||
@app.route("/lnurl")
|
||||
def lnurl():
|
||||
lnurl = request.args.get("lightning")
|
||||
return render_template("lnurl.html", lnurl=lnurl)
|
||||
|
||||
|
||||
@app.route("/lnurlwallet")
|
||||
def lnurlwallet():
|
||||
lnurl = Lnurl(request.args.get("lightning"))
|
||||
r = requests.get(lnurl.url)
|
||||
if not r.ok:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
data = json.loads(r.text)
|
||||
if data.get("status") == "ERROR":
|
||||
return redirect(url_for("home"))
|
||||
|
||||
withdraw_res = LnurlWithdrawResponse(**data)
|
||||
|
||||
_, pay_hash, pay_req = WALLET.create_invoice(withdraw_res.max_sats, "LNbits lnurl funding")
|
||||
|
||||
r = requests.get(
|
||||
withdraw_res.callback.base,
|
||||
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": pay_req}},
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
return redirect(url_for("home"))
|
||||
data = json.loads(r.text)
|
||||
|
||||
for i in range(10):
|
||||
r = WALLET.get_invoice_status(pay_hash).raw_response
|
||||
if not r.ok:
|
||||
continue
|
||||
|
||||
data = r.json()
|
||||
break
|
||||
|
||||
with open_db() as db:
|
||||
wallet_id = uuid.uuid4().hex
|
||||
user_id = uuid.uuid4().hex
|
||||
wallet_name = DEFAULT_USER_WALLET_NAME
|
||||
adminkey = uuid.uuid4().hex
|
||||
inkey = uuid.uuid4().hex
|
||||
|
||||
db.execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
|
||||
db.execute(
|
||||
"INSERT INTO wallets (id, name, user, adminkey, inkey) VALUES (?, ?, ?, ?, ?)",
|
||||
(wallet_id, wallet_name, user_id, adminkey, inkey),
|
||||
)
|
||||
db.execute(
|
||||
"INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, 0, ?)",
|
||||
(pay_hash, withdraw_res.max_sats * 1000, wallet_id, "LNbits lnurl funding",),
|
||||
)
|
||||
|
||||
return redirect(url_for("wallet", usr=user_id, wal=wallet_id))
|
||||
|
||||
|
||||
@app.route("/wallet")
|
||||
def wallet():
|
||||
usr = request.args.get("usr")
|
||||
wallet_id = request.args.get("wal")
|
||||
wallet_name = request.args.get("nme")
|
||||
|
||||
if usr:
|
||||
if not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
if wallet_id:
|
||||
if not len(wallet_id) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# just usr: return a the first user wallet or create one if none found
|
||||
# usr and wallet_id: return that wallet or create it if it doesn't exist
|
||||
# usr, wallet_id and wallet_name: same as above, but use the specified name
|
||||
# usr and wallet_name: generate a wallet_id and create
|
||||
# wallet_id and wallet_name: create a user, then move an existing wallet or create
|
||||
# just wallet_name: create a user, then generate a wallet_id and create
|
||||
# nothing: create everything
|
||||
|
||||
with open_db() as db:
|
||||
# ensure this user exists
|
||||
# -------------------------------
|
||||
|
||||
if not usr:
|
||||
usr = uuid.uuid4().hex
|
||||
return redirect(url_for("wallet", usr=usr, wal=wallet_id, nme=wallet_name))
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO accounts (id) VALUES (?)
|
||||
""",
|
||||
(usr,),
|
||||
)
|
||||
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
|
||||
if not wallet_id:
|
||||
if user_wallets and not wallet_name:
|
||||
# fetch the first wallet from this user
|
||||
# -------------------------------------
|
||||
wallet_id = user_wallets[0]["id"]
|
||||
else:
|
||||
# create for this user
|
||||
# --------------------
|
||||
wallet_name = wallet_name or DEFAULT_USER_WALLET_NAME
|
||||
wallet_id = uuid.uuid4().hex
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO wallets (id, name, user, adminkey, inkey)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(wallet_id, wallet_name, usr, uuid.uuid4().hex, uuid.uuid4().hex),
|
||||
)
|
||||
|
||||
return redirect(url_for("wallet", usr=usr, wal=wallet_id, nme=wallet_name))
|
||||
|
||||
# if wallet_id is given, try to move it to this user or create
|
||||
# ------------------------------------------------------------
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO wallets (id, user, name, adminkey, inkey)
|
||||
VALUES (?, ?,
|
||||
coalesce((SELECT name FROM wallets WHERE id = ?), ?),
|
||||
coalesce((SELECT adminkey FROM wallets WHERE id = ?), ?),
|
||||
coalesce((SELECT inkey FROM wallets WHERE id = ?), ?)
|
||||
)
|
||||
""",
|
||||
(
|
||||
wallet_id,
|
||||
usr,
|
||||
wallet_id,
|
||||
wallet_name or DEFAULT_USER_WALLET_NAME,
|
||||
wallet_id,
|
||||
uuid.uuid4().hex,
|
||||
wallet_id,
|
||||
uuid.uuid4().hex,
|
||||
),
|
||||
)
|
||||
|
||||
# finally, get the wallet with balance and transactions
|
||||
# -----------------------------------------------------
|
||||
|
||||
wallet = db.fetchone(
|
||||
"""
|
||||
SELECT
|
||||
coalesce((SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0) * ? AS balance,
|
||||
*
|
||||
FROM wallets
|
||||
WHERE user = ? AND id = ?
|
||||
""",
|
||||
(1 - FEE_RESERVE, usr, wallet_id),
|
||||
)
|
||||
|
||||
transactions = db.fetchall(
|
||||
"""
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE wallet = ? AND pending = 0
|
||||
ORDER BY time
|
||||
""",
|
||||
(wallet_id,),
|
||||
)
|
||||
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
return render_template(
|
||||
"wallet.html", user_wallets=user_wallets, wallet=wallet, user=usr, transactions=transactions, user_ext=user_ext
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/v1/invoices", methods=["GET", "POST"])
|
||||
def api_invoices():
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
postedjson = request.json
|
||||
|
||||
# Form validation
|
||||
if int(postedjson["value"]) < 0 or not postedjson["memo"].replace(" ", "").isalnum():
|
||||
return jsonify({"ERROR": "FORM ERROR"}), 401
|
||||
|
||||
if "value" not in postedjson:
|
||||
return jsonify({"ERROR": "NO VALUE"}), 400
|
||||
|
||||
if not postedjson["value"].isdigit():
|
||||
return jsonify({"ERROR": "VALUE MUST BE A NUMBER"}), 400
|
||||
|
||||
if int(postedjson["value"]) < 0:
|
||||
return jsonify({"ERROR": "AMOUNTLESS INVOICES NOT SUPPORTED"}), 400
|
||||
|
||||
if "memo" not in postedjson:
|
||||
return jsonify({"ERROR": "NO MEMO"}), 400
|
||||
|
||||
with open_db() as db:
|
||||
wallet = db.fetchone(
|
||||
"SELECT id FROM wallets WHERE inkey = ? OR adminkey = ?",
|
||||
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"],),
|
||||
)
|
||||
|
||||
if not wallet:
|
||||
return jsonify({"ERROR": "NO KEY"}), 200
|
||||
|
||||
r, pay_hash, pay_req = WALLET.create_invoice(postedjson["value"], postedjson["memo"])
|
||||
|
||||
if not r.ok or "error" in r.json():
|
||||
return jsonify({"ERROR": "UNEXPECTED BACKEND ERROR"}), 500
|
||||
|
||||
amount_msat = int(postedjson["value"]) * 1000
|
||||
|
||||
db.execute(
|
||||
"INSERT INTO apipayments (payhash, amount, wallet, pending, memo) VALUES (?, ?, ?, 1, ?)",
|
||||
(pay_hash, amount_msat, wallet["id"], postedjson["memo"],),
|
||||
)
|
||||
|
||||
return jsonify({"pay_req": pay_req, "payment_hash": pay_hash}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/channels/transactions", methods=["GET", "POST"])
|
||||
def api_transactions():
|
||||
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
data = request.json
|
||||
|
||||
print(data)
|
||||
if "payment_request" not in data:
|
||||
return jsonify({"ERROR": "NO PAY REQ"}), 400
|
||||
|
||||
with open_db() as db:
|
||||
wallet = db.fetchone("SELECT id FROM wallets WHERE adminkey = ?", (request.headers["Grpc-Metadata-macaroon"],))
|
||||
|
||||
if not wallet:
|
||||
return jsonify({"ERROR": "BAD AUTH"}), 401
|
||||
|
||||
# decode the invoice
|
||||
invoice = bolt11.decode(data["payment_request"])
|
||||
if invoice.amount_msat == 0:
|
||||
return jsonify({"ERROR": "AMOUNTLESS INVOICES NOT SUPPORTED"}), 400
|
||||
|
||||
# insert the payment
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO apipayments (payhash, amount, fee, wallet, pending, memo) VALUES (?, ?, ?, ?, 1, ?)",
|
||||
(
|
||||
invoice.payment_hash,
|
||||
-int(invoice.amount_msat),
|
||||
-int(invoice.amount_msat) * FEE_RESERVE,
|
||||
wallet["id"],
|
||||
invoice.description,
|
||||
),
|
||||
)
|
||||
|
||||
# check balance
|
||||
balance = db.fetchone("SELECT balance/1000 FROM balances WHERE wallet = ?", (wallet["id"],))[0]
|
||||
if balance < 0:
|
||||
db.execute("DELETE FROM apipayments WHERE payhash = ? AND wallet = ?", (invoice.payment_hash, wallet["id"]))
|
||||
return jsonify({"ERROR": "INSUFFICIENT BALANCE"}), 403
|
||||
|
||||
# check if the invoice is an internal one
|
||||
if db.fetchone("SELECT count(*) FROM apipayments WHERE payhash = ?", (invoice.payment_hash,))[0] == 2:
|
||||
# internal. mark both sides as fulfilled.
|
||||
db.execute("UPDATE apipayments SET pending = 0, fee = 0 WHERE payhash = ?", (invoice.payment_hash,))
|
||||
else:
|
||||
# actually send the payment
|
||||
r = WALLET.pay_invoice(data["payment_request"])
|
||||
|
||||
if not r.raw_response.ok or r.failed:
|
||||
return jsonify({"ERROR": "UNEXPECTED PAYMENT ERROR"}), 500
|
||||
|
||||
# payment went through, not pending anymore, save actual fees
|
||||
db.execute(
|
||||
"UPDATE apipayments SET pending = 0, fee = ? WHERE payhash = ? AND wallet = ?",
|
||||
(r.fee_msat, invoice.payment_hash, wallet["id"]),
|
||||
)
|
||||
|
||||
return jsonify({"PAID": "TRUE", "payment_hash": invoice.payment_hash}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/invoice/<payhash>", methods=["GET"])
|
||||
def api_checkinvoice(payhash):
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
|
||||
with open_db() as db:
|
||||
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (payhash,))
|
||||
|
||||
if not payment:
|
||||
return jsonify({"ERROR": "NO INVOICE"}), 404
|
||||
|
||||
if not payment[0][4]: # pending
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
if not WALLET.get_invoice_status(payhash).settled:
|
||||
return jsonify({"PAID": "FALSE"}), 200
|
||||
|
||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/payment/<payhash>", methods=["GET"])
|
||||
def api_checkpayment(payhash):
|
||||
if request.headers["Content-Type"] != "application/json":
|
||||
return jsonify({"ERROR": "MUST BE JSON"}), 400
|
||||
|
||||
with open_db() as db:
|
||||
payment = db.fetchone(
|
||||
"""
|
||||
SELECT pending
|
||||
FROM apipayments
|
||||
INNER JOIN wallets AS w ON apipayments.wallet = w.id
|
||||
WHERE payhash = ?
|
||||
AND (w.adminkey = ? OR w.inkey = ?)
|
||||
""",
|
||||
(payhash, request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
|
||||
)
|
||||
|
||||
if not payment:
|
||||
return jsonify({"ERROR": "NO INVOICE"}), 404
|
||||
|
||||
if not payment["pending"]: # pending
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
if not WALLET.get_payment_status(payhash).settled:
|
||||
return jsonify({"PAID": "FALSE"}), 200
|
||||
|
||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
||||
return jsonify({"PAID": "TRUE"}), 200
|
||||
|
||||
|
||||
@app.route("/api/v1/checkpending", methods=["POST"])
|
||||
def api_checkpending():
|
||||
with open_db() as db:
|
||||
for pendingtx in db.fetchall(
|
||||
"""
|
||||
SELECT
|
||||
payhash,
|
||||
CASE
|
||||
WHEN amount < 0 THEN 'send'
|
||||
ELSE 'recv'
|
||||
END AS kind
|
||||
FROM apipayments
|
||||
INNER JOIN wallets ON apipayments.wallet = wallets.id
|
||||
WHERE time > strftime('%s', 'now') - 86400
|
||||
AND pending = 1
|
||||
AND (adminkey = ? OR inkey = ?)
|
||||
""",
|
||||
(request.headers["Grpc-Metadata-macaroon"], request.headers["Grpc-Metadata-macaroon"]),
|
||||
):
|
||||
payhash = pendingtx["payhash"]
|
||||
kind = pendingtx["kind"]
|
||||
|
||||
if kind == "send":
|
||||
payment_complete = WALLET.get_payment_status(payhash).settled
|
||||
if payment_complete:
|
||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
||||
elif payment_complete is False:
|
||||
db.execute("DELETE FROM apipayments WHERE payhash = ?", (payhash,))
|
||||
|
||||
elif kind == "recv" and WALLET.get_invoice_status(payhash).settled:
|
||||
db.execute("UPDATE apipayments SET pending = 0 WHERE payhash = ?", (payhash,))
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
# Checks DB to see if the extensions are activated or not activated for the user
|
||||
@app.route("/extensions")
|
||||
def extensions():
|
||||
usr = request.args.get("usr")
|
||||
enable = request.args.get("enable")
|
||||
disable = request.args.get("disable")
|
||||
ext = None
|
||||
|
||||
if usr and not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
if enable and disable:
|
||||
# TODO: show some kind of error
|
||||
return redirect(url_for("extensions"))
|
||||
|
||||
with open_db() as db:
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
|
||||
if enable:
|
||||
ext, value = enable, 1
|
||||
if disable:
|
||||
ext, value = disable, 0
|
||||
|
||||
if ext:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO extensions (user, extension, active)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(usr, ext, value),
|
||||
)
|
||||
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
return render_template("extensions.html", user_wallets=user_wallets, user=usr, user_ext=user_ext)
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dokku": {
|
||||
"predeploy": "flask migrate"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
# type: ignore
|
||||
|
||||
import bitstring
|
||||
import re
|
||||
from binascii import hexlify
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
core_app = Blueprint("core", __name__, template_folder="templates")
|
||||
core_app: Blueprint = Blueprint("core", __name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
from .views.api import * # noqa
|
||||
from .views.generic import * # noqa
|
||||
from .views.lnurl import * # noqa
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
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:
|
||||
check_field = "adminkey" if key_type == "admin" else "inkey"
|
||||
row = db.fetchone(
|
||||
f"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE {check_field} = ?
|
||||
""",
|
||||
(key,),
|
||||
)
|
||||
|
||||
return Wallet(**row) if row else None
|
||||
|
||||
|
||||
# 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,))
|
||||
@@ -0,0 +1,75 @@
|
||||
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)
|
||||
@@ -0,0 +1,80 @@
|
||||
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)
|
||||
@@ -0,0 +1,73 @@
|
||||
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: 23 KiB After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,4 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin]
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,357 @@
|
||||
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 () {
|
||||
try {
|
||||
var invoice = decode(this.send.data.bolt11);
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 3000,
|
||||
type: 'warning',
|
||||
message: error + '.',
|
||||
caption: '400 BAD REQUEST',
|
||||
icon: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
<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>
|
||||
@@ -0,0 +1,42 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,72 @@
|
||||
{% 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/arcbtc/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
|
||||
<q-btn outline
|
||||
color="grey"
|
||||
type="a" href="https://paywall.link/to/f4e4e" 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 %}
|
||||
@@ -0,0 +1,259 @@
|
||||
{% 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.1.1/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.24.0/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.1.1/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,98 +0,0 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block menuitems %}
|
||||
|
||||
<li><a href="https://where39.com/"><p>Where39 anon locations</p><img src="static/where39.png" style="width:170px"></a></li>
|
||||
<li><a href="https://github.com/arcbtc/Quickening"><p>The Quickening <$8 PoS</p><img src="static/quick.gif" style="width:170px"></a></li>
|
||||
<li><a href="http://jigawatt.co/"><p>Buy BTC stamps + electronics</p><img src="static/stamps.jpg" style="width:170px"></a></li>
|
||||
<li><a href="mailto:ben@arc.wales"><h3>Advertise here!</h3></a></li>
|
||||
{% endblock %} {% block body %}
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a href="{{ url_for('core.home') }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
</ol>
|
||||
<br /><br />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<h4>
|
||||
TESTING ONLY - wallet is still in BETA and very unstable
|
||||
</h4>
|
||||
</div></div></div>
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<!-- Default box -->
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
{% block call_to_action %}
|
||||
<h1>
|
||||
<small>Make a wallet</small>
|
||||
</h1>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="walname"
|
||||
placeholder="Name your LNBits wallet"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="newwallet()">
|
||||
Submit
|
||||
</button>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Default box -->
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h1>
|
||||
<a href="index.html" class="logo"><b>LN</b>bits</a>
|
||||
<small>free and open-source lightning wallet</small>
|
||||
</h1>
|
||||
<p>
|
||||
LNbits is a simple, free and open-source lightning-network wallet
|
||||
for bits and bobs. You can run it on your own server, or use this
|
||||
one.
|
||||
<br /><br />
|
||||
The wallet can be used in a variety of ways, an instant wallet for
|
||||
LN demonstrations, a fallback wallet for the LNURL scheme, an
|
||||
accounts system to mitigate the risk of exposing applications to
|
||||
your full balance.
|
||||
<br /><br />
|
||||
The wallet can run on top of LND, lntxbot, paywall, opennode
|
||||
<br /><br />
|
||||
Please note that although one of the aims of this wallet is to
|
||||
mitigate exposure of all your funds, it’s still very BETA and may
|
||||
in fact do the opposite!
|
||||
<br />
|
||||
<a href="https://github.com/arcbtc/lnbits"
|
||||
>https://github.com/arcbtc/lnbits</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
{% endblock %}
|
||||
@@ -1,14 +0,0 @@
|
||||
from flask import render_template, send_from_directory
|
||||
from os import path
|
||||
|
||||
from lnbits.core import core_app
|
||||
|
||||
|
||||
@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("index.html")
|
||||
@@ -0,0 +1,89 @@
|
||||
from flask import g, jsonify, request
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
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()), Status.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)}), Status.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.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)}), Status.BAD_REQUEST
|
||||
except PermissionError as e:
|
||||
return jsonify({"message": str(e)}), Status.FORBIDDEN
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id}), Status.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."}), Status.NOT_FOUND
|
||||
elif not payment.pending:
|
||||
return jsonify({"paid": True}), Status.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}), Status.OK
|
||||
|
||||
if is_paid:
|
||||
payment.set_pending(False)
|
||||
return jsonify({"paid": True}), Status.OK
|
||||
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
@@ -0,0 +1,95 @@
|
||||
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
|
||||
from os import path
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
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(Status.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(Status.NOT_FOUND, "User does not exist.")
|
||||
|
||||
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(Status.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(Status.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"))
|
||||
@@ -0,0 +1,57 @@
|
||||
import requests
|
||||
|
||||
from flask import abort, redirect, request, url_for
|
||||
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.helpers import Status
|
||||
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"), response_class=LnurlWithdrawResponse)
|
||||
except LnurlException:
|
||||
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
|
||||
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(Status.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(Status.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
|
||||
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))
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,47 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT,
|
||||
pass TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extensions (
|
||||
user TEXT NOT NULL,
|
||||
extension TEXT NOT NULL,
|
||||
active BOOLEAN DEFAULT 0,
|
||||
|
||||
UNIQUE (user, extension)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
adminkey TEXT NOT NULL,
|
||||
inkey TEXT
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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;
|
||||
@@ -1,8 +1,7 @@
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from .helpers import ExtensionManager
|
||||
from .settings import LNBITS_PATH, LNBITS_DATA_FOLDER
|
||||
from .settings import LNBITS_DATA_FOLDER
|
||||
|
||||
|
||||
class Database:
|
||||
@@ -19,16 +18,16 @@ class Database:
|
||||
self.cursor.close()
|
||||
self.connection.close()
|
||||
|
||||
def fetchall(self, query: str, values: tuple) -> list:
|
||||
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):
|
||||
def fetchone(self, query: str, values: tuple = ()):
|
||||
self.cursor.execute(query, values)
|
||||
return self.cursor.fetchone()
|
||||
|
||||
def execute(self, query: str, values: tuple) -> None:
|
||||
def execute(self, query: str, values: tuple = ()) -> None:
|
||||
"""Given a query, cursor.execute() it."""
|
||||
self.cursor.execute(query, values)
|
||||
self.connection.commit()
|
||||
@@ -41,21 +40,3 @@ def open_db(db_name: str = "database") -> Database:
|
||||
|
||||
def open_ext_db(extension_name: str) -> Database:
|
||||
return open_db(f"ext_{extension_name}")
|
||||
|
||||
|
||||
def init_databases() -> None:
|
||||
"""Creates the necessary databases if they don't exist already."""
|
||||
"""TODO: see how we can deal with migrations."""
|
||||
|
||||
schemas = [
|
||||
("database", os.path.join(LNBITS_PATH, "data", "schema.sql")),
|
||||
]
|
||||
|
||||
for extension in ExtensionManager().extensions:
|
||||
schemas.append((f"ext_{extension.code}", os.path.join(extension.path, "schema.sql")))
|
||||
|
||||
for schema in [s for s in schemas if os.path.exists(s[1])]:
|
||||
with open_db(schema[0]) as db:
|
||||
with open(schema[1]) as schemafile:
|
||||
for stmt in schemafile.read().split(";\n\n"):
|
||||
db.execute(stmt, [])
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from cerberus import Validator # type: ignore
|
||||
from flask import g, abort, jsonify, request
|
||||
from functools import wraps
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from .helpers import Status
|
||||
|
||||
|
||||
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."}), Status.BAD_REQUEST
|
||||
|
||||
if not g.wallet:
|
||||
return jsonify({"message": "Wrong keys."}), Status.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`."}), Status.BAD_REQUEST
|
||||
|
||||
v = Validator(schema)
|
||||
g.data = {key: request.json[key] for key in schema.keys()}
|
||||
|
||||
if not v.validate(g.data):
|
||||
return jsonify({"message": f"Errors in request data: {v.errors}"}), Status.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(Status.NOT_FOUND, "User not found.")
|
||||
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(Status.BAD_REQUEST, f"`{param}` is required.")
|
||||
|
||||
if value:
|
||||
try:
|
||||
UUID(value, version=version)
|
||||
except ValueError:
|
||||
abort(Status.BAD_REQUEST, f"`{param}` is not a valid UUID.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
return wrap
|
||||
@@ -0,0 +1,11 @@
|
||||
<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>
|
||||
@@ -0,0 +1,8 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "AMilk",
|
||||
"short_description": "Assistant Faucet Milker",
|
||||
"icon": "room_service",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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,))
|
||||
@@ -0,0 +1,21 @@
|
||||
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)
|
||||
@@ -0,0 +1,9 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class AMilk(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
lnurl: str
|
||||
atime: int
|
||||
amount: int
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
<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>
|
||||
</q-card-section>
|
||||
|
||||
</q-card-section></q-expansion-item>
|
||||
@@ -0,0 +1,230 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,21 @@
|
||||
from flask import g, abort, render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.amilk import amilk_ext
|
||||
from lnbits.helpers import Status
|
||||
|
||||
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(Status.NOT_FOUND, "AMilk does not exist.")
|
||||
|
||||
return render_template("amilk/wall.html", amilk=amilk)
|
||||
@@ -0,0 +1,94 @@
|
||||
from flask import g, jsonify, request
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
|
||||
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)]), Status.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(Status.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(Status.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}), Status.OK
|
||||
break
|
||||
|
||||
return jsonify({"paid": True}), Status.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()), Status.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."}), Status.NOT_FOUND
|
||||
|
||||
if amilk.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your amilk."}), Status.FORBIDDEN
|
||||
|
||||
delete_amilk(amilk_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
@@ -0,0 +1,8 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Diagon Alley",
|
||||
"short_description": "Movable anonymous market stand",
|
||||
"icon": "add_shopping_cart",
|
||||
"contributors": ["eillarra"]
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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,))
|
||||
@@ -0,0 +1,64 @@
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
<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: Movable, anonymous market stall</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/>
|
||||
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
|
||||
</q-card>
|
||||
</q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</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>
|
||||
@@ -0,0 +1,660 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1 @@
|
||||
<script>console.log("{{ stall }}")</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
from flask import g, abort, render_template, jsonify
|
||||
import json
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.diagonalley import diagonalley_ext
|
||||
from lnbits.helpers import Status
|
||||
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)
|
||||
@@ -0,0 +1,251 @@
|
||||
from flask import g, jsonify, request
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.helpers import Status
|
||||
|
||||
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)]), Status.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."}), Status.NOT_FOUND
|
||||
|
||||
if product.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw product."}), Status.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()), Status.OK if product_id else Status.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."}), Status.NOT_FOUND
|
||||
|
||||
if product.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Diagon Alley."}), Status.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_product(product_id)
|
||||
|
||||
return "", Status.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)]), Status.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."}), Status.NOT_FOUND
|
||||
|
||||
if indexer.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your withdraw indexer."}), Status.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()), Status.OK if indexer_id else Status.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."}), Status.NOT_FOUND
|
||||
|
||||
if indexer.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_indexer(indexer_id)
|
||||
|
||||
return "", Status.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)]), Status.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()), Status.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."}), Status.NOT_FOUND
|
||||
|
||||
if order.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
|
||||
|
||||
delete_diagonalleys_indexer(order_id)
|
||||
|
||||
return "", Status.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 "", Status.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"])]), Status.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."}), Status.NOT_FOUND
|
||||
|
||||
products = db.fetchone("SELECT * FROM products WHERE wallet = ?", (rows[1],))
|
||||
if not products:
|
||||
return jsonify({"message": "No products"}), Status.NOT_FOUND
|
||||
|
||||
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), Status.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"]}), Status.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}), Status.OK
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
events_ext = Blueprint("events", __name__, static_folder="static", template_folder="templates")
|
||||
events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "LNEVENTS",
|
||||
"short_description": "LN tickets for events",
|
||||
"ion_icon": "calendar"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Events",
|
||||
"short_description": "LN tickets for events.",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Events",
|
||||
"short_description": "LN tickets for events.",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def migrate():
|
||||
print("pending")
|
||||
@@ -455,7 +455,7 @@ background-color:
|
||||
<div id="theform">
|
||||
<br/><br/><br/>
|
||||
<center>
|
||||
|
||||
|
||||
<form role="form">
|
||||
|
||||
<div class="form-group" style="width:300px;">
|
||||
@@ -497,7 +497,7 @@ function postAjax(url, data, thekey, success) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey)
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.send(params)
|
||||
return xhr
|
||||
@@ -513,7 +513,7 @@ function postAjax(url, data, thekey, success) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey)
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
|
||||
xhr.send()
|
||||
@@ -530,7 +530,7 @@ function submitforticket(){
|
||||
JSON.stringify({"unireg": "{{wave }}", "name": nam}),
|
||||
"filla",
|
||||
|
||||
function(data) {
|
||||
function(data) {
|
||||
theinvoice = JSON.parse(data).pay_req
|
||||
thehash = JSON.parse(data).payment_hash
|
||||
|
||||
@@ -549,7 +549,7 @@ function submitforticket(){
|
||||
|
||||
|
||||
document.getElementById('qrcodetxt').innerHTML = theinvoice + "<br/><br/>"
|
||||
|
||||
|
||||
var refreshId = setInterval(function(){
|
||||
|
||||
getAjax('/api/v1/invoice/' + thehash, "{{wave}}", function(datab) {
|
||||
@@ -559,7 +559,7 @@ function submitforticket(){
|
||||
clearInterval(refreshId)
|
||||
}
|
||||
})}, 3000);
|
||||
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- @format -->
|
||||
|
||||
{% extends "base.html" %} {% block messages %}
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
@@ -42,7 +42,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
@@ -63,7 +63,7 @@
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">Lightning tickets</i>
|
||||
@@ -220,13 +220,13 @@
|
||||
$('#datepicker').datepicker({
|
||||
autoclose: true
|
||||
})
|
||||
|
||||
|
||||
|
||||
window.user = {{ user | megajson | safe }}
|
||||
window.user_wallets = {{ user_wallets | megajson | safe }}
|
||||
window.user_ext = {{ user_ext | megajson | safe }}
|
||||
window.user_ev = {{ user_ev | megajson | safe }}
|
||||
|
||||
window.user = {{ user | tojson | safe }}
|
||||
window.user_wallets = {{ user_wallets | tojson | safe }}
|
||||
window.user_ext = {{ user_ext | tojson | safe }}
|
||||
window.user_ev = {{ user_ev | tojson | safe }}
|
||||
|
||||
const user_ev = window.user_ev
|
||||
console.log(user_ev)
|
||||
|
||||
@@ -453,7 +453,7 @@ background-color:
|
||||
<!-- Main content -->
|
||||
<section class="content"><br/><br/>
|
||||
<center><h1 style="font-size:500%">{{ user_ev[0][6] }}</h1></center>
|
||||
|
||||
|
||||
|
||||
<br/><br/><br/>
|
||||
|
||||
@@ -474,13 +474,13 @@ background-color:
|
||||
Scan ticket
|
||||
</button>
|
||||
</center>
|
||||
|
||||
|
||||
|
||||
|
||||
<div id="scantickets"></div>
|
||||
<br/><br/><br/>
|
||||
<div id="qrcodetxt"></div> <br/></center>
|
||||
<br/><br/><br/>
|
||||
|
||||
|
||||
<center>
|
||||
<div class="row" style="width:80%;margin-top:80px">
|
||||
|
||||
@@ -525,8 +525,8 @@ button:focus .ema {
|
||||
<script>
|
||||
|
||||
|
||||
window.user_ev = {{ user_ev | megajson | safe }}
|
||||
window.user_ev_sold = {{ user_ev_sold | megajson | safe }}
|
||||
window.user_ev = {{ user_ev | tojson | safe }}
|
||||
window.user_ev_sold = {{ user_ev_sold | tojson | safe }}
|
||||
console.log(user_ev)
|
||||
console.log(user_ev_sold)
|
||||
|
||||
@@ -578,7 +578,7 @@ function postAjax(url, data, thekey, success) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey)
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.send(params)
|
||||
return xhr
|
||||
@@ -594,7 +594,7 @@ function postAjax(url, data, thekey, success) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey)
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
|
||||
xhr.send()
|
||||
@@ -666,7 +666,7 @@ function scanQRsend() {
|
||||
}
|
||||
|
||||
function cancelsend() {
|
||||
location.reload();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def api_getticket():
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
|
||||
|
||||
|
||||
header = {"Content-Type": "application/json", "Grpc-Metadata-macaroon": user_ev[0][4]}
|
||||
header = {"Content-Type": "application/json", "X-Api-Key": user_ev[0][4]}
|
||||
data = {"value": str(user_ev[0][10]), "memo": user_ev[0][6]}
|
||||
print(url_for("api_invoices", _external=True))
|
||||
r = requests.post(url=url_for("api_invoices", _external=True), headers=header, data=json.dumps(data))
|
||||
|
||||
@@ -8,4 +8,4 @@ Try to include an image
|
||||
|
||||
<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 "Grpc-Metadata-macaroon: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
||||
<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,7 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
example_ext = Blueprint("example", __name__, static_folder="static", template_folder="templates")
|
||||
example_ext: Blueprint = Blueprint("example", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Build your own!",
|
||||
"short_description": "Join us, make an extension",
|
||||
"icon": "info",
|
||||
"contributors": ["github_username"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "SHORT-NAME-FOR-EXTENSIONS-PAGE",
|
||||
"short_description": "BLah blah blah.",
|
||||
"ion_icon": "calendar"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def migrate():
|
||||
print("pending")
|
||||
@@ -1,7 +0,0 @@
|
||||
/* create your extensions table and the variables needed here */
|
||||
CREATE TABLE IF NOT EXISTS example (
|
||||
key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
usr TEXT,
|
||||
wal TEXT,
|
||||
walnme TEXT
|
||||
);
|
||||
@@ -1,103 +1,54 @@
|
||||
<!-- @format -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% extends "base.html" %} {% block messages %}
|
||||
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<span class="label label-danger">!</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="header"><b>Instant wallet, bookmark to save</b></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
|
||||
|
||||
{% block menuitems %}
|
||||
<li class="treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-bitcoin"></i> <span>Wallets</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for w in user_wallets %}
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"><i class="fa fa-bolt"></i> {{ w.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><a onclick="sidebarmake()">Add a wallet +</a></li>
|
||||
<div id="sidebarmake"></div>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="active treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-th"></i> <span>Extensions</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for extension in EXTENSIONS %}
|
||||
{% if extension.code in user_ext %}
|
||||
<li>
|
||||
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"><i class="fa fa-plus"></i> {{ extension.name }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}">Manager</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% block page %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5>
|
||||
<q-list>
|
||||
<q-item v-for="tool in tools" :key="tool.name" tag="a" :href="tool.url" target="_blank">
|
||||
{% raw %} <!-- with raw Flask won't try to interpret the Vue moustaches -->
|
||||
<q-item-section>
|
||||
<q-item-label>{{ tool.name }}</q-item-label>
|
||||
<q-item-label caption>{{ tool.language }}</q-item-label>
|
||||
</q-item-section>
|
||||
{% endraw %}
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
<p>A magical "g" is always available, with info about the user, wallets and extensions:</p>
|
||||
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Withdraw link maker
|
||||
<small>powered by LNURL</small>
|
||||
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">example</i>
|
||||
</li>
|
||||
</ol>
|
||||
<br /><br />
|
||||
|
||||
|
||||
|
||||
<!-- DOWNLOAD AND SEARCH ADMINLITE2 FOR HTML-->
|
||||
|
||||
</section>
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<!-- Small boxes (Stat box) -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- general form elements -->
|
||||
<div class="box box-primary">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title"> EXAMPLE BOX HEADING</h3>
|
||||
</div><!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<p>*Some content in here</p>
|
||||
</div>
|
||||
</div><!-- /.box -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% block scripts %}
|
||||
{{ window_vars(user) }}
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tools: []
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
var self = this;
|
||||
|
||||
//ADD YOUR JAVASCIPT HERE//
|
||||
|
||||
// axios is available for making requests
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: '/example/api/v1/tools',
|
||||
headers: {
|
||||
'X-example-header': 'not-used'
|
||||
}
|
||||
}).then(function (response) {
|
||||
self.tools = response.data;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
#add your dependencies here
|
||||
from flask import g, render_template
|
||||
|
||||
from flask import jsonify, render_template, request, redirect, url_for
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.extensions.example import example_ext
|
||||
|
||||
#add your endpoints here
|
||||
|
||||
@example_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
"""Try to add descriptions for others."""
|
||||
usr = request.args.get("usr")
|
||||
|
||||
if usr:
|
||||
if not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# Get all the data
|
||||
with open_db() as db:
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
return render_template(
|
||||
"example/index.html"
|
||||
)
|
||||
return render_template("example/index.html", user=g.user)
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
#views_api.py is for you API endpoints that could be hit by another service
|
||||
# views_api.py is for you API endpoints that could be hit by another service
|
||||
|
||||
#add your dependencies here
|
||||
# add your dependencies here
|
||||
|
||||
import json
|
||||
import requests
|
||||
from flask import jsonify, render_template, request, redirect, url_for
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
# import json
|
||||
# import requests
|
||||
|
||||
from flask import jsonify
|
||||
|
||||
from lnbits.helpers import Status
|
||||
from lnbits.extensions.example import example_ext
|
||||
|
||||
#add your endpoints here
|
||||
|
||||
@example_ext.route("/api/v1/example", methods=["GET","POST"])
|
||||
# add your endpoints here
|
||||
|
||||
@example_ext.route("/api/v1/tools", methods=["GET"])
|
||||
def api_example():
|
||||
"""Try to add descriptions for others."""
|
||||
#YOUR-CODE
|
||||
|
||||
return jsonify({"status": "TRUE"}), 200
|
||||
tools = [
|
||||
{
|
||||
"name": "Flask",
|
||||
"url": "https://flask.palletsprojects.com/",
|
||||
"language": "Python",
|
||||
},
|
||||
{
|
||||
"name": "Vue.js",
|
||||
"url": "https://vuejs.org/",
|
||||
"language": "JavaScript",
|
||||
},
|
||||
{
|
||||
"name": "Quasar Framework",
|
||||
"url": "https://quasar.dev/",
|
||||
"language": "JavaScript",
|
||||
}
|
||||
]
|
||||
|
||||
return jsonify(tools), Status.OK
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<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>
|
||||
@@ -0,0 +1,8 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
paywall_ext: Blueprint = Blueprint("paywall", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Paywall",
|
||||
"short_description": "Create paywalls for content",
|
||||
"icon": "vpn_lock",
|
||||
"contributors": ["eillarra"]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import Paywall
|
||||
|
||||
|
||||
def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall:
|
||||
with open_ext_db("paywall") as db:
|
||||
paywall_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO paywalls (id, wallet, secret, url, memo, amount)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(paywall_id, wallet_id, urlsafe_short_hash(), url, memo, amount),
|
||||
)
|
||||
|
||||
return get_paywall(paywall_id)
|
||||
|
||||
|
||||
def get_paywall(paywall_id: str) -> Optional[Paywall]:
|
||||
with open_ext_db("paywall") as db:
|
||||
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
|
||||
|
||||
return Paywall(**row) if row else None
|
||||
|
||||
|
||||
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("paywall") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Paywall(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_paywall(paywall_id: str) -> None:
|
||||
with open_ext_db("paywall") as db:
|
||||
db.execute("DELETE FROM paywalls WHERE id = ?", (paywall_id,))
|
||||
@@ -0,0 +1,25 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
"""
|
||||
Initial paywalls table.
|
||||
"""
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS paywalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
memo TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("paywall") as db:
|
||||
m001_initial(db)
|
||||
@@ -0,0 +1,11 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Paywall(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
secret: str
|
||||
url: str
|
||||
memo: str
|
||||
amount: int
|
||||
time: int
|
||||
@@ -0,0 +1,28 @@
|
||||
<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="List paywalls">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create a paywall">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Delete a paywall" class="q-pb-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
@@ -0,0 +1,107 @@
|
||||
{% extends "public.html" %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h5 class="text-subtitle1 q-my-none">{{ paywall.memo }}</h5>
|
||||
<strong class="text-purple">Price: <lnbits-fsat :amount="{{ paywall.amount }}"></lnbits-fsat> sat</strong>
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
<div v-if="paymentReq">
|
||||
<a :href="'lightning:' + paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode :value="paymentReq" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(paymentReq)">Copy invoice</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="redirectUrl">
|
||||
<p>You can access the URL behind this paywall:<br>
|
||||
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong></p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" type="a" :href="redirectUrl">Open URL</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits paywall</h6>
|
||||
</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 {
|
||||
paymentReq: null,
|
||||
redirectUrl: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getInvoice: function () {
|
||||
var self = this;
|
||||
|
||||
axios.get(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice'
|
||||
).then(function (response) {
|
||||
self.paymentReq = response.data.payment_request;
|
||||
|
||||
dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
});
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios.post(
|
||||
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
|
||||
{checking_id: response.data.checking_id}
|
||||
).then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker);
|
||||
dismissMsg();
|
||||
self.redirectUrl = res.data.url;
|
||||
self.$q.localStorage.set('lnbits.paywall.{{ paywall.id }}', res.data.url);
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Payment received!',
|
||||
icon: null
|
||||
});
|
||||
}
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
}, 2000);
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}');
|
||||
|
||||
if (url) {
|
||||
this.redirectUrl = url;
|
||||
} else {
|
||||
this.getInvoice();
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,214 @@
|
||||
{% 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 paywall</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">Paywalls</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="paywalls"
|
||||
row-key="id"
|
||||
:columns="paywallsTable.columns"
|
||||
:pagination.sync="paywallsTable.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="launch" :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="link" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.url" 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="deletePaywall(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 paywall extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "paywall/_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="createPaywall" 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.url"
|
||||
type="url"
|
||||
label="Target URL *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.number="formDialog.data.amount"
|
||||
type="number"
|
||||
label="Amount (sat) *"></q-input>
|
||||
<q-input filled dense
|
||||
v-model.trim="formDialog.data.memo"
|
||||
label="Memo"
|
||||
placeholder="LNbits invoice"></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null"
|
||||
type="submit">Create paywall</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 mapPaywall = 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 = ['/paywall/', obj.id].join('');
|
||||
return obj;
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paywalls: [],
|
||||
paywallsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
|
||||
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
|
||||
{
|
||||
name: 'amount', align: 'right', label: 'Amount (sat)', field: 'fsat', sortable: true,
|
||||
sort: function (a, b, rowA, rowB) {
|
||||
return rowA.amount - rowB.amount;
|
||||
}
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getPaywalls: function () {
|
||||
var self = this;
|
||||
|
||||
LNbits.api.request(
|
||||
'GET',
|
||||
'/paywall/api/v1/paywalls?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
).then(function (response) {
|
||||
self.paywalls = response.data.map(function (obj) {
|
||||
return mapPaywall(obj);
|
||||
});
|
||||
});
|
||||
},
|
||||
createPaywall: function () {
|
||||
var data = {
|
||||
url: this.formDialog.data.url,
|
||||
memo: this.formDialog.data.memo,
|
||||
amount: this.formDialog.data.amount
|
||||
};
|
||||
var self = this;
|
||||
|
||||
LNbits.api.request(
|
||||
'POST',
|
||||
'/paywall/api/v1/paywalls',
|
||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey,
|
||||
data
|
||||
).then(function (response) {
|
||||
self.paywalls.push(mapPaywall(response.data));
|
||||
self.formDialog.show = false;
|
||||
self.formDialog.data = {};
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
},
|
||||
deletePaywall: function (paywallId) {
|
||||
var self = this;
|
||||
var paywall = _.findWhere(this.paywalls, {id: paywallId});
|
||||
|
||||
LNbits.utils.confirmDialog(
|
||||
'Are you sure you want to delete this paywall link?'
|
||||
).onOk(function () {
|
||||
LNbits.api.request(
|
||||
'DELETE',
|
||||
'/paywall/api/v1/paywalls/' + paywallId,
|
||||
_.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey
|
||||
).then(function (response) {
|
||||
self.paywalls = _.reject(self.paywalls, function (obj) { return obj.id == paywallId; });
|
||||
}).catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls);
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getPaywalls();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
from flask import g, abort, render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.helpers import Status
|
||||
|
||||
from lnbits.extensions.paywall import paywall_ext
|
||||
from .crud import get_paywall
|
||||
|
||||
|
||||
@paywall_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
return render_template("paywall/index.html", user=g.user)
|
||||
|
||||
|
||||
@paywall_ext.route("/<paywall_id>")
|
||||
def display(paywall_id):
|
||||
paywall = get_paywall(paywall_id) or abort(Status.NOT_FOUND, "Paywall does not exist.")
|
||||
|
||||
return render_template("paywall/display.html", paywall=paywall)
|
||||
@@ -0,0 +1,89 @@
|
||||
from flask import g, jsonify, request
|
||||
|
||||
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.helpers import Status
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.extensions.paywall import paywall_ext
|
||||
from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_paywalls():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), Status.OK
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls", methods=["POST"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"url": {"type": "string", "empty": False, "required": True},
|
||||
"memo": {"type": "string", "empty": False, "required": True},
|
||||
"amount": {"type": "integer", "min": 0, "required": True},
|
||||
}
|
||||
)
|
||||
def api_paywall_create():
|
||||
paywall = create_paywall(wallet_id=g.wallet.id, **g.data)
|
||||
|
||||
return jsonify(paywall._asdict()), Status.CREATED
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_paywall_delete(paywall_id):
|
||||
paywall = get_paywall(paywall_id)
|
||||
|
||||
if not paywall:
|
||||
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
|
||||
|
||||
if paywall.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your paywall."}), Status.FORBIDDEN
|
||||
|
||||
delete_paywall(paywall_id)
|
||||
|
||||
return "", Status.NO_CONTENT
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["GET"])
|
||||
def api_paywall_get_invoice(paywall_id):
|
||||
paywall = get_paywall(paywall_id)
|
||||
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=paywall.wallet, amount=paywall.amount, memo=f"#paywall {paywall.memo}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.OK
|
||||
|
||||
|
||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
|
||||
@api_validate_post_request(schema={"checking_id": {"type": "string", "empty": False, "required": True}})
|
||||
def api_paywal_check_invoice(paywall_id):
|
||||
paywall = get_paywall(paywall_id)
|
||||
|
||||
if not paywall:
|
||||
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
|
||||
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(g.data["checking_id"]).pending
|
||||
except Exception:
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
|
||||
if is_paid:
|
||||
wallet = get_wallet(paywall.wallet)
|
||||
payment = wallet.get_payment(g.data["checking_id"])
|
||||
payment.set_pending(False)
|
||||
|
||||
return jsonify({"paid": True, "url": paywall.url}), Status.OK
|
||||
|
||||
return jsonify({"paid": False}), Status.OK
|
||||
@@ -1,7 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
tpos_ext = Blueprint("tpos", __name__, static_folder="static", template_folder="templates")
|
||||
tpos_ext: Blueprint = Blueprint("tpos", __name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "TPOS",
|
||||
"short_description": "A shareable POS!",
|
||||
"ion_icon": "calculator"
|
||||
"name": "TPoS",
|
||||
"short_description": "A shareable PoS terminal!",
|
||||
"icon": "dialpad",
|
||||
"contributors": ["talvasconcelos", "arcbtc"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import TPoS
|
||||
|
||||
|
||||
def create_tpos(*, wallet_id: str, name: str, currency: str) -> TPoS:
|
||||
with open_ext_db("tpos") as db:
|
||||
tpos_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO tposs (id, wallet, name, currency)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(tpos_id, wallet_id, name, currency),
|
||||
)
|
||||
|
||||
return get_tpos(tpos_id)
|
||||
|
||||
|
||||
def get_tpos(tpos_id: str) -> Optional[TPoS]:
|
||||
with open_ext_db("tpos") as db:
|
||||
row = db.fetchone("SELECT * FROM tposs WHERE id = ?", (tpos_id,))
|
||||
|
||||
return TPoS(**row) if row else None
|
||||
|
||||
|
||||
def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("tpos") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM tposs WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [TPoS(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_tpos(tpos_id: str) -> None:
|
||||
with open_ext_db("tpos") as db:
|
||||
db.execute("DELETE FROM tposs WHERE id = ?", (tpos_id,))
|
||||
@@ -0,0 +1,22 @@
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
|
||||
def m001_initial(db):
|
||||
"""
|
||||
Initial tposs table.
|
||||
"""
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tposs (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
currency TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def migrate():
|
||||
with open_ext_db("tpos") as db:
|
||||
m001_initial(db)
|
||||
@@ -0,0 +1,8 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class TPoS(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
currency: str
|
||||
@@ -1,9 +0,0 @@
|
||||
/* create your extensions table and the variables needed here */
|
||||
CREATE TABLE IF NOT EXISTS tpos (
|
||||
key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nme TEXT,
|
||||
uni TEXT,
|
||||
usr TEXT,
|
||||
invkey TEXT,
|
||||
cur TEXT
|
||||
);
|
||||