Compare commits

..
762 Commits
Author SHA1 Message Date
Christian RotzollandGitHub f877dde2b0 fix #125 qrcode printing (#126)
Authored-by: rootzoll <rotzoll@MacBook2020.fritz.box>
2020-11-19 22:18:22 -03:00
fiatjaf 594e28eab7 formatting. 2020-11-18 22:26:30 -03:00
fiatjaf a67b93108b log errors on faulty lnpay webhooks. 2020-11-12 15:33:02 -03:00
fiatjaf a198842f50 wallet name on <title> 2020-11-12 15:32:41 -03:00
fiatjafandGitHub 22b31a91c7 Merge pull request #122 from lnbits/lnurl-auth 2020-11-12 10:02:55 -03:00
fiatjaf 54881d777e lnurl-auth: show wallet name on modal. 2020-11-11 22:43:01 -03:00
fiatjaf b7e337b0ee lnurl-auth: show the correct wallet linking_key on modal. 2020-11-11 22:42:40 -03:00
fiatjaf b794f8302d lnurl-auth: hashing_key -> linking_key. 2020-11-11 22:37:55 -03:00
fiatjaf 805000cd06 fix ecdsa signing because these libraries are too naïve. 2020-11-11 02:41:41 -03:00
fiatjaf bb94dc6526 fix perform_lnurl error handling. 2020-11-11 02:40:41 -03:00
fiatjaf d4e30356c7 fix: return "message" instead of "error" so it is handled better at the client. 2020-11-10 22:59:50 -03:00
fiatjaf 4456dd6f59 catch stderr when running git subprocess outside of a git repo. 2020-11-10 22:29:52 -03:00
fiatjaf eaec3480e6 lnurl-auth from lnbits wallets to services. 2020-11-10 11:36:10 -03:00
fiatjaf 8f1ae1646e opennode: fix webhook handling (json -> urlencoded data). 2020-11-09 09:38:06 -03:00
benarc d75c69cd10 fixed first opennode bug
Opennode funding source isnt working, there are a few bugs. This fixes the first
2020-11-08 23:15:27 +00:00
fiatjaf 2e12c0ba3c lnurlp: actually fix what the previous commit did not. 2020-11-03 19:47:13 -03:00
fiatjaf 8947358c80 lnurlp: fix ?amount satoshi -> msatoshi conversion. 2020-11-03 19:12:10 -03:00
benarc 091f9b74f3 Fixed LNURL bug 2020-11-02 21:15:47 +00:00
benarc cf3c5d83fe Fixed LNURLw QR bug 2020-11-02 15:52:56 +00:00
fiatjaf 2863653261 lnurlp: accept comments, USD prices, min/max ranges. 2020-10-22 15:58:15 -03:00
fiatjaf 2552fd8fc9 internal payments get reported on async listeners. 2020-10-22 15:36:37 -03:00
fiatjaf a773584f6f tags and extras metadata in payment details dialog. 2020-10-21 09:50:45 -03:00
fiatjaf 85ec111ff0 return proper error codes on /lnurlscan
so the notification works.
2020-10-20 23:19:23 -03:00
fiatjaf 52619ecc0f improve querystring handling on lnurl fallback scheme. 2020-10-18 13:33:05 -03:00
fiatjaf 11e04e4314 fix CSS bundle order issue.
a final fix for https://github.com/lnbits/lnbits/issues/99
2020-10-17 15:24:23 -03:00
fiatjaf 32d75f7a60 support the lnurl fallback scheme.
completing https://github.com/lnbits/lnbits/issues/63
2020-10-17 15:24:10 -03:00
fiatjaf 435f5405c6 minor prettier change. 2020-10-16 14:45:28 -03:00
fiatjaf a46ca45de0 some readme and docs clarification. 2020-10-15 23:12:32 -03:00
fiatjaf 9cb58833ad make httpx calls async when doing them from view functions.
this is important so lnbits can call itself without hanging forever.

and these functions are already async so it should have been this way since ever.
2020-10-15 13:04:56 -03:00
fiatjaf c81e61e4fe guards and safety timeouts around paymentChecker intervals. 2020-10-15 12:32:28 -03:00
fiatjaf bdbb58b2b0 wrap memo columns in payments table. 2020-10-15 00:59:00 -03:00
fiatjaf be7d36214a use payments/sse on the core wallet UI.
still fallback to the invoice polling (now with a 5 seconds interval
because less than that is too annoying).

this fixes issues with /lnurlwallet invoices not getting paid in time,
so we update the UI automatically when they do get paid.

(see https://t.me/lnbits/7069)
2020-10-15 00:18:57 -03:00
fiatjaf f02ec67f35 add lntxbot invoices listener. 2020-10-14 23:58:57 -03:00
fiatjaf d528d4e09e fix: wallet layout. 2020-10-14 23:13:55 -03:00
fiatjaf ec4b558638 fix: domain variable assignment in pay_lnurl. 2020-10-14 22:44:23 -03:00
fiatjaf 0dc60d4795 increase timeouts for http wallets when stuff may take more than 5 seconds.
before we were using requests which had no default timeouts, but httpx has a
default timeout of 5 seconds. should have noticed that earlier.

when the timeout expires we are left with a pending payment on the db with a
temporary checking_id so we can never know if it was completed or not.

this is still an issue, because technically a lightning payment may take 2 weeks
or more, and we must have a way to dispatch a payment and check for it later.

that should be the default (and we already do check for the payment status later,
so half of the work is done), but on the other hand backends like lnpay and
opennode do not give us a checking_id before the thing is already settled.
2020-10-13 20:39:24 -03:00
fiatjafandGitHub 3a56aaa3ad Merge pull request #113 from lnbits/lnurl 2020-10-13 20:31:00 -03:00
fiatjaf e1501e8ed6 fix bug with lnurl-withdraw dialog being skipped directly to the previous invoice shown. 2020-10-13 15:20:58 -03:00
fiatjaf 1529ebb689 lnurlpay comment. 2020-10-13 15:18:34 -03:00
fiatjaf bcdc065cc0 mypy fixes. 2020-10-13 14:46:23 -03:00
fiatjaf cf0bd7ece8 displaying lnurlpay success_actions. 2020-10-13 13:57:26 -03:00
Gabriel Comteandfiatjaf c6bd060d19 docs: fix typo in command 2020-10-13 07:42:01 -03:00
fiatjaf 8d135489ab fix: invoice parsing JS bug from the lnurl implementation. 2020-10-12 23:21:45 -03:00
fiatjaf 69063190ab also catch httpx.ConnectError whenever we do catch httpx.RequestError. 2020-10-12 23:21:45 -03:00
fiatjaf bc2207ba27 actually paying and withdrawing with lnurl. 2020-10-12 23:21:45 -03:00
fiatjaf 3cd15c40fc lnurl-pay and lnurl-withdraw UI. 2020-10-12 23:21:45 -03:00
fiatjaf 7a5159f293 send/create/scan buttons for clear LNURL support. 2020-10-12 23:21:45 -03:00
Arcandfiatjaf fa8713de17 move scan to outside of receive. 2020-10-12 23:21:45 -03:00
fiatjaf b1f30390b0 retrieve preimage from wallet backends, save it and show on UI dialog.
closes https://github.com/lnbits/lnbits/issues/77
2020-10-12 23:19:16 -03:00
fiatjaf f2ea8a0c74 fix lndrest payment checking: b64/hex mismatch.
finally fixes https://github.com/lnbits/lnbits/issues/55
and https://github.com/lnbits/lnbits/issues/110
2020-10-12 23:18:58 -03:00
fiatjaf b5a07c7ae7 add status() method to wallets to be used in initial check. 2020-10-12 22:30:19 -03:00
fiatjaf d5d85d16e6 fix: auth headers variable name on opennode.py 2020-10-12 20:59:09 -03:00
fiatjaf 2894944b1e pending=False on /sse payments. 2020-10-09 22:05:39 -03:00
fiatjaf 31820e041c /sse returns payment as objects instead of arrays. 2020-10-09 21:08:01 -03:00
fiatjaf 1d00060419 harden sse connections to backends a little. 2020-10-09 20:55:58 -03:00
fiatjaf 7f41d73a84 SSE-based invoices stream for LNbitsWallet. 2020-10-09 17:06:50 -03:00
fiatjaf f638a1088e lnurlp: fix querystring handling in success_url. 2020-10-09 16:40:49 -03:00
fiatjaf ea3418c21d lnurlp: support success_text and success_url. 2020-10-09 16:17:16 -03:00
fiatjaf f6bcff01f4 presort vendored paths list.
an attempt to fix https://github.com/lnbits/lnbits/issues/99.
2020-10-08 22:10:21 -03:00
fiatjaf 33fa540744 adjust docs to match 91853 2020-10-08 21:51:40 -03:00
fiatjaf 6f72f1fe6d fix typo: LND_MACAROON -> LND_REST_MACAROON 2020-10-08 16:24:23 -03:00
fiatjaf 76633fb71f lndrest: check last 20 payments instead of first 20.
fixes https://github.com/lnbits/lnbits/issues/110
2020-10-08 16:08:13 -03:00
fiatjaf 9185342c72 simplify environment variables required.
instead of multiple keys/macaroons with different permissions we request only one.
if someone wants to use lnbits with an invoice macaroon they're free to do it and
we will just fail on 'pay' methods, as before.

this also grandfathers the previous environment variables names so everything keeps
working without people having to change their setups.

in the meantime some bugs with lntxbot and c-lightning were fixed and the `requests`
dependency was eliminated because I can't organize myself into meaningful chunks of
changes.
2020-10-08 16:03:21 -03:00
HamishMacEwanandfiatjaf fc6e49b821 Change .env variable names to match lnbits.py
The required values are expected in LNBITS_INVOICE_KEY & LNBITS_ADMIN_KEY according to https://github.com/lnbits/lnbits/blob/master/lnbits/wallets/lnbits.py
2020-10-07 01:27:16 -03:00
fiatjaf 35f8e3334c fix typo: LnbitsWallet -> LNbitsWallet. 2020-10-06 23:04:25 -03:00
fiatjaf 0b1f3c22e0 lndhub: fix "token" -> "refresh_token". 2020-10-06 21:53:43 -03:00
fiatjaf 24c8201920 add current git commit to summary. 2020-10-06 17:42:33 -03:00
fiatjaf 9a16dfa960 remove annoying warnings and unnecessary prints.
show a summary of some settings on startup when running __main__.py
2020-10-06 12:59:08 -03:00
fiatjaf c5352c0309 register channel listeners instead of callbacks.
makes for a little less black magic and more reasonable use of nurseries
and less unnecessary pseudo-requests.
2020-10-06 01:50:56 -03:00
fiatjaf 95e8573ff8 SSE endpoint for paid invoices.
also move very essential stuff from core/tasks.py to tasks.py so things
are more organized.
2020-10-06 00:40:18 -03:00
fiatjaf 4e68a0e7e6 lndrest: two small fixes. 2020-10-05 22:43:49 -03:00
fiatjaf 16541d40c6 show more data in txinfo modal and fee on main table.
- also center-align icons in main table.
- and use "x time ago" instead of raw dates by default in main table
  (raw date is accessible on a tooltip, on the modal and on the CSV export).
2020-10-05 22:38:06 -03:00
fiatjaf bb965e4119 lndrest: account for when there are pending checking_ids from other sources. 2020-10-05 22:16:28 -03:00
fiatjaf 5ead82f57c fix /lnurlwallet again. db.commit() and wait 3 seconds.
should finally fix https://github.com/lnbits/lnbits/issues/103
2020-10-05 20:33:01 -03:00
fiatjaf 1eac618a25 lnurlp: allow creation of pay_links without webhook_url. 2020-10-05 20:12:24 -03:00
fiatjafandGitHub e1744caec5 Merge pull request #101 from lnbits/invoice-listeners 2020-10-05 13:40:31 -03:00
fiatjaf 39c331a7b9 fix opennode stupid syntax errors. 2020-10-05 08:46:20 -03:00
fiatjaf 6e6ce093f0 guard against lndrest URLs without a leading http... 2020-10-05 08:45:57 -03:00
fiatjaf 1ce9936561 better handling of missing dependencies on lndgrpc. 2020-10-05 08:45:32 -03:00
fiatjaf e408febf97 hypercorn -k trio 2020-10-04 23:09:11 -03:00
fiatjaf c3ded859df fix requirements after rebase. 2020-10-04 14:07:52 -03:00
fiatjaf f5b8ed8fc6 make lndgrpc work using the purerpc library and a manually-declared method. 2020-10-04 12:06:54 -03:00
fiatjaf 9994e61615 migrate to trio so c-lightning sockets stop hanging. 2020-10-04 12:06:20 -03:00
fiatjaf e74cf33f90 broken invoice listener on c-lightning and other fixes around wallets. 2020-10-04 12:05:59 -03:00
fiatjaf b3c69ad49c invoice listeners support on lnd and other fixes around wallets/ 2020-10-04 12:05:01 -03:00
fiatjaf 90c640b659 fix async so now it is a mess that works. 2020-10-04 12:05:01 -03:00
fiatjaf 28e6f40bea async fixes on _invoice_listener. 2020-10-04 12:05:01 -03:00
fiatjaf bbe8d05af4 migrate from aiohttp to httpx following master. 2020-10-04 12:04:59 -03:00
fiatjaf 2c92205703 async invoice listeners through webhooks: lnpay and opennode. 2020-10-04 12:04:49 -03:00
fiatjaf 74117ffc57 lnurlp webhooks. 2020-10-04 12:04:49 -03:00
fiatjaf 04222f1f01 basic invoice listeners. 2020-10-04 12:04:47 -03:00
fiatjaf e2f6c20e3b update docs so master is runnable. 2020-10-02 14:59:29 -03:00
fiatjaf 08b2ce5c05 update requirements.txt 2020-10-02 08:40:50 -03:00
benarc 5971eefe44 Fixed register page blank bug 2020-10-01 19:44:04 +01:00
benarc a11b967ffc Merge remote-tracking branch 'origin/master' 2020-10-01 19:40:16 +01:00
ArcandGitHub f6e6bf73d3 Pointed people towards a stable release 2020-10-01 18:12:29 +01:00
benarc f2619a4bbc piplockupdtae 2020-09-30 19:22:19 +01:00
fiatjaf 88b8826bfd fix balance calculation vue glitch. 2020-09-29 20:04:02 -03:00
fiatjaf 211ac0391b overengineered async fix for /lnurlwallet internal hanging. 2020-09-29 18:24:08 -03:00
fiatjaf e0b8470d40 show unpaid invoices, balance calculated on server-side so isn't affected. 2020-09-29 15:43:11 -03:00
fiatjaf 2dfae9ecc1 adapt Procfile hypercorn call. 2020-09-28 21:11:34 -03:00
Eneko Illarramendi 3ab29e6e50 fix: lnbits.com deploy 2020-09-28 23:49:11 +02:00
EnekoandGitHub f60cebb072 fix: add missing assets command (#100) 2020-09-28 23:11:59 +02:00
benarc d8da88aefa Fixed QR codes not printing 2020-09-28 16:59:20 +01:00
fiatjaf 49baa07141 fix proxyfix. 2020-09-28 00:21:53 -03:00
Eneko Illarramendiandfiatjaf 098089af75 PREFER_SECURE_URLS, get rid of FORCE_HTTPS hacks. 2020-09-24 13:08:59 -03:00
fiatjaf 39cd96594e pass appropriate parameters to create_payment on lnurlwallet. 2020-09-24 13:09:02 -03:00
fiatjafandGitHub dbabf937c4 Merge pull request #93 from lnbits/quart 2020-09-17 22:51:19 -03:00
fiatjaf cd8a447ba0 pytest quibbles and dev dependencies. 2020-09-17 22:41:30 -03:00
fiatjaf ef0bdf8363 mypy quibbles. 2020-09-16 20:24:17 -03:00
fiatjaf 75047c5bc3 fix static extensions js url. 2020-09-16 20:21:42 -03:00
fiatjaf 2b38207100 mimic x_proto, x_host params from old ProxyFix. 2020-09-15 17:19:34 -03:00
fiatjaf 5513b34a66 change 1 to true on env vars because that is supported now. 2020-09-15 17:19:11 -03:00
fiatjaf 6928f431a7 poor man's flask-assets. 2020-09-15 15:54:05 -03:00
fiatjaf 3a0762ff82 proxy fix (x-forwarded-...). 2020-09-14 23:56:25 -03:00
fiatjaf 42c6620a07 quart run settings and hypercorn on docker/Procfile. 2020-09-14 23:28:14 -03:00
fiatjaf f01028eac7 migrate from flask to quart.
also remove all flaskiness from static file serving.
and reference all vendored scripts on the base tempĺate for simplicity.
2020-09-14 16:03:25 -03:00
fiatjaf f452b9c00d withdraw: replace location.hostname with location.host.
fixes https://github.com/lnbits/lnbits/issues/88
2020-09-14 15:46:25 -03:00
fiatjaf 1e2dd412c7 lnurlp: remove wrong /pay/ segments from docs. 2020-09-14 09:35:58 -03:00
fiatjaf e6ad42dbe1 update prettier to 2.1.1 2020-09-14 09:14:51 -03:00
fiatjaf 1faf0b152c remove annoying print(). 2020-09-13 21:48:46 -03:00
fiatjafandfiatjaf f4fffc19aa lndhub extension frontend. 2020-09-12 21:55:13 -03:00
fiatjafandfiatjaf 6210305791 checking pending invoices and payments from lndhub interface. 2020-09-12 21:55:13 -03:00
fiatjafandfiatjaf ce038193cf lndhub extension backend. 2020-09-12 21:55:13 -03:00
fiatjaf 5ac91ee2cf db commits at the right places when sending payments.
fixing vulnerabilities introduced in https://github.com/lnbits/lnbits/pull/90
2020-09-11 21:25:44 -03:00
fiatjafandfiatjaf 4855e2cd3d make every request a database transaction. 2020-09-11 14:17:09 -03:00
fiatjafandfiatjaf 487f082eb2 take extra care on check_internal() 2020-09-11 14:17:09 -03:00
frennkieandGitHub a8e1aa3154 Consider port number on lnurlp URL
fix #86
2020-09-09 21:43:34 -03:00
ArcandGitHub afb2cc4339 Update .env.example 2020-09-08 09:55:31 +01:00
ArcandGitHub 6b94abcfb9 Moved install guide to more prominent position 2020-09-08 09:30:23 +01:00
supertestnetandGitHub bd48bdc04d Update .env.example due to recent changes (#91)
The name of the flask app has changed and the word "all" has been eliminated as unnecessary because allowing anyone to use lnbits is the default.
2020-09-07 16:05:50 +02:00
fiatjaf 130beaf38a fix undeclared ok error. 2020-09-06 23:39:46 -03:00
fiatjaf ab6229806c fix: column is called hash, not payment_hash. 2020-09-06 22:15:29 -03:00
Eneko Illarramendiandfiatjaf d62031b068 chore: let's just call it flask_migrate
I think it should stay, because it can come handy for some deployment methods.
2020-09-06 21:06:01 -03:00
Eneko Illarramendiandfiatjaf 1bc5e144d3 refactor: clean up __init__ file following some Flask conventions
Flask extensions are loaded in a way that makes them easily reusable by blueprints.
In this commit we are also adding `environs` to manage .env and settings:
breaking changes!

- FLASK_APP=lnbits.app
- LNBITS_ALLOWED_USERS needs to be empty now to allow all users (NOT "all")
2020-09-06 21:06:01 -03:00
fiatjafandfiatjaf ffa3c3f6a6 python -m lnbits 2020-09-04 21:33:30 -03:00
fiatjafandfiatjaf b8de30423f remove walrus operator because python3.7 2020-09-04 21:33:30 -03:00
fiatjafandfiatjaf c965bca41d migration tracking. 2020-09-04 21:33:30 -03:00
Eneko Illarramendi f238b3d1ef fix: TypedDict import 2020-09-04 21:24:30 +02:00
Eneko Illarramendi 6b33f150c6 fix: revert default python version to 3.7 in Pipfile, closes #85
But tests will be run on both 3.7 and 3.8 as we want to support >= 3.7
2020-09-04 21:19:06 +02:00
fiatjaf 8b7028d728 add VoidWallet and make it the default. 2020-09-03 21:43:32 -03:00
Eneko Illarramendi 23cfe0d417 chore: update github workflows 2020-09-03 23:10:41 +02:00
Eneko Illarramendi a651f747ac chore: remove gunicorn and gevent from minimum requirements
It is now up to the user to install these (or other) for production deployment.
More info: https://flask.palletsprojects.com/en/1.1.x/tutorial/deploy/#run-with-a-production-server
2020-09-03 19:38:01 +02:00
fiatjaf dcd3f80974 fix: invoice.payee is None by default. 2020-09-03 08:53:17 -03:00
fiatjafandGitHub 6513908a8d Merge pull request #81 from lnbits/internalpaymentsmegachanges 2020-09-03 00:29:20 -03:00
fiatjaf 753e9c5b15 make linting workflow reflect what is done locally (Makefile). 2020-09-02 23:16:23 -03:00
fiatjaf dc3d96c6a8 fix many mypy complaints, specially on bolt11.py 2020-09-02 22:21:56 -03:00
fiatjaf ce28db76c9 add a dialog with payment details for each payment.
for outgoing payments this needs a preimage to be good,
but we don't have it yet because we don't get it from
backends.
2020-09-02 19:19:19 -03:00
fiatjaf 197af922d0 use {"tag": ext} for extension-related payments. 2020-09-02 12:44:54 -03:00
fiatjaf 4447a48724 deleting expired invoices based on their actual expiry date.
also fixes a possible bug that could have caused pending outgoing
payments to be deleted and affecting the balance. probably never happened.
2020-09-02 11:10:48 -03:00
fiatjaf d2650d6e2c don't try to check payments that start with temp_ or internal_. 2020-09-02 11:10:48 -03:00
fiatjaf b56877d470 make db.py context manager operate in a transaction. 2020-09-02 11:10:48 -03:00
fiatjaf bf3c44b3c4 extra fields on apipayments + index payments by payment_hash 2020-09-02 11:10:48 -03:00
arcbtcandfiatjaf c96b22664e internal payments. 2020-09-02 11:10:48 -03:00
Eneko Illarramendi d4f957a5c8 chore: update js vendors 2020-09-02 14:39:20 +02:00
Eneko Illarramendi 0d0e00e699 chore: update Pipfile 2020-09-02 14:31:44 +02:00
fiatjaf b4a6b45645 only check pending payments on "check_pending". 2020-08-31 22:55:19 -03:00
fiatjaf fadddc995a get_wallet_payments with more fine-grained, explicit filters. 2020-08-31 22:16:19 -03:00
fiatjaf d09e624eb6 fix: wallet.balance computed property self-reference. 2020-08-31 16:19:27 -03:00
fiatjaf 5d523519c3 bring back code mysteriously deleted in 79d5befe0c
fixes https://github.com/lnbits/lnbits/issues/73
2020-08-31 00:09:34 -03:00
fiatjaf 68b0adfe66 fix: description_hash as an optional param to create_invoice.
fixes https://github.com/lnbits/lnbits/issues/74
2020-08-30 23:54:50 -03:00
fiatjaf 660d56d400 run black on everything. 2020-08-30 23:19:43 -03:00
fiatjaf 2cecaa229b bolt11.py now supports everything. 2020-08-30 17:40:28 -03:00
fiatjaf ac20e06dc3 fix: correct db.cursor() usage on lnurlp. 2020-08-29 22:33:01 -03:00
fiatjaf 79d5befe0c mypy on Makefile and apply prettier. 2020-08-29 14:10:26 -03:00
fiatjafandGitHub 479760c5a5 Merge pull request #43 from fiatjaf/lnurlpayserver
description_hash support, spark backend and lnurlp extension.
2020-08-29 14:02:59 -03:00
fiatjaf 3ce1e72e10 lnurlp lnurl explanation slightly different from lnurlw. 2020-08-29 14:06:07 -03:00
fiatjaf 1e4f39954e fix: spark create_invoice error case. 2020-08-29 13:48:16 -03:00
fiatjaf 01b19add79 format: prettier lnurlp files. 2020-08-29 13:41:59 -03:00
fiatjaf 96b80fb3fc lnurlp: store description as memo. 2020-08-29 13:11:19 -03:00
fiatjaf 3645e0ac38 fix: lnpay invoices with description_hash. 2020-08-29 13:04:11 -03:00
fiatjaf 0c7284be31 description_hash fix. 2020-08-29 12:23:17 -03:00
fiatjaf 0de08dda0b add spark backend and fix c-lightning. 2020-08-29 12:23:01 -03:00
fiatjaf a79ec8f4a4 lnurlp extension. 2020-08-28 23:03:12 -03:00
arcbtc 6cf488ea4f Fixed lnurlw bugs 2020-08-19 08:49:52 +01:00
Eneko Illarramendi e01de0eae5 chore: update requirements (but lock gevents)
`gevents` has been locked to avoid myNode installation problems.
2020-08-18 08:43:04 +02:00
ArcandGitHub 5650731bce Update index.html 2020-08-16 23:25:54 +01:00
ArcandGitHub 53d65c0258 Update index.html 2020-08-16 23:22:32 +01:00
ArcandGitHub c4db2d5c28 Update index.html 2020-08-16 23:21:44 +01:00
arcbtc b97595b06d Added "quick vouchers" option to withdraw ext 2020-08-16 22:46:06 +01:00
arcbtc dd2a282158 Fixed migration bug 2020-08-13 23:43:42 +01:00
arcbtc 3c7e4a3eaf Fixed events migration 2020-08-13 22:01:33 +01:00
arcbtc 89c31139f8 Merge remote-tracking branch 'origin/master' 2020-08-13 21:50:16 +01:00
arcbtc 2d580a0d0e Fixed lntickets migration 2020-08-13 21:49:50 +01:00
Eneko Illarramendi 0497c603c0 fix: lnurl bugfix
This is not using `$ pipenv update` because apparently other package updates break mynode installations, see #65
2020-08-13 22:19:36 +02:00
Eneko Illarramendi 5831fe2ab6 Revert "fix: requirements update, including lnurl bugfix"
This reverts commit bc984ab0db.
2020-08-13 22:16:16 +02:00
ArcandGitHub dfdb890751 Merge pull request #62 from losnappas/patch-1
Update extensions.md
2020-08-10 19:49:18 +01:00
arcbtc 3735fe7aa1 Merge remote-tracking branch 'origin/master' 2020-08-10 19:45:20 +01:00
arcbtc 97a13526d9 Made print outs bigger 2020-08-10 19:44:55 +01:00
ArcandGitHub be04623b9d Update README.md 2020-08-10 19:27:37 +01:00
arcbtc c56784340e LNtickets now works 2020-08-10 19:08:44 +01:00
arcbtc 0603e525ff Fixed LNticket extension 2020-08-10 19:06:56 +01:00
arcbtc 410e96771e Events extension works
Little bug fix
2020-08-10 15:39:04 +01:00
arcbtc 784a2dd876 Events extension now works 2020-08-10 15:27:30 +01:00
ArcandGitHub bc930ab606 Update README.md 2020-08-10 11:55:15 +01:00
ArcandGitHub f518e350a6 Update README.md 2020-08-10 11:54:28 +01:00
ArcandGitHub 8055e6a73c Update README.md 2020-08-08 21:17:07 +01:00
ArcandGitHub 6b2061f107 Update README.md 2020-08-08 21:16:46 +01:00
ArcandGitHub ce2f764432 Update README.md
Added some props to our forebears
2020-08-08 21:16:01 +01:00
losnappasandGitHub efe4b91877 Update extensions.md
Add a skeleton, better than nothing.
2020-08-07 07:37:41 +00:00
ArcandGitHub 55a9dc749a Merge pull request #56 from frennkie/fix-withdraw
fix https being stripped from QRCodes when showing print view
2020-08-04 21:19:52 +01:00
Eneko Illarramendi bc984ab0db fix: requirements update, including lnurl bugfix 2020-08-04 19:22:24 +02:00
Robert Habermann 1e36a8a393 add missing protocol (to ensure http - or more likely - https://) 2020-08-02 20:01:07 +02:00
benarc c17ac32c86 Fixed LNURLw migration 2020-07-22 20:17:37 +01:00
benarc c73fbe1b27 Fixed unique LNURLw for print 2020-07-20 22:08:54 +01:00
benarc 2ddc23de04 LNURLw fix 2020-07-20 21:32:55 +01:00
benarc 294beb2fca Updated Withdraw extension to handle unique facets 2020-07-20 21:03:23 +01:00
ArcandGitHub 6c286cd265 Merge pull request #51 from frennkie/patch-1
Update index.js to include port in links
2020-07-16 12:17:58 +01:00
frennkieandGitHub 68e41789ce Update index.js
`location.host` contains the hostname and the port while `location.hostname` is only the hostname
2020-07-12 16:24:18 +02:00
EnekoandGitHub e746dcd787 feat: enable code scanning 2020-07-06 18:51:02 +02:00
fiatjaf 24dc6e5415 fix validation rules so description_hash works. 2020-07-04 22:28:08 -03:00
fiatjaf bc27293315 support description_hash across all APIs. 2020-07-03 20:04:04 -03:00
ArcandGitHub 61f736878c Update installation.md 2020-06-22 15:15:27 +01:00
ArcandGitHub f51d44e99b Update installation.md 2020-06-22 15:14:51 +01:00
benarc 95b0ba536b Prettier 2020-06-15 16:53:34 +01:00
benarc 5e81c64831 Usermanager extension update 2020-06-15 16:35:30 +01:00
benarc d56c2627f4 Endpoint for updating user extensions vie usermanager 2020-06-15 16:30:24 +01:00
Dennis ReimannandGitHub c3281399d2 Improve setup instructions and README
- README cleanups and link to documentation
- Improve dev setup instructions
- Fix js linting errors
- Move local setup instructions to installation docs
- Project link fix, closes #42
- Notes about frontend
- Clarify different LND backends
2020-06-12 20:58:37 +02:00
Eneko Illarramendi 8ea72c9934 feat: add CORS headers 2020-05-18 23:33:07 +02:00
Eneko Illarramendi aed2414f3a fix: show better errors when a LNURL-withdraw wallet cannot be created, closes #39 2020-05-18 06:33:02 +02:00
benarc 76f17d8594 Removed API docs for Support Tickets 2020-05-11 13:21:03 +01:00
benarc 85e617aae9 Updated redme 2020-05-11 13:14:25 +01:00
benarc 68d94a43d0 Support Ticket name change 2020-05-11 13:11:36 +01:00
benarc 29dc33dd71 Merge remote-tracking branch 'origin/master' 2020-05-11 12:35:29 +01:00
benarc a54bff6640 Changed extension name to Support Tickets 2020-05-11 12:34:35 +01:00
benarc d2874a90e9 Working Events extension 2020-05-11 12:34:35 +01:00
benarc c099885bbb Changed extension name to Support Tickets 2020-05-11 12:34:16 +01:00
benarc b296c38795 Working events extension 2020-05-11 12:16:26 +01:00
benarc 9af4161679 Redirect feedback 2020-05-10 18:46:58 +01:00
benarc 76510b7892 Events extension mostly functional
Can register attendees via camera
2020-05-10 18:10:45 +01:00
benarc ea3b858695 Replaced events extension with almost working vuejs one 2020-05-09 22:27:11 +01:00
Eneko Illarramendi 41277e6931 fix(paywall): disable create button when an invoice already exists 2020-05-09 22:52:30 +02:00
Eneko Illarramendi 8add56a24c feat: allow limiting the use of LNbits to some user uuids
The `LNBITS_ALLOWED_USERS` env var can be used to limit access
to the application to some uuids only. "all" gives open access.
UUIDs should be comma separated.
2020-05-09 22:30:33 +02:00
ArcandGitHub e768e4b075 Update views_api.py 2020-05-09 18:07:31 +01:00
ArcandGitHub a40823de43 Update lntxbot.py 2020-05-09 17:29:24 +01:00
EnekoandGitHub 171e639109 chore: add paywall address to github funding 2020-05-09 15:23:22 +02:00
ArcandGitHub 6f4cac0c41 Update index.html 2020-05-08 22:19:43 +01:00
ArcandGitHub 97dff38af4 Update README.md 2020-05-08 22:17:43 +01:00
ArcandGitHub 13f01dfbe6 Add files via upload 2020-05-08 21:03:18 +01:00
Eneko Illarramendi 14d61eaf56 feat(withdraw): type casting 2020-05-08 21:12:31 +02:00
Eneko Illarramendi 63b9741c14 feat(tpos): type casting 2020-05-08 21:11:59 +02:00
Eneko Illarramendi 22451ca806 fix: allow empty values (nullable) 2020-05-08 21:10:04 +02:00
Eneko Illarramendi e73a508011 feat(paywall): improved extension
- make remember cookie optional
- improve database
- improve type casting
2020-05-08 21:05:32 +02:00
ArcandGitHub 2fac47c05a Update _api_docs.html 2020-05-05 20:53:13 +01:00
ArcandGitHub b8dd151043 Updated api docs to include listing specific LNURL 2020-05-05 20:07:54 +01:00
Eneko IllarramendiandGitHub 42d307adf9 Merge pull request #38 from fiatjaf/adminkey-to-inkey
allow inkey endpoints to be accessed with adminkeys.
2020-05-05 11:09:02 +02:00
fiatjaf c3ce18c8d9 allow inkey endpoints to be accessed with adminkeys. 2020-05-05 05:46:05 -03:00
Eneko IllarramendiandGitHub d914c3f701 Merge pull request #36 from sgeisler/2020-05-domain-https
Use https in base url to avoid mixed content
2020-05-04 18:34:22 +02:00
Sebastian Geisler 52aff1128e use https base url to avoid mixed content 2020-05-04 18:25:16 +02:00
Eneko Illarramendi 9eb253fe55 fix: typo 2020-05-04 17:34:53 +02:00
Eneko Illarramendi 35fb524e1e Update CNAME 2020-05-04 17:31:53 +02:00
Eneko Illarramendi 01c8e8a23f Create CNAME 2020-05-04 17:27:49 +02:00
Eneko Illarramendi b37af0c600 feat: use github pages and jekyll for docs 2020-05-04 17:20:36 +02:00
Eneko Illarramendi a61d346669 chore: update js vendors 2020-05-04 15:33:39 +02:00
Eneko IllarramendiandGitHub 8530413537 Merge pull request #35 from sgeisler/2020-05-js-formatting-ci
Add JS formatting enforcenemt to CI
2020-05-04 14:56:43 +02:00
Sebastian Geisler 3a6034e8f6 Add prettier JS formatting enforcenemt to CI 2020-05-04 11:40:44 +02:00
Eneko IllarramendiandGitHub d9fe80d41b Merge pull request #34 from fiatjaf/prettier
apply prettier to everything.
2020-05-03 23:40:54 +02:00
fiatjaf 4730500ed7 apply prettier to everything. 2020-05-03 13:55:17 -03:00
Eneko Illarramendi 083f7a0a8d chore: use standard library's HTTP status codes 2020-05-03 15:57:05 +02:00
ArcandGitHub 3a1167c892 Rename config.json.example to config.json 2020-05-02 20:17:33 +01:00
Eneko IllarramendiandGitHub 236bcac263 Merge pull request #26 from eillarra/less_requirements
refactor: remove wallet only dependencies from core
2020-05-02 19:25:47 +02:00
Eneko Illarramendi 2c5e539795 refactor: make some wallet dependencies optional 2020-05-02 19:16:10 +02:00
ArcandGitHub 5553ed82f0 Update index.html 2020-05-01 09:48:00 +01:00
ArcandGitHub 9fadb68fed Update index.html 2020-05-01 06:28:45 +01:00
ArcandGitHub d496869572 Update _api_docs.html 2020-05-01 06:12:55 +01:00
ArcandGitHub 9f33ed49e5 Update README.md 2020-05-01 06:12:20 +01:00
ArcandGitHub ea480866ea Update README.md 2020-05-01 06:10:44 +01:00
ArcandGitHub 690b33e096 Rename config.json to config.json.example 2020-05-01 06:10:16 +01:00
ArcandGitHub d72678972d Add files via upload 2020-05-01 06:09:51 +01:00
Eneko Illarramendi 8dfdd15adb fix: self host icon fonts 2020-04-29 13:24:49 +02:00
Eneko Illarramendi 8442f277c4 tests: only use github for CI 2020-04-28 23:09:59 +02:00
Eneko IllarramendiandGitHub 8f76da66e8 Merge pull request #25 from sgeisler/2020-04-ci-lnpay
Use LNPay backend for CI
2020-04-28 08:49:30 +02:00
Sebastian Geisler f7ca28db34 Use LNPay backend for CI
As a quickfix to make travis happy we'll
use an empty LNPay wallet as backend in tests.
This should be replaced with a mock backend
later on.
2020-04-28 00:00:56 +02:00
Eneko IllarramendiandGitHub c951577962 Merge pull request #19 from frennkie/allow-lightning-scheme
Allow lightning scheme
2020-04-28 00:00:19 +02:00
Robert Habermann 0847a1c933 add lightning scheme check 2020-04-27 23:13:42 +02:00
ArcandGitHub f731bd766e Update and rename config.json to config.json.example 2020-04-27 22:01:21 +01:00
ArcandGitHub 59819e2875 Update _api_docs.html 2020-04-27 21:08:31 +01:00
ArcandGitHub e7184346ab Update _api_docs.html 2020-04-27 21:08:17 +01:00
Eneko Illarramendi 178fe65fb2 test: configure Travis for CI 2020-04-27 19:39:00 +02:00
ArcandGitHub 1c8beba80b Added warning to use https or TOR for lnurl 2020-04-27 17:43:29 +01:00
ArcandGitHub 1516a14a0d Added warning to use https or TOR for lnurl 2020-04-27 17:43:15 +01:00
Eneko IllarramendiandGitHub 0197c6c44a Merge pull request #23 from sgeisler/mypy_fixes
Fix mypy errors
2020-04-27 14:39:34 +02:00
Eneko IllarramendiandSebastian Geisler c3e337a319 fix: mypy errors 2020-04-26 16:58:29 +02:00
ArcandGitHub 976a3d4e5c Update lndrest.py 2020-04-26 01:10:57 +01:00
Eneko Illarramendi 4b4a297c3f refactor(paywall): remove unnecessary hashing paranoia 2020-04-25 23:55:07 +02:00
ArcandGitHub 5d128523c7 Update lndrest.py 2020-04-25 22:41:27 +01:00
ArcandGitHub b34a0a9835 Update .env.example 2020-04-25 22:39:30 +01:00
ArcandGitHub 99cfdd5531 Update __init__.py 2020-04-25 22:11:27 +01:00
ArcandGitHub 435f9c9773 Add files via upload 2020-04-25 22:10:45 +01:00
Eneko Illarramendi ab190454f4 fix: LNbits backend wallet 2020-04-25 20:13:49 +02:00
Eneko IllarramendiandGitHub 5fe64d324d Merge pull request #16 from sgeisler/2020-04-ci
Add type checking CI
2020-04-25 09:28:38 +02:00
Sebastian Geisler 19fa6c2bf9 Add type checking CI 2020-04-25 00:24:59 +02:00
Eneko Illarramendi efd2cb7170 fix: use a fee reserve and actual wallet fees 2020-04-24 22:45:09 +02:00
Eneko Illarramendi 4995ab5aaf chore: linguist config 2020-04-24 15:17:05 +02:00
ArcandEneko Illarramendi 7acbe9f93c Update README.md 2020-04-24 14:56:10 +02:00
ArcandEneko Illarramendi 122281ba81 Update README.md 2020-04-24 14:56:10 +02:00
Eneko Illarramendi 97f4fabea1 fix: create temporal payments to keep balance updated 2020-04-24 14:56:10 +02:00
ArcandGitHub 2c30209cba Update README.md 2020-04-24 09:42:40 +01:00
ArcandGitHub 9f22c0395e Update README.md 2020-04-23 22:43:30 +01:00
ArcandGitHub 2cf5b13b93 Update README.md 2020-04-23 22:42:50 +01:00
ArcandGitHub bedf01b1b6 Update README.md 2020-04-23 22:29:07 +01:00
ArcandGitHub eff41ddf8d Update README.md 2020-04-23 22:27:45 +01:00
ArcandGitHub 19e439c9a2 Update README.md 2020-04-23 21:51:55 +01:00
ArcandGitHub fb86aad6a9 Update README.md 2020-04-23 21:47:48 +01:00
ArcandGitHub e226df2894 Update README.md 2020-04-23 21:46:00 +01:00
ArcandGitHub d15ba01f94 Update README.md 2020-04-23 21:40:51 +01:00
Eneko Illarramendi 897644956a feat: only dhow disclaimer if SERVICE_FEE has been set 2020-04-23 21:25:55 +02:00
Eneko Illarramendi f02526585f docs: add LNbits logos to repo 2020-04-23 18:42:27 +02:00
Eneko Illarramendi 4bf93d36c6 refactor: change the WITH_ONION env var and use FORCE_HTTPS instead 2020-04-23 18:37:22 +02:00
Eneko Illarramendi e461e34fc0 feat(vue): move confirmDialog to LNbits.utils so we can reuse the styling 2020-04-23 07:32:45 +02:00
Eneko Illarramendi 34e6d2a7bb chore: update requirements 2020-04-23 07:32:05 +02:00
Eneko Illarramendi 1978cafe44 chore: update supporter logos 2020-04-23 07:23:45 +02:00
ArcandGitHub cd22aaded9 Update index.html 2020-04-22 20:55:51 +01:00
Eneko Illarramendi 498eceeb46 feat: use dynamic root in api docs 2020-04-22 21:49:31 +02:00
ArcandGitHub 39041ffb5a Update _api_docs.html 2020-04-22 20:41:18 +01:00
ArcandGitHub 0c88aa8fcf Update views_api.py 2020-04-22 19:47:30 +01:00
Eneko Illarramendi eb938c11a8 chore(paywall): minor updates 2020-04-21 23:47:21 +02:00
Eneko Illarramendi b1da103080 chore: minor tweaks 2020-04-21 23:47:21 +02:00
Eneko Illarramendi d90ae26062 style: icon change 2020-04-21 23:43:15 +02:00
Eneko Illarramendi 403385c205 feat: paywall extension 2020-04-21 23:42:41 +02:00
Eneko Illarramendi fd4dc6c48f chore: remove pending macaroon mentions 2020-04-21 23:22:01 +02:00
Eneko Illarramendi e1c5e970c4 feat: allow disabling extensions at installation level 2020-04-21 23:17:11 +02:00
Eneko Illarramendi f12f52db2d fix: homepage bug 2020-04-21 23:17:11 +02:00
Eneko Illarramendi 8d328c5465 refactor: remove legacy code 2020-04-21 23:17:11 +02:00
Eneko Illarramendi efb9deb028 feat: remove FEE_RESERVE 2020-04-21 23:17:11 +02:00
Eneko Illarramendi 5806beaeb7 feat(core): move disclaimer to wallet page 2020-04-21 23:15:50 +02:00
Eneko Illarramendi d917feeb02 feat: prepare for deploying to Dokku 2020-04-21 23:13:37 +02:00
Eneko Illarramendi efbba20872 style: minor tweaks 2020-04-21 23:13:37 +02:00
Eneko Illarramendi 93e51a7398 fix: wallet button 2020-04-21 23:13:37 +02:00
Eneko Illarramendi 94e524bf4c chore: clean requirements 2020-04-21 08:17:55 +02:00
Eneko Illarramendi 364b6dc857 fix(vue): use @submit to send forms, and catch users pressing return 2020-04-21 08:16:56 +02:00
ArcandGitHub bb2d1ea140 Update .env.example 2020-04-20 20:35:59 +01:00
ArcandGitHub 409122271a Update index.html 2020-04-20 15:29:01 +01:00
ArcandGitHub 07b4d52432 Update index.html 2020-04-20 15:27:46 +01:00
ArcandGitHub ae778c19a1 Update index.js 2020-04-20 15:27:02 +01:00
ArcandGitHub 65c17488e3 Update index.html 2020-04-20 14:19:38 +01:00
ArcandGitHub 98f7dc8eff Add files via upload 2020-04-20 14:00:38 +01:00
ArcandGitHub 6bdf4e22e4 Delete quick.gif 2020-04-20 13:56:18 +01:00
ArcandGitHub 549976b9d0 Delete stamps.jpg 2020-04-20 13:56:06 +01:00
ArcandGitHub f0cbaea0b1 Delete where39.png 2020-04-20 13:55:54 +01:00
ArcandGitHub ca08dc2302 Update index.html 2020-04-20 13:48:46 +01:00
ArcandGitHub 7a1477c056 Update _api_docs.html 2020-04-20 08:37:27 +01:00
ArcandGitHub 27b02e85db Update _tpos.html 2020-04-20 08:36:27 +01:00
ArcandGitHub d82e06a4f9 Update views_api.py 2020-04-20 08:29:19 +01:00
ArcandGitHub 39098b2432 Update index.html 2020-04-20 08:28:12 +01:00
ArcandGitHub 5aab51b78c Update index.html 2020-04-20 08:27:08 +01:00
ArcandGitHub 5f9c9c9df6 Create _tpos.html 2020-04-20 08:26:21 +01:00
ArcandGitHub 40fc7ad08f Create _api_docs.html 2020-04-20 08:25:45 +01:00
ArcandGitHub 51587d7de2 Update views_api.py 2020-04-20 08:00:22 +01:00
Eneko Illarramendi 1154d632a2 chore: update currencies list from OpenNode 2020-04-19 23:36:57 +02:00
ArcandGitHub 1862cccb2a Update index.js 2020-04-19 20:56:54 +01:00
ArcandGitHub c33a45d20e Update _lnurl.html 2020-04-19 20:54:39 +01:00
ArcandGitHub 0178097ee6 Added admin to some of the calls 2020-04-19 20:54:11 +01:00
Eneko Illarramendi 57c26ed2bb chore: outline some buttons 2020-04-18 23:17:16 +02:00
Eneko Illarramendi 9e98dae317 chore: update js dependencies 2020-04-18 09:18:56 +02:00
ArcandGitHub aa13551bd3 Update config.json 2020-04-18 01:39:10 +01:00
ArcandGitHub b52618f6f5 Update config.json 2020-04-18 01:38:25 +01:00
ArcandGitHub 5c34c23617 Update crud.py 2020-04-18 01:37:31 +01:00
ArcandGitHub 5b16bd0224 Update migrations.py 2020-04-18 01:37:10 +01:00
ArcandGitHub 316b2256e4 Update models.py 2020-04-18 01:36:50 +01:00
ArcandGitHub 10b8f90fcc Update views_api.py 2020-04-18 01:35:59 +01:00
ArcandGitHub b38e6bf566 Update index.html 2020-04-18 01:35:14 +01:00
Eneko Illarramendi d4c9043278 feat(core): delete expired payments 2020-04-17 21:13:57 +02:00
Eneko Illarramendi d03e472cc2 feat(core): add search to payments table 2020-04-17 21:13:32 +02:00
Eneko Illarramendi de75057a15 chore: remove legacy frontend libraries 2020-04-17 20:43:14 +02:00
Eneko Illarramendi a455379ee1 refactor(tpos): migrate extension 2020-04-17 20:39:23 +02:00
ArcandGitHub 2d694c6b8a Update index.html 2020-04-17 18:52:07 +01:00
ArcandGitHub 3dadf5b54f Update crud.py 2020-04-17 18:51:24 +01:00
ArcandGitHub e330771b44 Update migrations.py 2020-04-17 18:51:06 +01:00
ArcandGitHub 03a365256f Update models.py 2020-04-17 18:50:47 +01:00
ArcandGitHub 83bb184ff3 Update views_api.py 2020-04-17 18:48:17 +01:00
ArcandGitHub 49c1537b25 Update index.html 2020-04-16 23:26:31 +01:00
ArcandGitHub c9ff6c9a4c Update and rename config.json.example to config.json 2020-04-16 23:03:12 +01:00
ArcandGitHub 68fbe2dabf Rename config.json to config.json.example 2020-04-16 22:12:02 +01:00
ArcandGitHub d5f4697251 Added Diagon Alley extension 2020-04-16 21:51:17 +01:00
Eneko Illarramendi fd4dddda6e chore: rename api-key-macaroon 2020-04-16 20:58:16 +02:00
Eneko Illarramendi a834a64319 refactor(withdraw): migrate extension to Vue 2020-04-16 20:58:16 +02:00
Eneko Illarramendi dd23b20090 chore: various tweaks 2020-04-16 20:58:16 +02:00
Eneko Illarramendi 490e166f75 refactor: improve database migrations 2020-04-16 20:58:16 +02:00
ArcandGitHub 6651122bb6 Update _api_docs.html 2020-04-16 12:04:17 +01:00
ArcandGitHub 764fde012c Update decorators.py 2020-04-16 12:03:00 +01:00
ArcandGitHub abc694307a Update base.js 2020-04-16 12:02:20 +01:00
ArcandGitHub 016d07bc4d Add files via upload 2020-04-14 11:39:54 +01:00
ArcandGitHub 2f9542bee6 Update index.html 2020-04-14 04:07:47 +01:00
ArcandGitHub a637a7f7e7 Update tpos.html 2020-04-14 04:06:19 +01:00
ArcandGitHub aff2ab1780 Update index.html 2020-04-14 03:50:01 +01:00
ArcandGitHub 609f5e8470 Update index.html 2020-04-13 22:33:37 +01:00
ArcandGitHub ba9449db23 Update views.py 2020-04-13 22:31:54 +01:00
ArcandGitHub 1b82df8094 Update tpos.html 2020-04-13 22:31:30 +01:00
ArcandGitHub 708b2f2652 Update lndgrpc.py 2020-04-13 22:23:42 +01:00
ArcandGitHub 7a6c87bd8e Update tpos.html 2020-04-13 21:38:15 +01:00
ArcandGitHub a468904356 Update views.py 2020-04-13 21:36:14 +01:00
ArcandGitHub 11c8e46b26 Update views_api.py 2020-04-13 21:35:35 +01:00
Eneko Illarramendi af5269e239 refactor: add core.utils for creating and paying invoices
This methods can be used by extensions too.
2020-04-11 23:15:24 +02:00
Eneko Illarramendi 4397a6cab3 feat: use cerberus schemas to validate POST data 2020-04-11 20:16:54 +02:00
ArcandGitHub 949f816a78 Update index.html 2020-04-10 22:06:56 +01:00
ArcandGitHub f40ead9701 Update tpos.html 2020-04-10 22:06:36 +01:00
ArcandGitHub ce46e8eb31 Create crud.py 2020-04-10 22:05:50 +01:00
ArcandGitHub 77d8851009 Create models.py 2020-04-10 22:05:31 +01:00
ArcandGitHub d282e6dea6 Update schema.sql 2020-04-10 22:05:11 +01:00
ArcandGitHub 211b72648b Update views.py 2020-04-10 22:04:48 +01:00
ArcandGitHub e65c0a47bb Update views_api.py 2020-04-10 22:04:29 +01:00
Eneko Illarramendi 768f0427e7 feat(paywall): extension basics 2020-04-05 12:20:03 +02:00
Eneko Illarramendi fb7dfb3a32 chore: minor fixes 2020-04-05 12:19:25 +02:00
ArcandGitHub 089793a486 Update wallet.js 2020-04-04 23:38:52 +01:00
ArcandGitHub c866d0572e Added top margin to modals 2020-04-04 22:53:28 +01:00
ArcandGitHub 7b962ffc4b Update views_api.py 2020-04-04 00:35:39 +01:00
Eneko Illarramendi 8dc2143080 chore: update requirements 2020-04-03 22:52:08 +02:00
Eneko Illarramendi a81de95f73 chore(vue): use g for consistency with Flask 2020-04-03 22:38:50 +02:00
Eneko Illarramendi d0b44eada9 chore: brokenbits example extension 2020-04-03 22:28:25 +02:00
ArcandGitHub 434029966f Update index.html 2020-04-03 20:36:44 +01:00
ArcandGitHub d6cab311c5 Update views.py 2020-04-03 20:00:16 +01:00
ArcandGitHub 676bd24d38 Update views.py 2020-04-03 19:59:40 +01:00
ArcandGitHub 5fb17a9244 Update index.html 2020-04-03 18:52:26 +01:00
ArcandGitHub de187559a8 Update lnbits.py 2020-04-03 13:50:54 +01:00
ArcandGitHub 7fced84f57 Update .env.example 2020-04-03 13:48:24 +01:00
ArcandGitHub 1e6c6a8c3c Update __init__.py 2020-04-03 13:46:40 +01:00
ArcandGitHub 426981fe1c Create lnbits.py 2020-04-03 13:46:14 +01:00
ArcandGitHub 46c11f3946 Update wallet.html 2020-04-03 13:16:23 +01:00
ArcandGitHub 8fe9864e43 Update wallet.html
Changed value to amount, added a curl example
2020-04-03 12:57:36 +01:00
ArcandGitHub 8db35ca1af Update lndgrpc.py 2020-04-03 12:27:07 +01:00
ArcandGitHub 77e849f056 Update clightning.py 2020-04-03 12:12:21 +01:00
ArcandGitHub 377b0c9d47 Update clightning.py 2020-04-03 11:48:41 +01:00
ArcandGitHub 0ca5586465 Update .env.example 2020-04-03 11:47:59 +01:00
ArcandGitHub 1248f85612 Update clightning.py 2020-04-03 11:27:55 +01:00
ArcandGitHub e57bebd01b Update clightning.py 2020-04-03 11:27:36 +01:00
ArcandGitHub 908d1cbf24 Update .env.example 2020-04-03 11:24:49 +01:00
ArcandGitHub 97719623a3 Update __init__.py 2020-04-03 11:22:05 +01:00
ArcandGitHub ccb9b462cc Create clightning.py 2020-04-03 11:21:10 +01:00
ArcandGitHub 127f1ee258 Update lndgrpc.py 2020-04-02 15:03:32 +01:00
ArcandGitHub 1db380d88e Update lndgrpc.py 2020-04-02 15:01:38 +01:00
ArcandGitHub 9ead85987f Update __init__.py 2020-04-02 13:07:01 +01:00
ArcandGitHub 4562d23f6e Delete lnd.py 2020-04-02 13:06:15 +01:00
ArcandGitHub f6f2304c5a Update requirements.txt 2020-04-02 12:50:13 +01:00
ArcandGitHub d194ba5fe1 Update lndgrpc.py 2020-04-02 12:43:48 +01:00
ArcandGitHub c2a116b713 Update lndgrpc.py 2020-04-02 12:43:23 +01:00
ArcandGitHub c6f47fc589 Update .env.example 2020-04-02 12:42:36 +01:00
ArcandGitHub 4533363c49 Update .env.example
Added lnd grpc vars, uses the Zap example
2020-04-02 12:12:03 +01:00
ArcandGitHub efc847d381 Update lndgrpc.py 2020-04-02 12:03:10 +01:00
ArcandGitHub b02bee01e5 Update lndgrpc.py 2020-04-02 07:45:00 +01:00
ArcandGitHub f364ce51c0 Update __init__.py 2020-04-02 07:44:35 +01:00
ArcandGitHub 73ec8deee1 Create lndgrpc.py
Doesn't work, but hopefully will soon
2020-04-02 07:44:03 +01:00
Eneko Illarramendi 649cc888ab refactor: /wallet tweaks 2020-04-01 22:18:46 +02:00
Eneko Illarramendi d03785558b refactor: a wallet is a wallet is a wallet 2020-03-31 19:05:25 +02:00
Eneko Illarramendi 75d97ddfc1 refactor: camera ready 2020-03-10 23:12:22 +01:00
Eneko Illarramendi 49addb8d56 refactor: breaking bad 2020-03-08 23:00:41 +01:00
Eneko Illarramendi d862b16ee6 refactor: "payments" is the name, and API updates 2020-03-07 22:27:00 +01:00
ArcandGitHub 6e26e06aea Added python-dotenv so flask would pick up .env 2020-03-05 23:27:13 +00:00
Eneko Illarramendi 7996c48b70 chore: remove wait option for LntxbotWallet 2020-03-05 23:01:37 +01:00
Eneko Illarramendi 9382381144 chore: initial version of tx chart 2020-03-05 23:01:37 +01:00
Eneko Illarramendi f98a5040ac refactor: decorators, models and more broken bits 2020-03-04 23:11:15 +01:00
ArcandGitHub ee70161854 Update requirements.txt 2020-03-03 19:42:18 +00:00
ArcandGitHub 80810ae0b4 Update README.md 2020-02-27 13:48:58 +00:00
ArcandGitHub a06c40e1e0 Update index.html 2020-02-27 13:24:00 +00:00
ArcandGitHub 882d24f395 Update index.html 2020-02-27 13:21:06 +00:00
ArcandGitHub 7ceab798c8 Update index.html 2020-02-27 13:11:33 +00:00
ArcandGitHub 02fdf6fb50 Update registration.html 2020-02-27 12:45:55 +00:00
ArcandGitHub e2cd162409 added gevent dependency 2020-02-27 10:16:54 +00:00
ArcandGitHub bb6d21abe6 Be gone target='_blank' , back to 2003 2020-02-26 19:40:57 +00:00
ArcandGitHub f69b54d6e8 Removed target='_blank'as this isnt 2002 2020-02-26 19:38:49 +00:00
ArcandGitHub d763107c5a Update index.html 2020-02-26 19:37:19 +00:00
ArcandGitHub e08aec9fc8 Update tpos.html 2020-02-26 19:21:47 +00:00
ArcandGitHub 89dc5bc3f5 Update tpos.html 2020-02-26 19:13:32 +00:00
ArcandGitHub 539bce0d54 Update views.py 2020-02-26 19:12:12 +00:00
ArcandGitHub 7a91414cf1 Rename example.config.json to config.json 2020-02-26 17:51:57 +00:00
ArcandGitHub 0b510e1e76 Rename example.config.json to config.json 2020-02-26 17:51:47 +00:00
ArcandGitHub ce9975fc18 Rename config.json to example.config.json 2020-02-26 10:22:35 +00:00
ArcandGitHub 67a29abfa2 Rename config.json to example.config.json 2020-02-26 09:24:52 +00:00
ArcandGitHub 233ed4ff4d Update views_api.py 2020-02-26 01:46:12 +00:00
ArcandGitHub 9bbfe367f5 Update index.html 2020-02-26 00:47:11 +00:00
ArcandGitHub cbfabf6b4a Rename example.config.json to config.json 2020-02-26 00:45:49 +00:00
ArcandGitHub ab157c557c Update schema.sql 2020-02-26 00:41:26 +00:00
ArcandGitHub d22ef892a4 Update views.py 2020-02-26 00:40:42 +00:00
ArcandGitHub 4bfa9a6a05 Update views_api.py 2020-02-26 00:40:23 +00:00
ArcandGitHub ac459b70b6 Update index.html 2020-02-26 00:39:45 +00:00
ArcandGitHub c5741b8414 Rename config.json to example.config.json 2020-02-25 23:33:25 +00:00
ArcandGitHub fc00eddaf3 Add files via upload 2020-02-25 23:13:03 +00:00
ArcandGitHub 62b590c8e5 Got rid of ads 2020-02-25 23:12:06 +00:00
ArcandGitHub cf456d7912 Update views_api.py 2020-02-25 23:08:36 +00:00
ArcandGitHub 233f8a8dec Update views.py 2020-02-25 23:07:59 +00:00
ArcandGitHub bff879406c Update display.html 2020-02-25 23:06:53 +00:00
ArcandGitHub 7a2d825406 Update and rename example.config.json to config.json 2020-02-25 19:51:36 +00:00
ArcandGitHub a6f0504cea Update views_api.py 2020-02-25 19:50:59 +00:00
ArcandGitHub 3a4d279479 Update tpos.html 2020-02-25 19:50:27 +00:00
ArcandGitHub f02066e077 Add files via upload 2020-02-25 00:45:21 +00:00
ArcandGitHub a9f15d093c Create README.md 2020-02-25 00:44:01 +00:00
ArcandGitHub 36ba680a5d Update views_api.py 2020-02-24 19:12:49 +00:00
ArcandGitHub 63ac385c59 Update index.html 2020-02-24 18:14:01 +00:00
ArcandGitHub af10c8274a Update index.html 2020-02-24 18:13:08 +00:00
ArcandGitHub 6f06bcfb36 Update views_api.py 2020-02-24 16:05:24 +00:00
ArcandGitHub 3c8a1f0667 Update views_api.py 2020-02-24 14:53:30 +00:00
ArcandGitHub b61f8f52e3 Update wallet.html 2020-02-23 01:13:13 +00:00
ArcandGitHub 6ae1327865 Update index.html 2020-02-23 01:05:32 +00:00
ArcandGitHub adf12888f4 Update index.html 2020-02-23 00:56:16 +00:00
ArcandGitHub d72ec7ba75 Update index.html 2020-02-23 00:46:59 +00:00
ArcandGitHub 0baea37447 Update views.py 2020-02-23 00:46:38 +00:00
ArcandGitHub 222f04f893 Update views.py 2020-02-23 00:46:08 +00:00
ArcandGitHub ab6211ab97 Update index.html 2020-02-23 00:45:25 +00:00
ArcandGitHub 1e2c47fdcd Update views.py 2020-02-23 00:44:56 +00:00
ArcandGitHub 6f26032744 Update index.html 2020-02-23 00:44:10 +00:00
ArcandGitHub 7b11167fef Update wallet.html 2020-02-23 00:43:26 +00:00
ArcandGitHub 15de047bb8 Update __init__.py 2020-02-23 00:42:22 +00:00
ArcandGitHub b35872c751 Update config.json 2020-02-23 00:28:16 +00:00
ArcandGitHub d7361abad1 Update config.json 2020-02-23 00:27:44 +00:00
ArcandGitHub 65ef030768 Rename example.config.json to config.json 2020-02-23 00:20:42 +00:00
ArcandGitHub 820bb8deea Update wallet.html 2020-02-23 00:18:00 +00:00
ArcandGitHub adfa707554 Update __init__.py 2020-02-23 00:16:13 +00:00
ArcandGitHub 0a815f87ff Update and rename register.html to registration.html 2020-02-23 00:14:16 +00:00
ArcandGitHub a002a597d0 Update index.html 2020-02-23 00:13:37 +00:00
ArcandGitHub c941aec662 Update display.html 2020-02-23 00:13:16 +00:00
ArcandGitHub d2d8c76baa Update views_api.py 2020-02-23 00:12:45 +00:00
ArcandGitHub 838ae02fc1 Update views.py 2020-02-23 00:12:00 +00:00
ArcandGitHub c814378f11 Update schema.sql 2020-02-23 00:11:37 +00:00
ArcandGitHub 74b760bb28 Update README.md 2020-02-23 00:11:12 +00:00
ArcandGitHub 4605203386 Update base.html 2020-02-20 15:23:07 +00:00
ArcandGitHub 8fca682ab1 Add files via upload 2020-02-20 15:22:09 +00:00
ArcandGitHub 2ed6e9cfff Delete npm.js 2020-02-20 15:21:41 +00:00
ArcandGitHub aead4bde20 Delete datepicker.min.js 2020-02-20 15:21:29 +00:00
ArcandGitHub 30729b334c Delete bootstrap.min.js 2020-02-20 15:21:19 +00:00
ArcandGitHub 0e81e1d998 Delete bootstrap.js 2020-02-20 15:21:08 +00:00
ArcandGitHub 9caa1e586c Add files via upload 2020-02-20 15:20:36 +00:00
ArcandGitHub 80ce215c9e Delete datepicker.min.css 2020-02-20 15:20:00 +00:00
ArcandGitHub b0f31c289e Delete bootstrap.min.css 2020-02-20 15:19:51 +00:00
ArcandGitHub 568319f1aa Delete bootstrap.css.map 2020-02-20 15:19:41 +00:00
ArcandGitHub 4f345c5d6d Delete bootstrap.css 2020-02-20 15:19:28 +00:00
ArcandGitHub 64448f826c Update index.html 2020-02-20 15:15:07 +00:00
ArcandGitHub f24b1a5d19 Merge pull request #12 from arcbtc/lnbitsevents
Lnbitsevents
2020-02-20 11:08:58 +00:00
ArcandGitHub 3c8dd56afc Merge branch 'master' into lnbitsevents 2020-02-20 11:08:46 +00:00
ArcandGitHub ea0dac0e5e Update schema.sql 2020-02-20 10:59:43 +00:00
ArcandGitHub 1fdbdfc1de Update views.py 2020-02-20 10:57:52 +00:00
ArcandGitHub 38c8c7b265 Update index.html 2020-02-20 10:57:04 +00:00
ArcandGitHub 9e7e53b634 Update base.html 2020-02-20 08:50:01 +00:00
ArcandGitHub a9aac916c7 Add files via upload 2020-02-20 08:48:58 +00:00
ArcandGitHub 2b43f9bab4 Add files via upload 2020-02-20 08:48:24 +00:00
ArcandGitHub 5103f0d2b9 Update views.py 2020-02-20 08:46:27 +00:00
ArcandGitHub ebf6c7cba5 Update schema.sql 2020-02-20 08:45:46 +00:00
ArcandGitHub 51e0d2b928 Update index.html 2020-02-20 08:45:09 +00:00
ArcandGitHub 154cbb0d48 Update index.html 2020-02-20 00:00:23 +00:00
ArcandGitHub 4254c45d20 Update views.py 2020-02-19 23:51:26 +00:00
ArcandGitHub 06bd993b09 Update __init__.py 2020-02-19 23:50:03 +00:00
ArcandGitHub 7acd549b05 Update example.config.json 2020-02-19 23:49:17 +00:00
ArcandGitHub c410e7d79e Update views_api.py 2020-02-19 23:42:04 +00:00
ArcandGitHub e9f21bb2cf Update index.html 2020-02-19 23:41:00 +00:00
ArcandGitHub 4044448fd0 Rename config.json to example.config.json 2020-02-19 23:22:46 +00:00
ArcandGitHub b6ec5edcab Update schema.sql 2020-02-19 23:21:44 +00:00
ArcandGitHub 3fb570c9fa Create views_api.py 2020-02-19 23:21:01 +00:00
ArcandGitHub 87cc8a484f Update views.py 2020-02-19 23:16:06 +00:00
ArcandGitHub 02261b1ba4 Update views.py 2020-02-19 23:15:50 +00:00
ArcandGitHub 53d375efd5 Create views.py 2020-02-19 23:14:53 +00:00
ArcandGitHub e64c8bcf53 Create index.html 2020-02-19 23:12:33 +00:00
ArcandGitHub b4cad9e6cd Create schema.sql 2020-02-19 23:02:57 +00:00
ArcandGitHub 44c6dc0c01 Create __init__.py 2020-02-19 22:58:58 +00:00
ArcandGitHub c111902e7f Create example.config.json 2020-02-19 22:57:04 +00:00
ArcandGitHub 7e88c504f6 Create README.md 2020-02-19 22:54:46 +00:00
ArcandGitHub 436bcfbd11 Rename example.config.json to config.json 2020-02-19 22:52:59 +00:00
ArcandGitHub 47bb06feb7 Rename config.json to example.config.json 2020-02-19 22:52:28 +00:00
ArcandGitHub 9c249bed70 Update README.md 2020-02-19 22:49:13 +00:00
ArcandGitHub 4ba9fbcb98 Update README.md 2020-02-19 22:48:28 +00:00
ArcandGitHub a4aaec28de Update README.md 2020-02-19 22:47:29 +00:00
ArcandGitHub 4cee231df4 Create README.md 2020-02-19 22:47:07 +00:00
ArcandGitHub 87d66e99d0 Update README.md 2020-02-19 22:20:58 +00:00
ArcandGitHub 3362b0cdbe Update README.md 2020-02-19 22:20:41 +00:00
ArcandGitHub 1b11f91517 Update README.md 2020-02-19 22:20:10 +00:00
ArcandGitHub 17f059a8de Added endpoint for easy LNURL fetching 2020-02-19 22:17:19 +00:00
Eneko Illarramendi 30cf2e9f99 feat: automatically import extension blueprints 2020-02-19 20:52:34 +01:00
ArcandGitHub 7212bad783 Update index.html 2020-02-19 14:26:04 +00:00
ArcandGitHub 1328c0ad3e Update views.py 2020-02-19 12:40:53 +00:00
ArcandGitHub 3f9fdf2da6 Add files via upload 2020-02-19 12:35:04 +00:00
ArcandGitHub 610c93622e Update README.md 2020-02-19 11:41:28 +00:00
ArcandGitHub ded5ec147c Update README.md 2020-02-19 11:41:12 +00:00
Eneko Illarramendi 6e921e5102 chore: update requirements 2020-02-18 21:01:28 +01:00
Eneko Illarramendi e5915bc493 refactor: add a simple config file to extensions
It makes possible to automagically keep the extensions admin page updated.
2020-02-18 20:27:04 +01:00
ArcandGitHub 792b787f1a Update lnd.py 2020-02-17 16:58:44 +00:00
ArcandGitHub 3b530d4d5e Update .env.example 2020-02-15 21:16:48 +00:00
ArcandGitHub c3b792ebeb Update index.html 2020-02-12 15:12:46 +00:00
ArcandGitHub dab238e182 Update views.py 2020-02-12 13:42:25 +00:00
ArcandGitHub 331905b8a9 Update views_api.py 2020-02-12 13:42:09 +00:00
ArcandGitHub 645261f274 Update views_api.py 2020-02-12 13:41:30 +00:00
ArcandGitHub e92b49487a Merge pull request #11 from arcbtc/extensions
refactor: remove databases from code an use schemas instead
2020-02-10 19:08:55 +00:00
Eneko Illarramendi 0d33bc2933 refactor: remove databases from code an use schemas instead
All databases are now saved together in the same LNBITS_DATA_FOLDER.
Extensions have to define a schema.yml file for creating the necessary database.
2020-02-10 19:40:01 +01:00
ArcandGitHub 1b9badc11a added verify false to lnd requests 2020-02-10 13:18:10 +00:00
ArcandGitHub e3fd6b4ff1 Update views_api.py 2020-02-05 13:04:43 +00:00
ArcandGitHub e8dd29ec5c Update __init__.py 2020-02-05 13:01:37 +00:00
ArcandGitHub 9f0c72acc9 Update views_api.py 2020-02-05 12:57:45 +00:00
ArcandGitHub 37cf44cf23 Update __init__.py 2020-02-05 12:31:48 +00:00
ArcandGitHub a8f91b00a5 Update views_api.py 2020-02-05 12:27:56 +00:00
ArcandGitHub 21bd049164 Crude hack to try and make the faucets work 2020-02-05 12:14:18 +00:00
ArcandGitHub 79b2616642 Update helpers.py 2020-02-05 11:47:17 +00:00
ArcandGitHub a245621ab8 Rollback, see if it fixes json decoding 2020-02-05 11:36:52 +00:00
ArcandGitHub 5a74f52712 Update views_api.py 2020-02-04 22:53:00 +00:00
ArcandGitHub 5a1727df58 Update views_api.py 2020-02-04 22:51:50 +00:00
ArcandGitHub e62eb89b96 Update print.html 2020-02-04 20:45:02 +00:00
ArcandGitHub 4d35041b2d Update print.html 2020-02-04 20:44:26 +00:00
ArcandGitHub ea25c684ae Add files via upload 2020-02-04 20:44:04 +00:00
ArcandGitHub a697752db3 Delete note (1).jpg 2020-02-04 20:43:37 +00:00
ArcandGitHub 0c65d997a5 Update print.html 2020-02-04 20:41:09 +00:00
ArcandGitHub faa3a3f197 Add files via upload 2020-02-04 20:40:04 +00:00
ArcandGitHub 66be419b3e Update print.html 2020-02-04 20:37:33 +00:00
ArcandGitHub 853962c322 Broken print link 2020-02-04 20:32:12 +00:00
ArcandGitHub 68d501f3ce fixed image liinks 2020-02-04 20:29:41 +00:00
ArcandGitHub 10a4c07a6e Update index.html 2020-02-04 20:23:07 +00:00
ArcandGitHub 8abdaaea48 Merge pull request #10 from arcbtc/extensions
Extensions
2020-02-04 14:15:09 +00:00
Eneko Illarramendi 9e90aabead refactor: use Flask Blueprints to organize extensions
- extensions are now blueprints: keep views, templastes and statics in the same folder
- increase app security using `flask-talisman`
- whenever possible use {{ url_for }} for links between pages
- remove references to non-existing JavaScript code
- add missing favicon.ico
2020-01-31 21:07:05 +01:00
Eneko Illarramendi 4beb7be852 refactor: rename faucet extension 2020-01-31 20:25:14 +01:00
Eneko Illarramendi f10585abe1 chore: update requirements 2020-01-31 20:20:45 +01:00
ArcandGitHub 044901b598 Update __init__.py 2020-01-29 20:28:24 +00:00
ArcandGitHub 78e7e638c8 Update settings.py 2020-01-29 19:43:15 +00:00
ArcandGitHub f247dbff50 Update displaywithdraw.html 2020-01-29 19:23:27 +00:00
ArcandGitHub 95bdd27379 Update index.html 2020-01-29 19:23:07 +00:00
ArcandGitHub 2982b60be6 Update extensions.html 2020-01-29 19:22:43 +00:00
ArcandGitHub cdc5797de4 Update wallet.html 2020-01-29 19:21:47 +00:00
ArcandGitHub 9c71a0d8f1 Update app.js 2020-01-29 19:21:12 +00:00
ArcandGitHub 351f4d3b4d Update app.js 2020-01-29 19:19:27 +00:00
ArcandGitHub b16312c9a2 Update __init__.py 2020-01-29 19:18:35 +00:00
ArcandGitHub 47d02a6853 Update base.html 2020-01-29 19:18:11 +00:00
ArcandGitHub b840e01c5c Fixed chrome issues 2020-01-29 19:17:01 +00:00
ArcandGitHub b42c98fadd Update __init__.py 2020-01-29 15:14:03 +00:00
ArcandGitHub b60af27038 Update __init__.py 2020-01-29 14:40:06 +00:00
ArcandGitHub 60739a7587 For validation 2020-01-29 12:34:42 +00:00
ArcandGitHub fc9b30bbee Form validation to withdrawmaker 2020-01-29 12:24:33 +00:00
ArcandGitHub e7f92f9fa9 ad images 2020-01-29 00:23:04 +00:00
ArcandGitHub 29c4eb3d1d Added space in display wallet 2020-01-29 00:21:03 +00:00
ArcandGitHub 42804b3337 Update index.html 2020-01-29 00:19:22 +00:00
ArcandGitHub cc977303dc Update app.js 2020-01-28 23:34:33 +00:00
ArcandGitHub bfdbb78a9c Update withdraw.html 2020-01-28 23:28:30 +00:00
ArcandGitHub 784d9740a9 Update displaywithdraw.html 2020-01-28 23:26:05 +00:00
ArcandGitHub 0a4c09a68b Update base.html 2020-01-28 22:27:45 +00:00
ArcandGitHub 1cdb64f21f Update withdraw.html 2020-01-28 22:26:38 +00:00
ArcandGitHub be8c837033 Update extensions.html 2020-01-28 22:26:13 +00:00
ArcandGitHub 4075d690ec Update wallet.html 2020-01-28 22:25:46 +00:00
ArcandGitHub 9e502f9750 Update app.js 2020-01-28 22:24:49 +00:00
ArcandGitHub b7af605c5b Update db.py 2020-01-27 23:41:16 +00:00
ArcandGitHub a5e64bd831 Update __init__.py 2020-01-27 23:37:25 +00:00
ArcandGitHub 3c498b3eae Update db.py 2020-01-27 23:31:40 +00:00
ArcandGitHub 700e982669 Classes for extensions
Added for both extensions and lnfaucet db
2020-01-27 23:30:31 +00:00
ArcandGitHub 045c2f23f3 Updated menu 2020-01-27 23:11:27 +00:00
ArcandGitHub 64b5fb20d4 Update app.js 2020-01-27 23:06:37 +00:00
ArcandGitHub 02eeafc6c9 Added calls for lnfaucet
All works, although I need to do a bit more on checking faucet time etc, but the basic lnurl withdraw stuff works.
Can we move this to the lnfaucet extension folder please
2020-01-27 23:05:21 +00:00
ArcandGitHub 9613870ac3 Needed to printing faucets
Ideally, add to lnurl faucet folder
2020-01-27 23:02:00 +00:00
ArcandGitHub aef01e2566 Added templates for lnfaucet extension
Ideally, these need to be moved to the lnfaucet template
2020-01-27 23:00:24 +00:00
ArcandGitHub 33d6f3080a Aded DB to manage user selection of extensions 2020-01-27 22:58:50 +00:00
ArcandGitHub 73886745f2 Add files via upload 2020-01-27 22:57:40 +00:00
ArcandGitHub 257a23d212 Update settings.py 2020-01-22 07:55:10 +00:00
ArcandGitHub 496f94249c Update __init__.py 2020-01-21 19:47:22 +00:00
ArcandGitHub 664614d1a4 Create README.md 2020-01-21 17:58:32 +00:00
ArcandGitHub a69a06957b Create README.md 2020-01-21 17:58:02 +00:00
ArcandGitHub ed749aee99 Create README.md 2020-01-21 17:56:44 +00:00
ArcandGitHub 17fa1c624c Update app.js 2020-01-21 14:10:44 +00:00
ArcandGitHub bd7b6c8616 Update settings.py 2020-01-21 12:28:46 +00:00
ArcandGitHub 87e76389b6 QR read check / error report 2020-01-21 11:06:22 +00:00
ArcandGitHub c119b35ecb Added lnpay support 2020-01-18 10:27:54 +00:00
ArcandGitHub bdecb5aee1 Added lnpay 2020-01-18 10:26:20 +00:00
ArcandGitHub ca1a774949 Update __init__.py 2020-01-18 10:24:49 +00:00
ArcandGitHub 108342791d added lnpay as a source 2020-01-18 10:24:22 +00:00
ArcandGitHub e6e3f2f675 Update app.js 2020-01-16 20:55:48 +00:00
ArcandGitHub 2b48f08884 Update app.js 2020-01-16 20:51:36 +00:00
ArcandGitHub c3a78df4ab Added support for processing modal 2020-01-16 20:49:48 +00:00
Eneko Illarramendi 4c739dbb90 fix: don't call r.json() until we know we have a response 2020-01-16 14:26:51 +01:00
Eneko Illarramendi 0d517b884a refactor: code review 2020-01-16 13:58:27 +01:00
ArcandGitHub b8b42d1475 Added read and invoice macs to lnd 2020-01-16 12:42:03 +00:00
ArcandGitHub ea292b5035 Update settings.py 2020-01-16 12:40:11 +00:00
ArcandGitHub 15f527fa39 Update __init__.py 2020-01-16 12:39:46 +00:00
ArcandGitHub cab3279f17 Update base.py 2020-01-16 12:38:40 +00:00
ArcandGitHub 480a89196b Update lnd.py 2020-01-16 12:38:11 +00:00
ArcandGitHub 4c8b9c322b Update lntxbot.py 2020-01-16 12:37:49 +00:00
ArcandGitHub 364ab472d2 Update opennode.py 2020-01-16 12:37:33 +00:00
ArcandGitHub 883e826aa4 Update __init__.py 2020-01-15 15:20:38 +00:00
ArcandGitHub 8f20755fd6 Update settings.py 2020-01-15 15:19:54 +00:00
ArcandGitHub 028f86151d Update wallet.html 2020-01-15 15:18:59 +00:00
ArcandGitHub bcb27519bf Update app.js 2020-01-15 15:18:14 +00:00
ArcandGitHub 65d9a146c5 Update __init__.py 2020-01-15 15:17:22 +00:00
ArcandGitHub f765163486 Updated requests 2020-01-15 15:16:10 +00:00
ArcandGitHub f598fe29be ON init 2020-01-12 00:48:45 +00:00
ArcandGitHub 5f2cee4303 ON settings 2020-01-12 00:47:33 +00:00
ArcandGitHub c304ab4f93 ON credentials 2020-01-12 00:46:09 +00:00
ArcandGitHub ac5df27c02 Removed buggy line drawing around QR codes 2020-01-12 00:44:02 +00:00
ArcandGitHub 5cce3e9043 Added intervals
for funding sources without subscriptions
2020-01-12 00:41:47 +00:00
ArcandGitHub d3bc33df51 Opennode support
Needs setintervals in app.js to work
2020-01-12 00:26:40 +00:00
Eneko Illarramendi 2bb30aa880 fix: LND invoice payments and status checks 2020-01-11 23:51:35 +01:00
Eneko Illarramendi 6d019bcb2b fix: get correct LND payment_hash 2020-01-11 23:31:07 +01:00
Eneko Illarramendi 47b93a97d6 refactor: unify responses in backend wallets 2020-01-10 21:26:42 +01:00
Eneko Illarramendi 676fa29852 chore: update requirements 2020-01-10 13:43:52 +01:00
ArcandGitHub 4345b45ed5 Update README.md 2019-12-31 08:32:31 +00:00
fiatjaf b89753abd5 400->200 on invoice check timeout. 2019-12-30 10:59:11 -03:00
fiatjaf cedce73f07 order transaction chart by date. 2019-12-29 07:56:10 -03:00
ArcandGitHub 1e3d3ddd70 Wrong extend call 2019-12-19 14:14:47 +00:00
fiatjaf 7440602acd black 2019-12-18 13:52:49 +00:00
fiatjaf 5f6585eb45 fee reserve defaults to 0. 2019-12-18 13:44:58 +00:00
fiatjaf 098b681747 lnurl template using index.html as base. 2019-12-18 13:44:26 +00:00
ArcandGitHub a5fb10d288 Update README.md 2019-12-18 13:02:58 +00:00
ArcandGitHub 72e6a0b129 Update README.md 2019-12-18 13:00:27 +00:00
ArcandGitHub 89c62c3cc5 Update README.md 2019-12-18 12:59:59 +00:00
ArcandGitHub 3336e91546 Update README.md 2019-12-18 12:55:20 +00:00
ArcandGitHub 1200fec70e Update wallet.html
Added reminder to bookmark
2019-12-18 11:15:50 +00:00
ArcandGitHub 08abdd23f6 Update base.html 2019-12-18 11:09:56 +00:00
ArcandGitHub f382bc8f83 Update index.html
fixed link to github
2019-12-18 11:08:22 +00:00
ArcandGitHub 3793529de8 Update README.md 2019-12-18 11:02:48 +00:00
ArcandGitHub 9322be442d Update README.md 2019-12-18 11:02:00 +00:00
ArcandGitHub 071a23b31e Merge pull request #3 from arcbtc/add-license-1
Create LICENSE
2019-12-17 21:53:45 +00:00
ArcandGitHub 35ea661d52 Create LICENSE 2019-12-17 21:52:52 +00:00
Eneko Illarramendi 9a21cee902 refactor: change folder case 2019-12-17 17:04:51 +01:00
ArcandGitHub d1219aa85b Update __init__.py
Added temp screen for claiming lnurl
2019-12-17 15:31:35 +00:00
ArcandGitHub 25458cf168 Create lnurl.html 2019-12-17 15:30:10 +00:00
ArcandGitHub 9620d305e3 Update index.html
MAde homepage a little more mobile friendly
2019-12-17 09:30:24 +00:00
ArcandGitHub f87da0fb44 Update app.js
Added copy function
2019-12-16 21:48:27 +00:00
ArcandGitHub 8d06f2f402 Update app.js
Wrapped invoice output into box
2019-12-16 21:41:10 +00:00
fiatjaf 21598f862d internal invoices resolved internally. 2019-12-16 18:24:06 +00:00
fiatjaf ce94fc8984 fix schema so payments can be sent between lnbits wallets. 2019-12-16 16:23:06 +00:00
fiatjaf 377243b9cb bring readme back. 2019-12-16 15:07:21 +00:00
fiatjaf 391ec689ff fix wallet creation from homepage. 2019-12-15 20:33:25 +00:00
fiatjaf 29a187d36b reverse order in transactions list. 2019-12-15 20:33:25 +00:00
fiatjaf 7616b4ec89 check pending payments and invoices on startup. 2019-12-15 20:33:25 +00:00
fiatjaf ad1c561862 fix /wallet for usr&nme case. 2019-12-15 18:15:13 +00:00
fiatjaf 76403793c0 check errors and adapt sql for older sqlite versions. 2019-12-15 18:15:00 +00:00
Eneko Illarramendi 0ddf6f76c9 chore: use lnurl.LnurlWithdrawResponse instead of a magic .handle() 2019-12-15 09:39:59 +01:00
Eneko Illarramendi da6b67a1ad fix: redirect to homepage when no more user_wallets are found 2019-12-15 09:18:06 +01:00
fiatjaf 1812b4a803 fix hexlify bytes -> str. 2019-12-14 17:35:35 -03:00
fiatjaf 463ee21c9f take JS out of HTML, fix transactions chart, add apipayments.time. 2019-12-14 17:26:26 -03:00
fiatjaf 14dfa9ecc6 big bundle of changes.
* reorganize templates even more (and a small layout break)
* rename wallets.hash and accounts.userhash to id
* refactor /wallets so it's idempotent for each param combination
* many small changes
2019-12-14 15:49:41 -03:00
fiatjaf 1cee6dad42 reorganize templates with jinja2. 2019-12-14 11:48:53 -03:00
fiatjaf 8d9ab6cc77 fix balances view. 2019-12-14 09:18:48 -03:00
fiatjaf b4426ba021 hashes as hex. 2019-12-14 09:18:42 -03:00
Eneko Illarramendi 80c43701b3 refactor: clean lnurlwallet view 2019-12-14 12:24:21 +01:00
Eneko Illarramendi 21a6958458 fix: minor issues 2019-12-14 11:08:54 +01:00
Eneko Illarramendi 5849ffc678 refactor: abstract wallets 2019-12-14 10:13:27 +01:00
fiatjaf 23ffa40f30 balances view and other minor edits. 2019-12-14 00:11:41 -03:00
fiatjaf 582aaa812b payment logic safer and adapt db calls to new schema. 2019-12-13 22:59:35 -03:00
fiatjaf 754782b598 super naïve bolt11 decoder. 2019-12-13 21:57:22 -03:00
fiatjaf 7097d01f9e move schema to sqlfile, create on init. 2019-12-13 20:34:19 -03:00
fiatjaf 978854fc72 move variables to environment. 2019-12-13 19:41:39 -03:00
Eneko Illarramendi e5dc309eb1 refactor: clean database calls in api_checkinvoice view 2019-12-13 23:22:49 +01:00
Eneko Illarramendi 3f7c7e7807 chore: clean imports 2019-12-13 23:02:56 +01:00
Eneko Illarramendi d875bb84bd refactor: clean database calls in api_transactions view
And minor fixes.
2019-12-13 23:02:56 +01:00
Eneko Illarramendi badc60f44e chore: this looks better 2019-12-13 23:02:56 +01:00
Eneko Illarramendi dfd00cfe05 refactor: clean database calls in /v1/invoices view
And return fails first to simplify if..else and make the
code more readable.
2019-12-13 23:02:56 +01:00
Eneko Illarramendi 52f31da4f8 refactor: clean database calls in wallet view 2019-12-13 18:41:22 +01:00
Eneko Illarramendi 69a979eb8f refactor: ad a Database class and clean deletewallet view 2019-12-13 18:14:25 +01:00
Eneko Illarramendi 7c6c7a2bf6 refactor: move encrypt() function to helpers 2019-12-13 17:59:40 +01:00
Eneko Illarramendi 3d3632a06f refactor: add some dev files and black original code
Original server code has been moved to __init__ file, so we can
work on `lnbits` as a package.
2019-12-13 17:56:19 +01:00
ArcandGitHub 37d96345c9 Update README.md 2019-12-13 14:51:40 +00:00
ArcandGitHub 741dc86cea Update README.md 2019-12-13 14:41:58 +00:00
ArcandGitHub 136117065a Update README.md 2019-12-13 14:35:07 +00:00
ArcandGitHub 773e294b85 Update README.md 2019-12-13 14:34:21 +00:00
ArcandGitHub 7e99a91fad Update README.md 2019-12-13 14:33:52 +00:00
ArcandGitHub 93c0f0fe41 Update README.md 2019-12-13 14:25:17 +00:00
ArcandGitHub 6b9ee3a3ec Update README.md 2019-12-13 14:21:32 +00:00
ArcandGitHub f3c38ac723 Update README.md 2019-12-13 14:18:42 +00:00
ArcandGitHub ab26878173 Update README.md 2019-12-13 14:15:28 +00:00
ArcandGitHub 65806ef1cd Update README.md 2019-12-13 14:09:42 +00:00
ArcandGitHub 815df37f80 Update README.md 2019-12-13 14:08:53 +00:00
ArcandGitHub 11323cc710 Add files via upload 2019-12-12 23:15:49 +00:00
ArcandGitHub 9617c25d37 Add files via upload 2019-12-12 23:13:55 +00:00
ArcandGitHub 340ea68696 Create README 2019-12-12 23:12:53 +00:00
ArcandGitHub 2e4b266648 Add files via upload 2019-12-12 23:11:30 +00:00
ArcandGitHub b7e5bff034 Add files via upload 2019-12-12 23:08:26 +00:00
ArcandGitHub 4921ffc776 Create LICENSE 2019-12-12 23:06:51 +00:00
ArcandGitHub 750dd7dfbf Add files via upload 2019-12-12 23:04:35 +00:00
ArcandGitHub 5f3030401d Create requirements.txt 2019-12-12 23:03:50 +00:00
ArcandGitHub 8d4f9b0591 Delete lnbits.zip 2019-12-12 23:01:46 +00:00
ArcandGitHub e9ce0dd6f2 Add files via upload 2019-12-09 21:39:41 +00:00
ArcandGitHub eb37b15814 Update README.md 2019-12-09 21:39:02 +00:00
ArcandGitHub 87ae25fdc7 Initial commit 2019-12-09 21:38:29 +00:00
267 changed files with 72942 additions and 5880 deletions
+16
View File
@@ -0,0 +1,16 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 2
indent_style = space
[*.md]
trim_trailing_whitespace = false
[*.py]
indent_size = 4
indent_style = space
+56
View File
@@ -0,0 +1,56 @@
QUART_APP=lnbits.app:create_app()
QUART_ENV=development
QUART_DEBUG=true
HOST=127.0.0.1
PORT=5000
LNBITS_SITE_TITLE=LNbits
LNBITS_ALLOWED_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="."
LNBITS_DISABLED_EXTENSIONS="amilk"
LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
# just so you can see the UI before dealing with this file.
# Set one of these blocks depending on the wallet kind you chose above:
# SparkWallet
SPARK_URL=http://localhost:9737/rpc
SPARK_TOKEN=myaccesstoken
# CLightningWallet
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
# LnbitsWallet
LNBITS_ENDPOINT=http://127.0.0.1:5000
LNBITS_KEY=LNBITS_ADMIN_KEY
# LndWallet
LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon"
# LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_MACAROON="HEXSTRING"
# LNPayWallet
LNPAY_API_ENDPOINT=https://lnpay.co/v1/
LNPAY_API_KEY=LNPAY_API_KEY
LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
# LntxbotWallet
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/
LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
# OpenNodeWallet
OPENNODE_API_ENDPOINT=https://api.opennode.com/
OPENNODE_KEY=OPENNODE_ADMIN_KEY
+1
View File
@@ -0,0 +1 @@
custom: https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK
+28
View File
@@ -0,0 +1,28 @@
name: codeql
on:
push:
branches: [master, ]
pull_request:
branches: [master]
schedule:
- cron: '0 12 * * 5'
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 2
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript, python
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+23
View File
@@ -0,0 +1,23 @@
name: formatting
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: sudo apt-get install python3-venv
- run: python3 -m venv venv
- run: ./venv/bin/pip install black
- run: make checkblack
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install
- run: make checkprettier
+12
View File
@@ -0,0 +1,12 @@
name: mypy
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master
with:
path: lnbits
+36
View File
@@ -0,0 +1,36 @@
name: tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test with pytest
env:
LNBITS_BACKEND_WALLET_CLASS: LNPayWallet
LNBITS_FORCE_HTTPS: 0
LNPAY_API_ENDPOINT: https://lnpay.co/v1/
LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
run: |
pip install pytest pytest-cov
pytest --cov=lnbits --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
+33
View File
@@ -0,0 +1,33 @@
.DS_Store
._*
__pycache__
*.py[cod]
*$py.class
.mypy_cache
.vscode
*.egg
*.egg-info
.coverage
.pytest_cache
.webassets-cache
htmlcov
test-reports
*.swo
*.swp
*.pyo
*.pyc
*.env
.env
.venv
venv
*.sqlite3
.pyre*
__bundle__
node_modules
lnbits/static/bundle.*
+12
View File
@@ -0,0 +1,12 @@
{
"semi": false,
"arrowParens": "avoid",
"insertPragma": false,
"printWidth": 80,
"proseWrap": "preserve",
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"jsxBracketSameLine": false,
"bracketSpacing": false
}
+8
View File
@@ -0,0 +1,8 @@
FROM python:3.7-slim
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -q -r requirements.txt
COPY . /app
EXPOSE 5000
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Arc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+23
View File
@@ -0,0 +1,23 @@
all: format check requirements.txt
format: prettier black
check: mypy checkprettier checkblack
prettier: $(shell find lnbits -name "*.js" -name ".html")
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
black: $(shell find lnbits -name "*.py")
./venv/bin/black lnbits
mypy: $(shell find lnbits -name "*.py")
./venv/bin/mypy lnbits
checkprettier: $(shell find lnbits -name "*.js" -name ".html")
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js
checkblack: $(shell find lnbits -name "*.py")
./venv/bin/black --check lnbits
requirements.txt: Pipfile.lock
cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt
+32
View File
@@ -0,0 +1,32 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[requires]
python_version = "3.7"
[packages]
bitstring = "*"
cerberus = "*"
ecdsa = "*"
environs = "*"
lnurl = "*"
pyscss = "*"
shortuuid = "*"
quart = "*"
quart-cors = "*"
quart-compress = "*"
secure = "*"
typing-extensions = "*"
httpx = "*"
quart-trio = "*"
trio = "*"
hypercorn = {extras = ["trio"], version = "*"}
[dev-packages]
black = "==20.8b1"
pytest = "*"
pytest-cov = "*"
mypy = "==0.761"
pytest-trio = "*"
Generated
+739
View File
@@ -0,0 +1,739 @@
{
"_meta": {
"hash": {
"sha256": "193ef930ac0906127fd292b2fc7ba5b4d786227b6795d182dad9b57448fca75e"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb",
"sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af"
],
"version": "==0.5.0"
},
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"markers": "python_version >= '3.5'",
"version": "==1.10"
},
"attrs": {
"hashes": [
"sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
"sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.2.0"
},
"bech32": {
"hashes": [
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
},
"bitstring": {
"hashes": [
"sha256:fdf3eb72b229d2864fb507f8f42b1b2c57af7ce5fec035972f9566de440a864a"
],
"index": "pypi",
"version": "==3.1.7"
},
"blinker": {
"hashes": [
"sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"
],
"version": "==1.4"
},
"brotli": {
"hashes": [
"sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8",
"sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c",
"sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70",
"sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f",
"sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429",
"sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126",
"sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4",
"sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438",
"sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f",
"sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6",
"sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26",
"sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14",
"sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430",
"sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296",
"sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12",
"sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452",
"sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea",
"sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a",
"sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5",
"sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d",
"sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa",
"sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb",
"sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b",
"sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4",
"sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7",
"sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1",
"sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"
],
"version": "==1.0.9"
},
"cerberus": {
"hashes": [
"sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"
],
"index": "pypi",
"version": "==1.3.2"
},
"certifi": {
"hashes": [
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.6.20"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"ecdsa": {
"hashes": [
"sha256:494c6a853e9ed2e9be33d160b41d47afc50a6629b993d2b9c5ad7bb226add892",
"sha256:ca359c971594dceebf334f3d623dae43163ab161c7d09f28cae70a86df26eb7a"
],
"index": "pypi",
"version": "==0.16.0"
},
"environs": {
"hashes": [
"sha256:a98005aab7613b6fe7a1af7192a5163f72a52d3348d3918e6c7a2a32e4012779",
"sha256:bf3fd6bc54fcfd7f512ddcb80a7781f0ced2b0c83dd123d619e9468ecdaaf537"
],
"index": "pypi",
"version": "==8.0.0"
},
"h11": {
"hashes": [
"sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
"sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
],
"version": "==0.11.0"
},
"h2": {
"hashes": [
"sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25",
"sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==4.0.0"
},
"hpack": {
"hashes": [
"sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c",
"sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==4.0.0"
},
"httpcore": {
"hashes": [
"sha256:18c4afcbfe884b635e59739105aed1692e132bc5d31597109f3c1c97e4ec1cac",
"sha256:2526a38f31ac5967d38b7f593b5d8c4bd3fa82c21400402f866ba3312946acbf"
],
"markers": "python_version >= '3.6'",
"version": "==0.12.0"
},
"httpx": {
"hashes": [
"sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537",
"sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"
],
"index": "pypi",
"version": "==0.16.1"
},
"hypercorn": {
"extras": [
"trio"
],
"hashes": [
"sha256:81c69dd84a87b8e8b3ebf06ef5dd92836a8238f0ac65ded3d86befb8ba9acfeb",
"sha256:e3f317d6d64d15ce589f49e4f5057947259fa35332d169e62cb060e9997189e4"
],
"index": "pypi",
"version": "==0.11.1"
},
"hyperframe": {
"hashes": [
"sha256:742d2a4bc3152a340a49d59f32e33ec420aa8e7054c1444ef5c7efff255842f1",
"sha256:a51026b1591cac726fc3d0b7994fbc7dc5efab861ef38503face2930fd7b2d34"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==6.0.0"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.2"
},
"lnurl": {
"hashes": [
"sha256:aaff8552cd4c02f8ae30c82dd3ebe6c6c3a8e06cbc59991281534ba0bbe12890"
],
"index": "pypi",
"version": "==0.3.5"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
},
"marshmallow": {
"hashes": [
"sha256:2272273505f1644580fbc66c6b220cc78f893eb31f1ecde2af98ad28011e9811",
"sha256:47911dd7c641a27160f0df5fd0fe94667160ffe97f70a42c3cc18388d86098cc"
],
"markers": "python_version >= '3.5'",
"version": "==3.8.0"
},
"outcome": {
"hashes": [
"sha256:ee46c5ce42780cde85d55a61819d0e6b8cb490f1dbd749ba75ff2629771dcd2d",
"sha256:fc7822068ba7dd0fc2532743611e8a73246708d3564e29a39f93d6ab3701b66f"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.0.1"
},
"priority": {
"hashes": [
"sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe",
"sha256:be4fcb94b5e37cdeb40af5533afe6dd603bd665fe9c8b3052610fc1001d5d1eb"
],
"version": "==1.3.0"
},
"pydantic": {
"hashes": [
"sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c",
"sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e",
"sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e",
"sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1",
"sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d",
"sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9",
"sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614",
"sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99",
"sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633",
"sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73",
"sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7",
"sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b",
"sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d",
"sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df",
"sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20",
"sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1",
"sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"
],
"markers": "python_version >= '3.6'",
"version": "==1.6.1"
},
"pyscss": {
"hashes": [
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
],
"index": "pypi",
"version": "==1.3.7"
},
"python-dotenv": {
"hashes": [
"sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
"sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
],
"version": "==0.14.0"
},
"quart": {
"hashes": [
"sha256:9c634e4c1e4b21b824003c676de1583581258c72b0ac4d2ba747db846e97ff56",
"sha256:d885d782edd9d5dcfd2c4a56e020db3b82493d4c3950f91c221b7d88d239ac93"
],
"index": "pypi",
"version": "==0.13.1"
},
"quart-compress": {
"hashes": [
"sha256:41cd0cc8d26905a45025ddda7022461a71b9d1d950b21b006dc106a1c41c75ef",
"sha256:63af5e6370aa7850fb219d22e1db89965aeb13b8f27bc83e7f9a44118faa3c54"
],
"index": "pypi",
"version": "==0.2.1"
},
"quart-cors": {
"hashes": [
"sha256:020a17d504264db86cada3c1335ef174af28b33f57cee321ddc46d69c33d5c8e",
"sha256:c08bdb326219b6c186d19ed6a97a7fd02de8fe36c7856af889494c69b525c53c"
],
"index": "pypi",
"version": "==0.3.0"
},
"quart-trio": {
"hashes": [
"sha256:00f3b20f8d82ce7e81ead61db4efba38ed7653c7e28199defded46b663ab2595",
"sha256:dafc8f0440d4b70fa60d24122a161d2373894d2bfa9f713d9f1df1fd508f0834"
],
"index": "pypi",
"version": "==0.5.1"
},
"rfc3986": {
"extras": [
"idna2008"
],
"hashes": [
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
],
"version": "==1.4.0"
},
"secure": {
"hashes": [
"sha256:4dc8dd4b548831c3ad7f94079332c41d67c781eccc32215ff5a8a49582c1a447",
"sha256:b3bf1e39ebf40040fc3248392343a5052aa14cb45fc87ec91b0bd11f19cc46bd"
],
"index": "pypi",
"version": "==0.2.1"
},
"shortuuid": {
"hashes": [
"sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f",
"sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"
],
"index": "pypi",
"version": "==1.0.1"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"sniffio": {
"hashes": [
"sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5",
"sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"
],
"markers": "python_version >= '3.5'",
"version": "==1.1.0"
},
"sortedcontainers": {
"hashes": [
"sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba",
"sha256:c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f"
],
"version": "==2.2.2"
},
"toml": {
"hashes": [
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.1"
},
"trio": {
"hashes": [
"sha256:e85cf9858e445465dfbb0e3fdf36efe92082d2df87bfe9d62585eedd6e8e9d7d",
"sha256:fc70c74e8736d1105b3c05cc2e49b30c58755733740f9c51ae6d88a4d6d0a291"
],
"index": "pypi",
"version": "==0.17.0"
},
"typing-extensions": {
"hashes": [
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
],
"index": "pypi",
"version": "==3.7.4.3"
},
"werkzeug": {
"hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.0.1"
},
"wsproto": {
"hashes": [
"sha256:614798c30e5dc2b3f65acc03d2d50842b97621487350ce79a80a711229edfa9d",
"sha256:e3d190a11d9307112ba23bbe60055604949b172143969c8f641318476a9b6f1d"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==0.15.0"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
],
"version": "==1.4.4"
},
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"markers": "python_version >= '3.5'",
"version": "==1.10"
},
"attrs": {
"hashes": [
"sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
"sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.2.0"
},
"black": {
"hashes": [
"sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
],
"index": "pypi",
"version": "==20.8b1"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"coverage": {
"hashes": [
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==5.3"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"iniconfig": {
"hashes": [
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
"sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"
],
"version": "==1.0.1"
},
"mypy": {
"hashes": [
"sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a",
"sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7",
"sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2",
"sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474",
"sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0",
"sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217",
"sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749",
"sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6",
"sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf",
"sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36",
"sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b",
"sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72",
"sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1",
"sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"
],
"index": "pypi",
"version": "==0.761"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"outcome": {
"hashes": [
"sha256:ee46c5ce42780cde85d55a61819d0e6b8cb490f1dbd749ba75ff2629771dcd2d",
"sha256:fc7822068ba7dd0fc2532743611e8a73246708d3564e29a39f93d6ab3701b66f"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.0.1"
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.4"
},
"pathspec": {
"hashes": [
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
"sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
],
"version": "==0.8.0"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"py": {
"hashes": [
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.9.0"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
"hashes": [
"sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9",
"sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"
],
"index": "pypi",
"version": "==6.1.1"
},
"pytest-cov": {
"hashes": [
"sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191",
"sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"
],
"index": "pypi",
"version": "==2.10.1"
},
"pytest-trio": {
"hashes": [
"sha256:3f48cc1df66d279d705af38ad38d1639c2e2380ddffcdc3a45bb81758de61f03",
"sha256:9bf0a490fd177a33617e8709242293fae47934de2b51f8209eb2c0545b6ca8fe"
],
"index": "pypi",
"version": "==0.6.0"
},
"regex": {
"hashes": [
"sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef",
"sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c",
"sha256:3d20024a70b97b4f9546696cbf2fd30bae5f42229fbddf8661261b1eaff0deb7",
"sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b",
"sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c",
"sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63",
"sha256:49f23ebd5ac073765ecbcf046edc10d63dcab2f4ae2bce160982cb30df0c0302",
"sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc",
"sha256:5d892a4f1c999834eaa3c32bc9e8b976c5825116cde553928c4c8e7e48ebda67",
"sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be",
"sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab",
"sha256:816064fc915796ea1f26966163f6845de5af78923dfcecf6551e095f00983650",
"sha256:84cada8effefe9a9f53f9b0d2ba9b7b6f5edf8d2155f9fdbe34616e06ececf81",
"sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19",
"sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637",
"sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc",
"sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b",
"sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d",
"sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b",
"sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100",
"sha256:c9443124c67b1515e4fe0bb0aa18df640965e1030f468a2a5dc2589b26d130ad",
"sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3",
"sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121",
"sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b",
"sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707",
"sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7",
"sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"
],
"version": "==2020.9.27"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"sniffio": {
"hashes": [
"sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5",
"sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"
],
"markers": "python_version >= '3.5'",
"version": "==1.1.0"
},
"sortedcontainers": {
"hashes": [
"sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba",
"sha256:c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f"
],
"version": "==2.2.2"
},
"toml": {
"hashes": [
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.1"
},
"trio": {
"hashes": [
"sha256:e85cf9858e445465dfbb0e3fdf36efe92082d2df87bfe9d62585eedd6e8e9d7d",
"sha256:fc70c74e8736d1105b3c05cc2e49b30c58755733740f9c51ae6d88a4d6d0a291"
],
"index": "pypi",
"version": "==0.17.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:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
],
"index": "pypi",
"version": "==3.7.4.3"
}
}
}
+1
View File
@@ -0,0 +1 @@
web: hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
+79 -6
View File
@@ -1,11 +1,84 @@
# Cashu
LNbits
======
## Create ecash mint for pegging in/out of ecash
[![github-tests-badge]][github-tests]
[![github-mypy-badge]][github-mypy]
[![codecov-badge]][codecov]
[![license-badge]](LICENSE)
[![docs-badge]][docs]
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png)
### Usage
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
1. Enable extension
2. Create a Mint
3. Share wallet
Use [lnbits.com](https://lnbits.com), or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
The wallet can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily.
See [lnbits.org](https://lnbits.org) for more detailed documentation.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits.
## Running LNbits
See the [install guide](docs/guide/installation.md) for details on installation and setup.
### Contributing to LNbits
There's a [slightly different setup](docs/devs/installation.md) if you want to contribute to LNbits, but if your changes don't require adding or removing any package dependencies you don't have to bother with that, just follow the [normal installation](docs/guide/installation.md) steps.
## 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..
![Lightning network wallet](https://i.imgur.com/w8jdGpF.png)
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)
![lnurl ATM](https://i.imgur.com/WfCg8wY.png)
## LNbits as an LNURL-withdraw fallback
LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the [LNURL-withdraw](https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-withdraw.md).
![lnurl fallback](https://i.imgur.com/CPBKHIv.png)
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds.
![lnurl ATM](https://i.imgur.com/Gi6bn3L.jpg)
## LNbits as an insta-wallet
Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon).
"Go to this website", has a lot less friction than "Download this app".
![lnurl ATM](https://i.imgur.com/xFWDnwy.png)
## Tip me
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
[docs]: https://lnbits.org/
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
[github-tests]: https://github.com/lnbits/lnbits/actions?query=workflow%3Atests
[github-tests-badge]: https://github.com/lnbits/lnbits/workflows/tests/badge.svg
[codecov]: https://codecov.io/gh/lnbits/lnbits
[codecov-badge]: https://codecov.io/gh/lnbits/lnbits/branch/master/graph/badge.svg
[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
-47
View File
@@ -1,47 +0,0 @@
import asyncio
from environs import Env
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_cashu")
cashu_static_files = [
{
"path": "/cashu/static",
"app": StaticFiles(directory="lnbits/extensions/cashu/static"),
"name": "cashu_static",
}
]
from cashu.mint.ledger import Ledger
env = Env()
env.read_env()
ledger = Ledger(
db=db,
seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"),
derivation_path="0/0/0/1",
)
cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"])
def cashu_renderer():
return template_renderer(["lnbits/extensions/cashu/templates"])
from .tasks import startup_cashu_mint, wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
def cashu_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(startup_cashu_mint))
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+7
View File
@@ -0,0 +1,7 @@
{
"scripts": {
"dokku": {
"predeploy": "quart migrate && quart assets"
}
}
}
-7
View File
@@ -1,7 +0,0 @@
{
"name": "Cashu",
"short_description": "Ecash mint and wallet",
"tile": "/cashu/static/image/cashu.png",
"contributors": ["calle", "vlad", "arcbtc"],
"hidden": false
}
-51
View File
@@ -1,51 +0,0 @@
from typing import List, Optional, Union
from . import db
from .models import Cashu
async def create_cashu(
cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
) -> Cashu:
await db.execute(
"""
INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, keyset_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
cashu_id,
wallet_id,
data.name,
data.tickershort,
data.fraction,
data.maxsats,
data.coins,
keyset_id,
),
)
cashu = await get_cashu(cashu_id)
assert cashu, "Newly created cashu couldn't be retrieved"
return cashu
async def get_cashu(cashu_id) -> Optional[Cashu]:
row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
return Cashu(**row) if row else None
async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM cashu.cashu WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Cashu(**row) for row in rows]
async def delete_cashu(cashu_id) -> None:
await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,))
+5
View File
@@ -0,0 +1,5 @@
_site
.sass-cache
.jekyll-cache
.jekyll-metadata
vendor
+1
View File
@@ -0,0 +1 @@
lnbits.org
+11
View File
@@ -0,0 +1,11 @@
title: "LNbits docs"
remote_theme: pmarsceill/just-the-docs
logo: "/logos/lnbits-full.png"
search_enabled: true
url: https://lnbits.org
aux_links:
"LNbits on GitHub":
- "//github.com/lnbits/lnbits"
"lnbits.com":
- "//lnbits.com"
+12
View File
@@ -0,0 +1,12 @@
---
layout: default
parent: For developers
title: API reference
nav_order: 3
---
API reference
=============
Coming soon...
+12
View File
@@ -0,0 +1,12 @@
---
layout: default
title: For developers
nav_order: 4
has_children: true
---
For developers
==============
Thanks for contributing :)
+43
View File
@@ -0,0 +1,43 @@
---
layout: default
parent: For developers
title: Making extensions
nav_order: 2
---
Making extensions
=================
Start off by copying the example extension in `lnbits/extensions/example` into your own:
```sh
cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use dashes or anything; it doesn't like those.
cd lnbits/extensions/mysuperplugin
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
```
Going over the example extension's structure:
* views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.
* views.py: The `/` path will show up as your plugin's home page in lnbits' UI. Other pages you can define yourself. The `templates` folder should explain itself in relation to this.
* migrations.py: Create database tables for your plugin. They'll be created automatically when you start lnbits.
... This document is a work-in-progress. Send pull requests if you get stuck, so others don't.
Adding new dependencies
-----------------------
If for some reason your extensions needs a new python package to work, you can add a new package using Pipenv:
```sh
$ pipenv install new_package_name
```
This will create a new entry in the `Pipenv` file.
**But we need an extra step to make sure LNbits doesn't break in production.**
All tests and deployments should run against the `requirements.txt` file so every time a new package is added
it is necessary to run the Pipenv `lock` command and manually update the requirements file:
```sh
$ pipenv lock -r
```
+60
View File
@@ -0,0 +1,60 @@
---
layout: default
parent: For developers
title: Installation
nav_order: 1
---
Installation
============
Download the latest stable release https://github.com/lnbits/lnbits/releases
Application dependencies
------------------------
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
```sh
$ pipenv shell
$ pipenv install --dev
```
If you wish to use a version of Python higher than 3.7:
```sh
$ pipenv --python 3.8 install --dev
```
You will need to copy `.env.example` to `.env`, then set variables there.
![Files](https://i.imgur.com/ri2zOe8.png)
You might also need to install additional packages, depending on the [backend wallet](../guide/wallets.md) you use.
E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `pipenv run pip install purerpc`.
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
Running the server
------------------
LNbits uses [Quart][quart] as an application server.
```sh
$ pipenv run python -m lnbits
```
Frontend
--------
The frontend uses [Vue.js and Quasar][quasar].
[quart]: https://pgjones.gitlab.io/
[pipenv]: https://pipenv.pypa.io/
[polar]: https://lightningpolar.com/
[quasar]: https://quasar.dev/start/how-to-use-vue
+30
View File
@@ -0,0 +1,30 @@
---
layout: default
title: Basic installation
nav_order: 2
---
Basic installation
==================
Download this repo and install the dependencies:
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits/
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
cp .env.example .env
./venv/bin/quart assets
./venv/bin/quart migrate
./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
```
No you can visit your LNbits at http://localhost:5000/.
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
Then you can run restart it and it will be using the new settings.
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
+78
View File
@@ -0,0 +1,78 @@
---
layout: default
title: Backend wallets
nav_order: 3
---
Backend wallets
===============
LNbits can run on top of many lightning-network funding sources. Currently there is support for
CLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularily.
A backend wallet can be configured using the following LNbits environment variables:
### CLightning
Using this wallet requires the installation of the `pylightning` Python package.
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
### Spark (c-lightning)
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
- `SPARK_URL`: http://10.147.17.230:9737/rpc
- `SPARK_TOKEN`: secret_access_key
### LND (gRPC)
Using this wallet requires the installation of the `lndgrpc` and `purerpc` Python packages.
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
- `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon
### LND (REST)
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: ip_address
- `LND_REST_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon
### LNbits
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
- `LNBITS_ENDPOINT`: e.g. https://lnbits.com
- `LNBITS_KEY`: lnbitsAdminKey
### LNPay
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret.
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: https://lnpay.co/v1/
- `LNPAY_API_KEY`: sak_apiKey
- `LNPAY_WALLET_KEY`: waka_apiKey
### lntxbot
- `LNBITS_BACKEND_WALLET_CLASS`: **LntxbotWallet**
- `LNTXBOT_API_ENDPOINT`: https://lntxbot.bigsun.xyz/
- `LNTXBOT_KEY`: lntxbotAdminApiKey
### OpenNode
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary.
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
- `OPENNODE_KEY`: opennodeAdminApiKey
+24
View File
@@ -0,0 +1,24 @@
---
layout: default
title: Users Guide
nav_order: 1
---
LNbits, free and open-source lightning-network wallet/accounts system
=====================================================================
LNbits is a very simple Python application that sits on top of any funding source, and can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
LNbits as an account system
---------------------------
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending,
export to csv + more to come...
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

+1
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+1
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+1
View File
@@ -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

-28
View File
@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCqdYhs1zok/3Es
uSycCn0YKGXaXO9Vbhb1Aol1vzYc0UkQseDrGCiEF/0D5Vol6n3aqsZabKfQC46e
ylEs+HWUYStsDApkSsyrlT+38nNYFtbCGNzf3lFqJxaOKrelCmhiYc3vzJc4yHpp
OaWtuWDlx3bWnJSMi8VF6UkqAtZrlLTrx45Wd/X2lPh5V3hHbwQy+BJ0kGOJuNWO
iMg4PUtaCVa8QQQSSdbXzLeMnkDXJRgGr6tEaFbZWCmQBOYIrmYcXTa4xSg+SaLa
fBqGGY5CFa2mnGarhQ9I6/Or7KES4CgyIewuJSC4VZQ9WNFTeWRJmwF59LQIB1B+
Su61Ds5vAgMBAAECggEAUTUJN4EZzEJ9smbNn9FHY1jrnSolQzfdbAA3aIcdyoDS
JEf59UZfFv8upMqBNgsOxjwRn0agc/btjSypm5kKuxPH0gOyd7FC7IOABaxV0yOe
UH3zzBtoAgB5OGWVc+UPcb9SkumPKZgo6TRzf94PEakDLp8ODh8QbowDNSaA7GL2
FXvSgcxtLFv24/wMj7MPtUJDPPzhzRafe097RAJKi9ig33GpiohVg7HWxBEt/m2q
WPJqV1izIYVuyUV1NN2qV/7KP35vqGu3sMyuKjmz33+7G5OUzxoixFp1hx1iQwNM
QmzrNjKgGKPOvI169K73LtT6GfcAcVyvudPioJi2oQKBgQDlvEg79bXuV72OMkDI
LINTk0Pncs5fJMhsPrbhvAbi8C9qH3j/e3uh4/f/RZVy8PZFe2YsmJo6cE60t445
oejGpj0L9gB+ptWQa+6tJ0vO2DFrGz7siBDQDK08bBfE0FRAYYH1oy81oZihAwh/
ypUWgxAAkFIDG1b8IYozUwlgaQKBgQC98mWG1ypNFDKU5ffFmxzCermXhe4sIOny
E9Na51lMuzp1HVOHqYio27KGWB9NhFnhCRdzq2csJrdL5GqNBzF3mVc7Wp1QA9Iz
GAN1BWYsObtffmePI73UOtdOf3vWRWt9xFDw07CuPUJoZLxCO8DE2R8TfBEZpMVx
L99xZ4ldFwKBgCCCZk5l1Nrne6F1pwksk2DuQ/uqmV2JXPtEXKjQ7MCMcIk57uto
U/RyyBNNi3p6cr8eWTUSWzDN17VnDC9HpCMIPzdEFonVnf8T5vCF0QpyOXwInPzn
vElUoecc/LgTKvj5U2SpNPMH7QtL3JYI2tQWs9sAkTpzUfnX3TCqHXeRAoGAGeEJ
phpwn/nNfm3qmRiuDW0tFhDgR1DgruO2mx8EEqk7qLMrK0OU4/Sy2QzujuAmYrub
X14N+3z24jcnsTk2qyXQNbz9y+YJYH64Edx/dC0Vc0hEkcL73Qt7RC2oUKQ6pfL2
WClvW/3qq1B2OcvsUqWGT2BK8CR75hTwq4HAvFcCgYB0Cuv/cHo0iLzvCX2LwIiq
N38udO9oYa2VV/Ny4lf/oWFyLuL2syNJnW6fhqZk3ACVbIxQ4t85IJ+kKSKgSBXV
9p3kAo93/sP+Qd3yWK/2kc8fwQXPQWhEynmS0c2LmfmA6h+JlPUG+HHhXwEMcplc
iuvTCsY3KzPtBZgm8hFIKg==
-----END PRIVATE KEY-----
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

-24
View File
@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
background: #350a60;
margin: 0;
width: 380px;
height: 595px;
}
iframe {
width: 100%;
height: 100%;
border: 0;
}
</style>
</head>
<body>
<iframe
id="iframe"
src="https://cashu.calle.wtf:5001/cashu/wallet?mint_id=W358Nzvmb7iuQpt6mXArKx"
></iframe>
</body>
</html>
-19
View File
@@ -1,19 +0,0 @@
{
"description": "Cashu ecash wallet",
"icons": {"128": "128.png", "32": "32.png", "16": "16.png"},
"manifest_version": 3,
"name": "Cashu",
"version": "0.1",
"action": {
"default_icon": "32.png",
"default_popup": "embedder.html",
"default_title": "Cashu"
},
"permissions": [
"unlimitedStorage",
"storage"
],
"content_security_policy": {
"extension_pages": "default-src 'self'"
}
}
View File
+22
View File
@@ -0,0 +1,22 @@
from .app import create_app
from .commands import migrate_databases, transpile_scss, bundle_vendored
from .settings import LNBITS_SITE_TITLE, SERVICE_FEE, DEBUG, LNBITS_DATA_FOLDER, WALLET, LNBITS_COMMIT
migrate_databases()
transpile_scss()
bundle_vendored()
app = create_app()
print(
f"""Starting LNbits with
- git version: {LNBITS_COMMIT}
- site title: {LNBITS_SITE_TITLE}
- debug: {DEBUG}
- data folder: {LNBITS_DATA_FOLDER}
- funding source: {WALLET.__class__.__name__}
- service fee: {SERVICE_FEE}
"""
)
app.run(host=app.config["HOST"], port=app.config["PORT"])
+135
View File
@@ -0,0 +1,135 @@
import importlib
import warnings
from quart import g
from quart_trio import QuartTrio
from quart_cors import cors # type: ignore
from quart_compress import Compress # type: ignore
from secure import SecureHeaders # type: ignore
from .commands import db_migrate, handle_assets
from .core import core_app
from .db import open_db, open_ext_db
from .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored
from .proxy_fix import ASGIProxyFix
from .tasks import run_deferred_async, invoice_listener, internal_invoice_listener, webhook_handler, grab_app_for_later
from .settings import WALLET
secure_headers = SecureHeaders(hsts=False)
def create_app(config_object="lnbits.settings") -> QuartTrio:
"""Create application factory.
:param config_object: The configuration object to use.
"""
app = QuartTrio(__name__, static_folder="static")
app.config.from_object(config_object)
app.asgi_http_class = ASGIProxyFix
cors(app)
Compress(app)
check_funding_source(app)
register_assets(app)
register_blueprints(app)
register_filters(app)
register_commands(app)
register_request_hooks(app)
register_async_tasks(app)
grab_app_for_later(app)
return app
def check_funding_source(app: QuartTrio) -> None:
@app.before_serving
async def check_wallet_status():
error_message, balance = WALLET.status()
if error_message:
warnings.warn(
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
RuntimeWarning,
)
else:
print(f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat.")
def register_blueprints(app: QuartTrio) -> None:
"""Register Flask blueprints / LNbits extensions."""
app.register_blueprint(core_app)
for ext in get_valid_extensions():
try:
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
bp = getattr(ext_module, f"{ext.code}_ext")
@bp.before_request
async def before_request():
g.ext_db = open_ext_db(ext.code)
@bp.teardown_request
async def after_request(exc):
g.ext_db.close()
app.register_blueprint(bp, url_prefix=f"/{ext.code}")
except Exception:
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.")
def register_commands(app: QuartTrio):
"""Register Click commands."""
app.cli.add_command(db_migrate)
app.cli.add_command(handle_assets)
def register_assets(app: QuartTrio):
"""Serve each vendored asset separately or a bundle."""
@app.before_request
async def vendored_assets_variable():
if app.config["DEBUG"]:
g.VENDORED_JS = map(url_for_vendored, get_js_vendored())
g.VENDORED_CSS = map(url_for_vendored, get_css_vendored())
else:
g.VENDORED_JS = ["/static/bundle.js"]
g.VENDORED_CSS = ["/static/bundle.css"]
def register_filters(app: QuartTrio):
"""Jinja filters."""
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
def register_request_hooks(app: QuartTrio):
"""Open the core db for each request so everything happens in a big transaction"""
@app.before_request
async def before_request():
g.db = open_db()
g.nursery = app.nursery
@app.after_request
async def set_secure_headers(response):
secure_headers.quart(response)
return response
@app.teardown_request
async def after_request(exc):
g.db.close()
def register_async_tasks(app):
@app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
async def webhook_listener():
return await webhook_handler()
@app.before_serving
async def listeners():
run_deferred_async(app.nursery)
app.nursery.start_soon(invoice_listener)
app.nursery.start_soon(internal_invoice_listener)
@app.after_serving
async def stop_listeners():
pass
+172
View File
@@ -0,0 +1,172 @@
import bitstring # type: ignore
import re
import hashlib
from typing import List, NamedTuple, Optional
from bech32 import bech32_decode, CHARSET # type: ignore
from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string # type: ignore
from binascii import unhexlify
class Route(NamedTuple):
pubkey: str
short_channel_id: str
base_fee_msat: int
ppm_fee: int
cltv: int
class Invoice(object):
payment_hash: str
amount_msat: int = 0
description: Optional[str] = None
description_hash: Optional[str] = None
payee: Optional[str] = None
date: int
expiry: int = 3600
secret: Optional[str] = None
route_hints: List[Route] = []
min_final_cltv_expiry: int = 18
def decode(pr: str) -> Invoice:
"""bolt11 decoder,
based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py
"""
hrp, decoded_data = bech32_decode(pr)
if hrp is None or decoded_data is None:
raise ValueError("Bad bech32 checksum")
if not hrp.startswith("ln"):
raise ValueError("Does not start with ln")
bitarray = _u5_to_bitarray(decoded_data)
# final signature 65 bytes, split it off.
if len(bitarray) < 65 * 8:
raise ValueError("Too short to contain signature")
# extract the signature
signature = bitarray[-65 * 8 :].tobytes()
# the tagged fields as a bitstream
data = bitstring.ConstBitStream(bitarray[: -65 * 8])
# build the invoice object
invoice = Invoice()
# decode the amount from the hrp
m = re.search("[^\d]+", hrp[2:])
if m:
amountstr = hrp[2 + m.end() :]
if amountstr != "":
invoice.amount_msat = _unshorten_amount(amountstr)
# pull out date
invoice.date = data.read(35).uint
while data.pos != data.len:
tag, tagdata, data = _pull_tagged(data)
data_length = len(tagdata) / 5
if tag == "d":
invoice.description = _trim_to_bytes(tagdata).decode("utf-8")
elif tag == "h" and data_length == 52:
invoice.description_hash = _trim_to_bytes(tagdata).hex()
elif tag == "p" and data_length == 52:
invoice.payment_hash = _trim_to_bytes(tagdata).hex()
elif tag == "x":
invoice.expiry = tagdata.uint
elif tag == "n":
invoice.payee = _trim_to_bytes(tagdata).hex()
# this won't work in most cases, we must extract the payee
# from the signature
elif tag == "s":
invoice.secret = _trim_to_bytes(tagdata).hex()
elif tag == "r":
s = bitstring.ConstBitStream(tagdata)
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
route = Route(
pubkey=s.read(264).tobytes().hex(),
short_channel_id=_readable_scid(s.read(64).intbe),
base_fee_msat=s.read(32).intbe,
ppm_fee=s.read(32).intbe,
cltv=s.read(16).intbe,
)
invoice.route_hints.append(route)
# BOLT #11:
# A reader MUST check that the `signature` is valid (see the `n` tagged
# field specified below).
# A reader MUST use the `n` field to validate the signature instead of
# performing signature recovery if a valid `n` field is provided.
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
sig = signature[0:64]
if invoice.payee:
key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
else:
keys = VerifyingKey.from_public_key_recovery(sig, message, SECP256k1, hashlib.sha256)
signaling_byte = signature[64]
key = keys[int(signaling_byte)]
invoice.payee = key.to_string("compressed").hex()
return invoice
def _unshorten_amount(amount: str) -> int:
"""Given a shortened amount, return millisatoshis"""
# BOLT #11:
# The following `multiplier` letters are defined:
#
# * `m` (milli): multiply by 0.001
# * `u` (micro): multiply by 0.000001
# * `n` (nano): multiply by 0.000000001
# * `p` (pico): multiply by 0.000000000001
units = {
"p": 10 ** 12,
"n": 10 ** 9,
"u": 10 ** 6,
"m": 10 ** 3,
}
unit = str(amount)[-1]
# BOLT #11:
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
# anything except a `multiplier` in the table above.
if not re.fullmatch("\d+[pnum]?", str(amount)):
raise ValueError("Invalid amount '{}'".format(amount))
if unit in units:
return int(int(amount[:-1]) * 100_000_000_000 / units[unit])
else:
return int(amount) * 100_000_000_000
def _pull_tagged(stream):
tag = stream.read(5).uint
length = stream.read(5).uint * 32 + stream.read(5).uint
return (CHARSET[tag], stream.read(length * 5), stream)
def _trim_to_bytes(barr):
# Adds a byte if necessary.
b = barr.tobytes()
if barr.len % 8 != 0:
return b[:-1]
return b
def _readable_scid(short_channel_id: int) -> str:
return "{blockheight}x{transactionindex}x{outputindex}".format(
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
outputindex=(short_channel_id & 0xFFFF),
)
def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
ret = bitstring.BitArray()
for a in arr:
ret += bitstring.pack("uint:5", a)
return ret
+83
View File
@@ -0,0 +1,83 @@
import warnings
import click
import importlib
import re
import os
import sqlite3
from .core import migrations as core_migrations
from .db import open_db, open_ext_db
from .helpers import get_valid_extensions, get_css_vendored, get_js_vendored, url_for_vendored
from .settings import LNBITS_PATH
@click.command("migrate")
def db_migrate():
migrate_databases()
@click.command("assets")
def handle_assets():
transpile_scss()
bundle_vendored()
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss:
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
css.write(compile_string(scss.read()))
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")),
(get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
with open_db() as core_db:
try:
rows = core_db.fetchall("SELECT * FROM dbversions")
except sqlite3.OperationalError:
# migration 3 wasn't ran
core_migrations.m000_create_migrations_table(core_db)
rows = core_db.fetchall("SELECT * FROM dbversions")
current_versions = {row["db"]: row["version"] for row in rows}
matcher = re.compile(r"^m(\d\d\d)_")
def run_migration(db, migrations_module):
db_name = migrations_module.__name__.split(".")[-2]
for key, run_migration in migrations_module.__dict__.items():
match = match = matcher.match(key)
if match:
version = int(match.group(1))
if version > current_versions.get(db_name, 0):
print(f"running migration {db_name}.{version}")
run_migration(db)
core_db.execute(
"INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", (db_name, version)
)
run_migration(core_db, core_migrations)
for ext in get_valid_extensions():
try:
ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations")
with open_ext_db(ext.code) as db:
run_migration(db, ext_migrations)
except ImportError:
raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.")
+15
View File
@@ -0,0 +1,15 @@
from quart import Blueprint
core_app: Blueprint = Blueprint(
"core", __name__, template_folder="templates", static_folder="static", static_url_path="/core/static"
)
from .views.api import * # noqa
from .views.generic import * # noqa
from .tasks import register_listeners
from lnbits.tasks import record_async
core_app.record(record_async(register_listeners))
+309
View File
@@ -0,0 +1,309 @@
import json
import datetime
from uuid import uuid4
from typing import List, Optional, Dict
from quart import g
from lnbits import bolt11
from lnbits.settings import DEFAULT_WALLET_NAME
from .models import User, Wallet, Payment
# accounts
# --------
def create_account() -> User:
user_id = uuid4().hex
g.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]:
row = g.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]:
user = g.db.fetchone("SELECT id, email FROM accounts WHERE id = ?", (user_id,))
if user:
extensions = g.db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,))
wallets = g.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:
g.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:
wallet_id = uuid4().hex
g.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:
g.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]:
row = g.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]:
row = g.db.fetchone(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE adminkey = ? OR inkey = ?
""",
(key, key),
)
if not row:
return None
if key_type == "admin" and row["adminkey"] != key:
return None
return Wallet(**row)
# wallet payments
# ---------------
def get_standalone_payment(checking_id: str) -> Optional[Payment]:
row = g.db.fetchone(
"""
SELECT *
FROM apipayments
WHERE checking_id = ?
""",
(checking_id,),
)
return Payment.from_row(row) if row else None
def get_wallet_payment(wallet_id: str, payment_hash: str) -> Optional[Payment]:
row = g.db.fetchone(
"""
SELECT *
FROM apipayments
WHERE wallet = ? AND hash = ?
""",
(wallet_id, payment_hash),
)
return Payment.from_row(row) if row else None
def get_wallet_payments(
wallet_id: str,
*,
complete: bool = False,
pending: bool = False,
outgoing: bool = False,
incoming: bool = False,
exclude_uncheckable: bool = False,
) -> List[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
clause = ""
if complete and pending:
clause += ""
elif complete:
clause += "AND ((amount > 0 AND pending = 0) OR amount < 0)"
elif pending:
clause += "AND pending = 1"
else:
raise TypeError("at least one of [complete, pending] must be True.")
clause += " "
if outgoing and incoming:
clause += ""
elif outgoing:
clause += "AND amount < 0"
elif incoming:
clause += "AND amount > 0"
else:
raise TypeError("at least one of [outgoing, incoming] must be True.")
clause += " "
if exclude_uncheckable: # checkable means it has a checking_id that isn't internal
clause += "AND checking_id NOT LIKE 'temp_%' "
clause += "AND checking_id NOT LIKE 'internal_%' "
rows = g.db.fetchall(
f"""
SELECT *
FROM apipayments
WHERE wallet = ? {clause}
ORDER BY time DESC
""",
(wallet_id,),
)
return [Payment.from_row(row) for row in rows]
def delete_expired_invoices() -> None:
rows = g.db.fetchall(
"""
SELECT bolt11
FROM apipayments
WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 86400
"""
)
for (payment_request,) in rows:
try:
invoice = bolt11.decode(payment_request)
except:
continue
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration_date > datetime.datetime.utcnow():
continue
g.db.execute(
"""
DELETE FROM apipayments
WHERE pending = 1 AND hash = ?
""",
(invoice.payment_hash,),
)
# payments
# --------
def create_payment(
*,
wallet_id: str,
checking_id: str,
payment_request: str,
payment_hash: str,
amount: int,
memo: str,
fee: int = 0,
preimage: Optional[str] = None,
pending: bool = True,
extra: Optional[Dict] = None,
) -> Payment:
g.db.execute(
"""
INSERT INTO apipayments
(wallet, checking_id, bolt11, hash, preimage,
amount, pending, memo, fee, extra)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
wallet_id,
checking_id,
payment_request,
payment_hash,
preimage,
amount,
int(pending),
memo,
fee,
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None,
),
)
new_payment = get_wallet_payment(wallet_id, payment_hash)
assert new_payment, "Newly created payment couldn't be retrieved"
return new_payment
def update_payment_status(checking_id: str, pending: bool) -> None:
g.db.execute(
"UPDATE apipayments SET pending = ? WHERE checking_id = ?",
(
int(pending),
checking_id,
),
)
def delete_payment(checking_id: str) -> None:
g.db.execute("DELETE FROM apipayments WHERE checking_id = ?", (checking_id,))
def check_internal(payment_hash: str) -> Optional[str]:
row = g.db.fetchone(
"""
SELECT checking_id FROM apipayments
WHERE hash = ? AND pending AND amount > 0
""",
(payment_hash,),
)
if not row:
return None
else:
return row["checking_id"]
+122
View File
@@ -0,0 +1,122 @@
import sqlite3
def m000_create_migrations_table(db):
db.execute(
"""
CREATE TABLE dbversions (
db TEXT PRIMARY KEY,
version INT NOT NULL
)
"""
)
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 m002_add_fields_to_apipayments(db):
"""
Adding fields to apipayments for better accounting,
and renaming payhash to checking_id since that is what it really is.
"""
try:
db.execute("ALTER TABLE apipayments RENAME COLUMN payhash TO checking_id")
db.execute("ALTER TABLE apipayments ADD COLUMN hash TEXT")
db.execute("CREATE INDEX by_hash ON apipayments (hash)")
db.execute("ALTER TABLE apipayments ADD COLUMN preimage TEXT")
db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT")
db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT")
import json
rows = db.fetchall("SELECT * FROM apipayments")
for row in rows:
if not row["memo"] or not row["memo"].startswith("#"):
continue
for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]:
prefix = "#" + ext + " "
if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :]
db.execute(
"""
UPDATE apipayments SET extra = ?, memo = ?
WHERE checking_id = ? AND memo = ?
""",
(json.dumps({"tag": ext}), new, row["checking_id"], row["memo"]),
)
break
except sqlite3.OperationalError:
# this is necessary now because it may be the case that this migration will
# run twice in some environments.
# catching errors like this won't be necessary in anymore now that we
# keep track of db versions so no migration ever runs twice.
pass
+147
View File
@@ -0,0 +1,147 @@
import json
import hmac
import hashlib
from ecdsa import SECP256k1, SigningKey # type: ignore
from typing import List, NamedTuple, Optional, Dict
from sqlite3 import Row
from lnbits.settings import WALLET
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_msat // 1000
def lnurlauth_key(self, domain: str) -> SigningKey:
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
return SigningKey.from_string(
linking_key,
curve=SECP256k1,
hashfunc=hashlib.sha256,
)
def get_payment(self, payment_hash: str) -> Optional["Payment"]:
from .crud import get_wallet_payment
return get_wallet_payment(self.id, payment_hash)
def get_payments(
self,
*,
complete: bool = True,
pending: bool = False,
outgoing: bool = True,
incoming: bool = True,
exclude_uncheckable: bool = False
) -> List["Payment"]:
from .crud import get_wallet_payments
return get_wallet_payments(
self.id,
complete=complete,
pending=pending,
outgoing=outgoing,
incoming=incoming,
exclude_uncheckable=exclude_uncheckable,
)
class Payment(NamedTuple):
checking_id: str
pending: bool
amount: int
fee: int
memo: str
time: int
bolt11: str
preimage: str
payment_hash: str
extra: Dict
wallet_id: str
@classmethod
def from_row(cls, row: Row):
return cls(
checking_id=row["checking_id"],
payment_hash=row["hash"] or "0" * 64,
bolt11=row["bolt11"] or "",
preimage=row["preimage"] or "0" * 64,
extra=json.loads(row["extra"] or "{}"),
pending=row["pending"],
amount=row["amount"],
fee=row["fee"],
memo=row["memo"],
time=row["time"],
wallet_id=row["wallet"],
)
@property
def tag(self) -> Optional[str]:
return self.extra.get("tag")
@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
@property
def is_uncheckable(self) -> bool:
return self.checking_id.startswith("temp_") or self.checking_id.startswith("internal_")
def set_pending(self, pending: bool) -> None:
from .crud import update_payment_status
update_payment_status(self.checking_id, pending)
def check_pending(self) -> None:
if self.is_uncheckable:
return
if self.is_out:
pending = WALLET.get_payment_status(self.checking_id)
else:
pending = WALLET.get_invoice_status(self.checking_id)
self.set_pending(pending.pending)
def delete(self) -> None:
from .crud import delete_payment
delete_payment(self.checking_id)
+238
View File
@@ -0,0 +1,238 @@
import trio # type: ignore
import json
import httpx
from io import BytesIO
from binascii import unhexlify
from typing import Optional, Tuple, Dict
from urllib.parse import urlparse, parse_qs
from quart import g
from lnurl import LnurlErrorResponse, LnurlWithdrawResponse # type: ignore
try:
from typing import TypedDict # type: ignore
except ImportError: # pragma: nocover
from typing_extensions import TypedDict
from lnbits import bolt11
from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import WALLET
from lnbits.wallets.base import PaymentStatus, PaymentResponse
from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status, get_wallet_payment
def create_invoice(
*,
wallet_id: str,
amount: int, # in satoshis
memo: str,
description_hash: Optional[bytes] = None,
extra: Optional[Dict] = None,
) -> Tuple[str, str]:
invoice_memo = None if description_hash else memo
storeable_memo = memo
ok, checking_id, payment_request, error_message = WALLET.create_invoice(
amount=amount, memo=invoice_memo, description_hash=description_hash
)
if not ok:
raise Exception(error_message or "Unexpected backend error.")
invoice = bolt11.decode(payment_request)
amount_msat = amount * 1000
create_payment(
wallet_id=wallet_id,
checking_id=checking_id,
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=amount_msat,
memo=storeable_memo,
extra=extra,
)
g.db.commit()
return invoice.payment_hash, payment_request
def pay_invoice(
*,
wallet_id: str,
payment_request: str,
max_sat: Optional[int] = None,
extra: Optional[Dict] = None,
description: str = "",
) -> str:
temp_id = f"temp_{urlsafe_short_hash()}"
internal_id = f"internal_{urlsafe_short_hash()}"
invoice = bolt11.decode(payment_request)
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.")
# put all parameters that don't change here
PaymentKwargs = TypedDict(
"PaymentKwargs",
{
"wallet_id": str,
"payment_request": str,
"payment_hash": str,
"amount": int,
"memo": str,
"extra": Optional[Dict],
},
)
payment_kwargs: PaymentKwargs = dict(
wallet_id=wallet_id,
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=-invoice.amount_msat,
memo=description or invoice.description or "",
extra=extra,
)
# check_internal() returns the checking_id of the invoice we're waiting for
internal_checking_id = check_internal(invoice.payment_hash)
if internal_checking_id:
# create a new payment from this wallet
create_payment(checking_id=internal_id, fee=0, pending=False, **payment_kwargs)
else:
# create a temporary payment here so we can check if
# the balance is enough in the next step
fee_reserve = max(1000, int(invoice.amount_msat * 0.01))
create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs)
# do the balance check
wallet = get_wallet(wallet_id)
assert wallet
if wallet.balance_msat < 0:
g.db.rollback()
raise PermissionError("Insufficient balance.")
else:
g.db.commit()
if internal_checking_id:
# mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure
# the payer has enough to deduct from
update_payment_status(checking_id=internal_checking_id, pending=False)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_paid
try:
internal_invoice_paid.send_nowait(internal_checking_id)
except trio.WouldBlock:
pass
else:
# actually pay the external invoice
payment: PaymentResponse = WALLET.pay_invoice(payment_request)
if payment.ok and payment.checking_id:
create_payment(
checking_id=payment.checking_id,
fee=payment.fee_msat,
preimage=payment.preimage,
**payment_kwargs,
)
delete_payment(temp_id)
else:
raise Exception(payment.error_message or "Failed to pay_invoice on backend.")
g.db.commit()
return invoice.payment_hash
async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo: Optional[str] = None) -> None:
_, payment_request = create_invoice(
wallet_id=wallet_id,
amount=res.max_sats,
memo=memo or res.default_description or "",
extra={"tag": "lnurlwallet"},
)
async with httpx.AsyncClient() as client:
await client.get(
res.callback.base,
params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
)
async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
cb = urlparse(callback)
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
key = g.wallet.lnurlauth_key(cb.netloc)
def int_to_bytes_suitable_der(x: int) -> bytes:
"""for strict DER we need to encode the integer with some quirks"""
b = x.to_bytes((x.bit_length() + 7) // 8, "big")
if len(b) == 0:
# ensure there's at least one byte when the int is zero
return bytes([0])
if b[0] & 0x80 != 0:
# ensure it doesn't start with a 0x80 and so it isn't
# interpreted as a negative number
return bytes([0]) + b
return b
def encode_strict_der(r_int, s_int, order):
# if s > order/2 verification will fail sometimes
# so we must fix it here (see https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147)
if s_int > order // 2:
s_int = order - s_int
# now we do the strict DER encoding copied from
# https://github.com/KiriKiri/bip66 (without any checks)
r = int_to_bytes_suitable_der(r_int)
s = int_to_bytes_suitable_der(s_int)
r_len = len(r)
s_len = len(s)
sign_len = 6 + r_len + s_len
signature = BytesIO()
signature.write(0x30 .to_bytes(1, "big", signed=False))
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02 .to_bytes(1, "big", signed=False))
signature.write(r_len.to_bytes(1, "big", signed=False))
signature.write(r)
signature.write(0x02 .to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s)
return signature.getvalue()
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
async with httpx.AsyncClient() as client:
r = await client.get(
callback,
params={
"k1": k1.hex(),
"key": key.verifying_key.to_string("compressed").hex(),
"sig": sig.hex(),
},
)
try:
resp = json.loads(r.text)
if resp["status"] == "OK":
return None
return LnurlErrorResponse(reason=resp["reason"])
except (KeyError, json.decoder.JSONDecodeError):
return LnurlErrorResponse(
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,
)
def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus:
payment = get_wallet_payment(wallet_id, payment_hash)
if not payment:
return PaymentStatus(None)
return WALLET.get_invoice_status(payment.checking_id)
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+4
View File
@@ -0,0 +1,4 @@
new Vue({
el: '#vue',
mixins: [windowMixin]
})
+25
View File
@@ -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
})
}
}
})
+648
View File
@@ -0,0 +1,648 @@
/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart, decryptLnurlPayAES */
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.filter(p => !p.pending).sort((a, b) => a.time - b.time),
tx => {
txs.push({
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
sat: tx.sat
})
}
)
_.each(_.groupBy(txs, 'hour'), (value, day) => {
var income = _.reduce(
value,
(memo, tx) => (tx.sat >= 0 ? memo + tx.sat : memo),
0
)
var outcome = _.reduce(
value,
(memo, tx) => (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 {
user: LNbits.map.user(window.user),
receive: {
show: false,
status: 'pending',
paymentReq: null,
paymentHash: null,
minMax: [0, 2100000000000000],
lnurl: null,
data: {
amount: null,
memo: ''
}
},
parse: {
show: false,
invoice: null,
lnurlpay: null,
lnurlauth: null,
data: {
request: '',
amount: 0,
comment: ''
},
paymentChecker: null,
camera: {
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
},
{
name: 'fee',
align: 'right',
label: 'Fee (msat)',
field: 'fee'
}
],
pagination: {
rowsPerPage: 10
},
filter: null
},
paymentsChart: {
show: false
},
disclaimerDialog: {
show: false,
location: window.location
},
balance: 0
}
},
computed: {
formattedBalance: function () {
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
},
filteredPayments: function () {
var q = this.paymentsTable.filter
if (!q || q === '') return this.payments
return LNbits.utils.search(this.payments, q)
},
canPay: function () {
if (!this.parse.invoice) return false
return this.parse.invoice.sat <= this.balance
},
pendingPaymentsExist: function () {
return this.payments
? _.where(this.payments, {pending: 1}).length > 0
: false
}
},
filters: {
msatoshiFormat: function (value) {
return LNbits.utils.formatSat(value / 1000)
}
},
methods: {
closeCamera: function () {
this.parse.camera.show = false
},
showCamera: function () {
this.parse.camera.show = true
},
showChart: function () {
this.paymentsChart.show = true
this.$nextTick(() => {
generateChart(this.$refs.canvas, this.payments)
})
},
showReceiveDialog: function () {
this.receive.show = true
this.receive.status = 'pending'
this.receive.paymentReq = null
this.receive.paymentHash = null
this.receive.data.amount = null
this.receive.data.memo = null
this.receive.paymentChecker = null
this.receive.minMax = [0, 2100000000000000]
this.receive.lnurl = null
},
showParseDialog: function () {
this.parse.show = true
this.parse.invoice = null
this.parse.lnurlpay = null
this.parse.lnurlauth = null
this.parse.data.request = ''
this.parse.data.comment = ''
this.parse.data.paymentChecker = null
this.parse.camera.show = false
},
closeReceiveDialog: function () {
setTimeout(() => {
clearInterval(this.receive.paymentChecker)
}, 10000)
},
closeParseDialog: function () {
setTimeout(() => {
clearInterval(this.parse.paymentChecker)
}, 10000)
},
onPaymentReceived: function (paymentHash) {
this.fetchPayments()
this.fetchBalance()
if (this.receive.paymentHash === paymentHash) {
this.receive.show = false
this.receive.paymentHash = null
clearInterval(this.receive.paymentChecker)
}
},
createInvoice: function () {
this.receive.status = 'loading'
LNbits.api
.createInvoice(
this.g.wallet,
this.receive.data.amount,
this.receive.data.memo,
this.receive.lnurl && this.receive.lnurl.callback
)
.then(response => {
this.receive.status = 'success'
this.receive.paymentReq = response.data.payment_request
this.receive.paymentHash = response.data.payment_hash
if (response.data.lnurl_response !== null) {
if (response.data.lnurl_response === false) {
response.data.lnurl_response = `Unable to connect`
}
if (typeof response.data.lnurl_response === 'string') {
// failure
this.$q.notify({
timeout: 5000,
type: 'warning',
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
caption: response.data.lnurl_response
})
return
} else if (response.data.lnurl_response === true) {
// success
this.$q.notify({
timeout: 5000,
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
spinner: true
})
}
}
clearInterval(this.receive.paymentChecker)
setTimeout(() => {
clearInterval(this.receive.paymentChecker)
}, 40000)
this.receive.paymentChecker = setInterval(() => {
let hash = response.data.payment_hash
LNbits.api.getPayment(this.g.wallet, hash).then(response => {
if (response.data.paid) {
this.onPaymentReceived(hash)
}
})
}, 5000)
})
.catch(err => {
LNbits.utils.notifyApiError(err)
this.receive.status = 'pending'
})
},
decodeQR: function (res) {
this.parse.data.request = res
this.decodeRequest()
this.parse.camera.show = false
},
decodeRequest: function () {
this.parse.show = true
if (this.parse.data.request.startsWith('lightning:')) {
this.parse.data.request = this.parse.data.request.slice(10)
} else if (this.parse.data.request.startsWith('lnurl:')) {
this.parse.data.request = this.parse.data.request.slice(6)
} else if (this.parse.data.request.indexOf('lightning=lnurl1') !== -1) {
this.parse.data.request = this.parse.data.request
.split('lightning=')[1]
.split('&')[0]
}
if (this.parse.data.request.toLowerCase().startsWith('lnurl1')) {
LNbits.api
.request(
'GET',
'/api/v1/lnurlscan/' + this.parse.data.request,
this.g.wallet.adminkey
)
.catch(err => {
LNbits.utils.notifyApiError(err)
})
.then(response => {
let data = response.data
if (data.status === 'ERROR') {
this.$q.notify({
timeout: 5000,
type: 'warning',
message: `${data.domain} lnurl call failed.`,
caption: data.reason
})
return
}
if (data.kind === 'pay') {
this.parse.lnurlpay = Object.freeze(data)
this.parse.data.amount = data.minSendable / 1000
} else if (data.kind === 'auth') {
this.parse.lnurlauth = Object.freeze(data)
} else if (data.kind === 'withdraw') {
this.parse.show = false
this.receive.show = true
this.receive.status = 'pending'
this.receive.paymentReq = null
this.receive.paymentHash = null
this.receive.data.amount = data.maxWithdrawable / 1000
this.receive.data.memo = data.defaultDescription
this.receive.minMax = [
data.minWithdrawable / 1000,
data.maxWithdrawable / 1000
]
this.receive.lnurl = {
domain: data.domain,
callback: data.callback,
fixed: data.fixed
}
}
})
return
}
let invoice
try {
invoice = decode(this.parse.data.request)
} catch (error) {
this.$q.notify({
timeout: 3000,
type: 'warning',
message: error + '.',
caption: '400 BAD REQUEST'
})
this.parse.show = false
return
}
let cleanInvoice = {
msat: invoice.human_readable_part.amount,
sat: invoice.human_readable_part.amount / 1000,
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
}
_.each(invoice.data.tags, 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.parse.invoice = Object.freeze(cleanInvoice)
},
payInvoice: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...'
})
LNbits.api
.payInvoice(this.g.wallet, this.parse.data.request)
.then(response => {
clearInterval(this.parse.paymentChecker)
setTimeout(() => {
clearInterval(this.parse.paymentChecker)
}, 40000)
this.parse.paymentChecker = setInterval(() => {
LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash)
.then(res => {
if (res.data.paid) {
this.parse.show = false
clearInterval(this.parse.paymentChecker)
dismissPaymentMsg()
this.fetchPayments()
this.fetchBalance()
}
})
}, 2000)
})
.catch(err => {
dismissPaymentMsg()
LNbits.utils.notifyApiError(err)
})
},
payLnurl: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...'
})
LNbits.api
.payLnurl(
this.g.wallet,
this.parse.lnurlpay.callback,
this.parse.lnurlpay.description_hash,
this.parse.data.amount * 1000,
this.parse.lnurlpay.description.slice(0, 120),
this.parse.data.comment
)
.then(response => {
this.parse.show = false
clearInterval(this.parse.paymentChecker)
setTimeout(() => {
clearInterval(this.parse.paymentChecker)
}, 40000)
this.parse.paymentChecker = setInterval(() => {
LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash)
.then(res => {
if (res.data.paid) {
dismissPaymentMsg()
clearInterval(this.parse.paymentChecker)
this.fetchPayments()
this.fetchBalance()
// show lnurlpay success action
if (response.data.success_action) {
switch (response.data.success_action.tag) {
case 'url':
this.$q.notify({
message: `<a target="_blank" style="color: inherit" href="${response.data.success_action.url}">${response.data.success_action.url}</a>`,
caption: response.data.success_action.description,
html: true,
type: 'positive',
timeout: 0,
closeBtn: true
})
break
case 'message':
this.$q.notify({
message: response.data.success_action.message,
type: 'positive',
timeout: 0,
closeBtn: true
})
break
case 'aes':
LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash)
.then(({data: payment}) =>
decryptLnurlPayAES(
response.data.success_action,
payment.preimage
)
)
.then(value => {
this.$q.notify({
message: value,
caption: response.data.success_action.description,
html: true,
type: 'positive',
timeout: 0,
closeBtn: true
})
})
break
}
}
}
})
}, 2000)
})
.catch(err => {
dismissPaymentMsg()
LNbits.utils.notifyApiError(err)
})
},
authLnurl: function () {
let dismissAuthMsg = this.$q.notify({
timeout: 10,
message: 'Performing authentication...'
})
LNbits.api
.authLnurl(this.g.wallet, this.parse.lnurlauth.callback)
.then(response => {
dismissAuthMsg()
this.$q.notify({
message: `Authentication successful.`,
type: 'positive',
timeout: 3500
})
this.parse.show = false
})
.catch(err => {
dismissAuthMsg()
if (err.response.data.reason) {
this.$q.notify({
message: `Authentication failed. ${this.parse.lnurlauth.domain} says:`,
caption: err.response.data.reason,
type: 'warning',
timeout: 5000
})
} else {
LNbits.utils.notifyApiError(err)
}
})
},
deleteWallet: function (walletId, user) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet?')
.onOk(() => {
LNbits.href.deleteWallet(walletId, user)
})
},
fetchPayments: function (checkPending) {
return LNbits.api
.getPayments(this.g.wallet, checkPending)
.then(response => {
this.payments = response.data
.map(obj => {
return LNbits.map.payment(obj)
})
.sort((a, b) => {
return b.time - a.time
})
})
},
fetchBalance: function () {
LNbits.api.getWallet(this.g.wallet).then(response => {
this.balance = Math.round(response.data.balance / 1000)
EventHub.$emit('update-wallet-balance', [
this.g.wallet.id,
this.balance
])
})
},
checkPendingPayments: function () {
var dismissMsg = this.$q.notify({
timeout: 0,
message: 'Checking pending transactions...'
})
this.fetchPayments(true).then(() => {
dismissMsg()
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments)
}
},
watch: {
payments: function () {
this.fetchBalance()
}
},
created: function () {
this.fetchBalance()
this.fetchPayments()
setTimeout(this.checkPendingPayments(), 1200)
},
mounted: function () {
// show disclaimer
if (
this.$refs.disclaimer &&
!this.$q.localStorage.getItem('lnbits.disclaimerShown')
) {
this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true)
}
// listen to incoming payments
LNbits.events.onInvoicePaid(this.g.wallet, payment =>
this.onPaymentReceived(payment.payment_hash)
)
}
})
+22
View File
@@ -0,0 +1,22 @@
import trio # type: ignore
from typing import List
from lnbits.tasks import register_invoice_listener
sse_listeners: List[trio.MemorySendChannel] = []
async def register_listeners():
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(5)
register_invoice_listener(invoice_paid_chan_send)
await wait_for_paid_invoices(invoice_paid_chan_recv)
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
async for payment in invoice_paid_chan:
for send_channel in sse_listeners:
try:
send_channel.send_nowait(payment)
except trio.WouldBlock:
print("removing sse listener", send_channel)
sse_listeners.remove(send_channel)
+119
View File
@@ -0,0 +1,119 @@
<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 wallet details"
>
<q-card>
<q-card-section>
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "<i>{{ wallet.adminkey }}</i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "name": &lt;string&gt;, "balance":
&lt;int&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl {{ request.url_root }}api/v1/wallet -H "X-Api-Key:
<i>{{ wallet.inkey }}</i>"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<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": &lt;int&gt;, "memo": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"payment_hash": &lt;string&gt;, "payment_request":
&lt;string&gt;}</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": &lt;int&gt;, "memo": &lt;string&gt;}' -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": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"payment_hash": &lt;string&gt;}</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": &lt;string&gt;}' -H "X-Api-Key:
<i>{{ 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/&lt;payment_hash&gt;</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": &lt;bool&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}api/v1/payments/&lt;payment_hash&gt; -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>
@@ -0,0 +1,54 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script src="/core/static/js/extensions.js"></script>
{% 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 %}
+110
View File
@@ -0,0 +1,110 @@
{% extends "public.html" %} {% block scripts %}
<script src="/core/static/js/index.js"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
{% if lnurl %}
<q-btn
unelevated
color="deep-purple"
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}"
>
Press to claim bitcoin
</q-btn>
{% else %}
<q-form @submit="createWallet" class="q-gutter-md">
<q-input
filled
dense
v-model="walletName"
label="Name your LNbits wallet *"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="walletName == ''"
type="submit"
>Add a new wallet</q-btn
>
</q-form>
{% endif %}
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<h3 class="q-my-none"><strong>LN</strong>bits</h3>
<h5 class="q-my-md">Free and open-source lightning wallet</h5>
<p>
Easy to set up and lightweight, LNbits can run on any
lightning-network funding source, currently supporting LND,
c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself!
</p>
<p>
You can run LNbits for yourself, or easily offer a custodian solution
for others.
</p>
<p>
Each wallet has its own API keys and there is no limit to the number
of wallets you can make. Being able to partition funds makes LNbits a
useful tool for money management and as a development tool.
</p>
<p>
Extensions add extra functionality to LNbits so you can experiment
with a range of cutting-edge technologies on the lightning network. We
have made developing extensions as easy as possible, and as a free and
open-source project, we encourage people to develop and submit their
own.
</p>
<div class="row q-mt-md q-gutter-sm">
<q-btn
outline
color="grey"
type="a"
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener"
>View project in GitHub</q-btn
>
<q-btn
outline
color="grey"
type="a"
href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener"
>Donate</q-btn
>
</div>
</q-card-section>
</q-card>
</div>
<!-- Ads -->
<div class="col-12 col-md-3 col-lg-3">
<div class="row q-col-gutter-lg justify-center">
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn
flat
color="purple"
label="Supporters"
type="a"
href="mailto:lnbits@pm.me"
class="full-width"
></q-btn>
<a href="https://github.com/rootzoll/raspiblitz">
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/raspiblitz-negative.svg' : '/static/images/raspiblitz.svg'"
style="height: 90px"
></q-img>
</a>
</div>
</div>
</div>
</div>
{% endblock %}
+543
View File
@@ -0,0 +1,543 @@
{% extends "base.html" %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user, wallet) }}
<script src="/core/static/js/wallet.js"></script>
{% endblock %}
<!---->
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% 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 %}{{ formattedBalance }}{% 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="deep-purple"
class="full-width"
@click="showParseDialog"
>Paste Request</q-btn
>
</div>
<div class="col">
<q-btn
unelevated
color="deep-purple"
class="full-width"
@click="showReceiveDialog"
>Create Invoice</q-btn
>
</div>
<div class="col">
<q-btn
unelevated
color="purple"
icon="photo_camera"
@click="showCamera"
>scan
<q-tooltip>Use camera to scan an invoice/QR</q-tooltip>
</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 tag, memo, amount"
class="q-mb-md"
>
</q-input>
<q-table
dense
flat
:data="filteredPayments"
row-key="payment_hash"
: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="text-center">
<q-icon
v-if="props.row.isPaid"
size="14px"
:name="props.row.isOut ? 'call_made' : 'call_received'"
:color="props.row.isOut ? 'pink' : 'green'"
@click="props.expand = !props.expand"
></q-icon>
<q-icon
v-else
name="settings_ethernet"
color="grey"
@click="props.expand = !props.expand"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
</q-td>
<q-td
key="memo"
:props="props"
style="white-space: normal; word-break: break-all"
>
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a
class="inherit"
:href="['/', props.row.tag, '?usr=', user.id].join('')"
>
#{{ props.row.tag }}
</a>
</q-badge>
{{ props.row.memo }}
</q-td>
<q-td auto-width key="date" :props="props">
<q-tooltip>{{ props.row.date }}</q-tooltip>
{{ props.row.dateFrom }}
</q-td>
<q-td auto-width key="sat" :props="props">
{{ props.row.fsat }}
</q-td>
<q-td auto-width key="fee" :props="props">
{{ props.row.fee }}
</q-td>
</q-tr>
<q-dialog v-model="props.expand" :props="props">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<div v-if="props.row.isIn && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Invoice waiting to be paid
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
<div v-if="props.row.bolt11" class="text-center q-mb-lg">
<a :href="'lightning:' + props.row.bolt11">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="props.row.bolt11"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(props.row.bolt11)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Close</q-btn
>
</div>
</div>
<div v-else-if="props.row.isPaid && props.row.isIn">
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
Payment Received
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isPaid && props.row.isOut">
<q-icon
size="18px"
:name="'call_made'"
:color="'pink'"
></q-icon>
Payment Sent
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isOut && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Outgoing payment pending
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
</div>
</q-card>
</q-dialog>
</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" @hide="closeReceiveDialog">
{% raw %}
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p>
<q-input
filled
dense
v-model.number="receive.data.amount"
type="number"
label="Amount (sat) *"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></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"
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
</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>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
{% raw %}
<h6 class="q-my-none">{{ parse.invoice.fsat }} sat</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="deep-purple" @click="payInvoice">Pay</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>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair will be
deterministically generated so your identity can't be tied to your
LNbits wallet or linked across websites. No other data will be shared
with {{ parse.lnurlauth.domain }}.
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="deep-purple" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }} sat
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.domain }}</b> is requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b> sat
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount (sat) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
filled
dense
v-model.number="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="deep-purple" type="submit"
>Send satoshis</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else>
<q-form
v-if="!parse.camera.show"
@submit="decodeRequest"
class="q-gutter-md"
>
<q-input
filled
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="parse.data.request == ''"
type="submit"
>Read</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>
</q-card>
</q-dialog>
<q-dialog v-model="parse.camera.show">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show">
<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 %}
View File
+375
View File
@@ -0,0 +1,375 @@
import trio # type: ignore
import json
import lnurl # type: ignore
import httpx
import traceback
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
from quart import g, jsonify, request, make_response
from http import HTTPStatus
from binascii import unhexlify
from typing import Dict, Union
from lnbits import bolt11
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from .. import core_app
from ..services import create_invoice, pay_invoice, perform_lnurlauth
from ..crud import delete_expired_invoices
from ..tasks import sse_listeners
@core_app.route("/api/v1/wallet", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_wallet():
return (
jsonify(
{
"id": g.wallet.id,
"name": g.wallet.name,
"balance": g.wallet.balance_msat,
}
),
HTTPStatus.OK,
)
@core_app.route("/api/v1/payments", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_payments():
if "check_pending" in request.args:
delete_expired_invoices()
for payment in g.wallet.get_payments(complete=False, pending=True, exclude_uncheckable=True):
payment.check_pending()
return jsonify(g.wallet.get_payments(pending=True)), HTTPStatus.OK
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"amount": {"type": "integer", "min": 1, "required": True},
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
}
)
async def api_payments_create_invoice():
if "description_hash" in g.data:
description_hash = unhexlify(g.data["description_hash"])
memo = ""
else:
description_hash = b""
memo = g.data["memo"]
try:
payment_hash, payment_request = create_invoice(
wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash
)
except Exception as e:
g.db.rollback()
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
invoice = bolt11.decode(payment_request)
lnurl_response: Union[None, bool, str] = None
if g.data.get("lnurl_callback"):
async with httpx.AsyncClient() as client:
try:
r = await client.get(
g.data["lnurl_callback"],
params={"pr": payment_request},
timeout=10,
)
if r.is_error:
lnurl_response = r.text
else:
resp = json.loads(r.text)
if resp["status"] != "OK":
lnurl_response = resp["reason"]
else:
lnurl_response = True
except (httpx.ConnectError, httpx.RequestError):
lnurl_response = False
return (
jsonify(
{
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
"lnurl_response": lnurl_response,
}
),
HTTPStatus.CREATED,
)
@api_check_wallet_key("admin")
@api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}})
async def api_payments_pay_invoice():
try:
payment_hash = pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"])
except ValueError as e:
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
except PermissionError as e:
return jsonify({"message": str(e)}), HTTPStatus.FORBIDDEN
except Exception as exc:
traceback.print_exc(7)
g.db.rollback()
return jsonify({"message": str(exc)}), HTTPStatus.INTERNAL_SERVER_ERROR
return (
jsonify(
{
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
),
HTTPStatus.CREATED,
)
@core_app.route("/api/v1/payments", methods=["POST"])
@api_validate_post_request(schema={"out": {"type": "boolean", "required": True}})
async def api_payments_create():
if g.data["out"] is True:
return await api_payments_pay_invoice()
return await api_payments_create_invoice()
@core_app.route("/api/v1/payments/lnurl", methods=["POST"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"description_hash": {"type": "string", "empty": False, "required": True},
"callback": {"type": "string", "empty": False, "required": True},
"amount": {"type": "number", "empty": False, "required": True},
"comment": {"type": "string", "nullable": True, "empty": True, "required": False},
"description": {"type": "string", "nullable": True, "empty": True, "required": False},
}
)
async def api_payments_pay_lnurl():
domain = urlparse(g.data["callback"]).netloc
async with httpx.AsyncClient() as client:
try:
r = await client.get(
g.data["callback"],
params={"amount": g.data["amount"], "comment": g.data["comment"]},
timeout=40,
)
if r.is_error:
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
except (httpx.ConnectError, httpx.RequestError):
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
params = json.loads(r.text)
if params.get("status") == "ERROR":
return jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}), HTTPStatus.BAD_REQUEST
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != g.data["amount"]:
return (
jsonify(
{
"message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
}
),
HTTPStatus.BAD_REQUEST,
)
if invoice.description_hash != g.data["description_hash"]:
return (
jsonify(
{
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
}
),
HTTPStatus.BAD_REQUEST,
)
try:
extra = {}
if params.get("successAction"):
extra["success_action"] = params["successAction"]
if g.data["comment"]:
extra["comment"] = g.data["comment"]
payment_hash = pay_invoice(
wallet_id=g.wallet.id,
payment_request=params["pr"],
description=g.data.get("description", ""),
extra=extra,
)
except Exception as exc:
traceback.print_exc(7)
g.db.rollback()
return jsonify({"message": str(exc)}), HTTPStatus.INTERNAL_SERVER_ERROR
return (
jsonify(
{
"success_action": params.get("successAction"),
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
),
HTTPStatus.CREATED,
)
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_payment(payment_hash):
payment = g.wallet.get_payment(payment_hash)
if not payment:
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
elif not payment.pending:
return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK
try:
payment.check_pending()
except Exception:
return jsonify({"paid": False}), HTTPStatus.OK
return jsonify({"paid": not payment.pending, "preimage": payment.preimage}), HTTPStatus.OK
@core_app.route("/api/v1/payments/sse", methods=["GET"])
@api_check_wallet_key("invoice", accept_querystring=True)
async def api_payments_sse():
g.db.close()
this_wallet_id = g.wallet.id
send_payment, receive_payment = trio.open_memory_channel(0)
print("adding sse listener", send_payment)
sse_listeners.append(send_payment)
send_event, event_to_send = trio.open_memory_channel(0)
async def payment_received() -> None:
async for payment in receive_payment:
if payment.wallet_id == this_wallet_id:
await send_event.send(("payment-received", payment))
async def repeat_keepalive():
await trio.sleep(1)
while True:
await send_event.send(("keepalive", ""))
await trio.sleep(25)
g.nursery.start_soon(payment_received)
g.nursery.start_soon(repeat_keepalive)
async def send_events():
try:
async for typ, data in event_to_send:
message = [f"event: {typ}".encode("utf-8")]
if data:
jdata = json.dumps(dict(data._asdict(), pending=False))
message.append(f"data: {jdata}".encode("utf-8"))
yield b"\n".join(message) + b"\r\n\r\n"
except trio.Cancelled:
return
response = await make_response(
send_events(),
{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
"Transfer-Encoding": "chunked",
},
)
response.timeout = None
return response
@core_app.route("/api/v1/lnurlscan/<code>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_lnurlscan(code: str):
try:
url = lnurl.Lnurl(code)
except ValueError:
return jsonify({"message": "invalid lnurl"}), HTTPStatus.BAD_REQUEST
domain = urlparse(url.url).netloc
# params is what will be returned to the client
params: Dict = {"domain": domain}
if url.is_login:
params.update(kind="auth")
params.update(callback=url.url) # with k1 already in it
lnurlauth_key = g.wallet.lnurlauth_key(domain)
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
r = await client.get(url.url, timeout=40)
if r.is_error:
return (
jsonify({"domain": domain, "message": "failed to get parameters"}),
HTTPStatus.SERVICE_UNAVAILABLE,
)
try:
jdata = json.loads(r.text)
data: lnurl.LnurlResponseModel = lnurl.LnurlResponse.from_dict(jdata)
except (json.decoder.JSONDecodeError, lnurl.exceptions.LnurlResponseException):
return (
jsonify({"domain": domain, "message": f"got invalid response '{r.text[:200]}'"}),
HTTPStatus.SERVICE_UNAVAILABLE,
)
if type(data) is lnurl.LnurlChannelResponse:
return jsonify({"domain": domain, "kind": "channel", "message": "unsupported"}), HTTPStatus.BAD_REQUEST
params.update(**data.dict())
if type(data) is lnurl.LnurlWithdrawResponse:
params.update(kind="withdraw")
params.update(fixed=data.min_withdrawable == data.max_withdrawable)
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data.callback)
qs: Dict = parse_qs(parsed_callback.query)
qs["k1"] = data.k1
parsed_callback = parsed_callback._replace(query=urlencode(qs, doseq=True))
params.update(callback=urlunparse(parsed_callback))
if type(data) is lnurl.LnurlPayResponse:
params.update(kind="pay")
params.update(fixed=data.min_sendable == data.max_sendable)
params.update(description_hash=data.metadata.h)
params.update(description=data.metadata.text)
if data.metadata.images:
image = min(data.metadata.images, key=lambda image: len(image[1]))
data_uri = "data:" + image[0] + "," + image[1]
params.update(image=data_uri)
params.update(commentAllowed=jdata.get("commentAllowed", 0))
return jsonify(params)
@core_app.route("/api/v1/lnurlauth", methods=["POST"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"callback": {"type": "string", "required": True},
}
)
async def api_perform_lnurlauth():
err = await perform_lnurlauth(g.data["callback"])
if err:
return jsonify({"reason": err.reason}), HTTPStatus.SERVICE_UNAVAILABLE
return "", HTTPStatus.OK
+133
View File
@@ -0,0 +1,133 @@
import trio # type: ignore
import httpx
from os import path
from http import HTTPStatus
from quart import g, abort, redirect, request, render_template, send_from_directory, url_for
from lnurl import LnurlResponse, LnurlWithdrawResponse, decode as decode_lnurl # type: ignore
from lnbits.core import core_app
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE
from lnbits.tasks import run_on_pseudo_request
from ..crud import (
create_account,
get_user,
update_user_extension,
create_wallet,
delete_wallet,
)
from ..services import redeem_lnurl_withdraw
@core_app.route("/favicon.ico")
async def favicon():
return await send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico")
@core_app.route("/")
async def home():
return await render_template("core/index.html", lnurl=request.args.get("lightning", None))
@core_app.route("/extensions")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def extensions():
extension_to_enable = request.args.get("enable", type=str)
extension_to_disable = request.args.get("disable", type=str)
if extension_to_enable and extension_to_disable:
abort(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
if extension_to_enable:
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
elif extension_to_disable:
update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0)
return await render_template("core/extensions.html", user=get_user(g.user.id))
@core_app.route("/wallet")
@validate_uuids(["usr", "wal"])
async def wallet():
user_id = request.args.get("usr", type=str)
wallet_id = request.args.get("wal", type=str)
wallet_name = request.args.get("nme", type=str)
service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE
# just wallet_name: create a new user, then create a new wallet for user with wallet_name
# just user_id: return the first user wallet or create one if none found (with default wallet_name)
# user_id and wallet_name: create a new wallet for user with wallet_name
# user_id and wallet_id: return that wallet if user is the owner
# nothing: create everything
if not user_id:
user = get_user(create_account().id)
else:
user = get_user(user_id) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
if not wallet_id:
if user.wallets and not wallet_name:
wallet = user.wallets[0]
else:
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
wallet = user.get_wallet(wallet_id)
if not wallet:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
return await render_template("core/wallet.html", user=user, wallet=wallet, service_fee=service_fee)
@core_app.route("/deletewallet")
@validate_uuids(["usr", "wal"], required=True)
@check_user_exists()
async def deletewallet():
wallet_id = request.args.get("wal", type=str)
user_wallet_ids = g.user.wallet_ids
if wallet_id not in user_wallet_ids:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
else:
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
user_wallet_ids.remove(wallet_id)
if user_wallet_ids:
return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0]))
return redirect(url_for("core.home"))
@core_app.route("/lnurlwallet")
async def lnurlwallet():
async with httpx.AsyncClient() as client:
try:
lnurl = decode_lnurl(request.args.get("lightning"))
r = await client.get(str(lnurl))
withdraw_res = LnurlResponse.from_dict(r.json())
if not withdraw_res.ok:
return f"Could not process lnurl-withdraw: {withdraw_res.error_msg}", HTTPStatus.BAD_REQUEST
if not isinstance(withdraw_res, LnurlWithdrawResponse):
return f"Expected an lnurl-withdraw code, got {withdraw_res.tag}", HTTPStatus.BAD_REQUEST
except Exception as exc:
return f"Could not process lnurl-withdraw: {exc}", HTTPStatus.INTERNAL_SERVER_ERROR
account = create_account()
user = get_user(account.id)
wallet = create_wallet(user_id=user.id)
g.db.commit()
await run_on_pseudo_request(
redeem_lnurl_withdraw, wallet.id, withdraw_res, "LNbits initial funding: voucher redeem."
)
await trio.sleep(3)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+66
View File
@@ -0,0 +1,66 @@
import os
import sqlite3
from .settings import LNBITS_DATA_FOLDER
class Database:
def __init__(self, db_path: str):
self.path = db_path
self.connection = sqlite3.connect(db_path)
self.connection.row_factory = sqlite3.Row
self.cursor = self.connection.cursor()
self.closed = False
def close(self):
self.__exit__(None, None, None)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.closed:
return
if exc_val:
self.connection.rollback()
self.cursor.close()
self.connection.close()
else:
self.connection.commit()
self.cursor.close()
self.connection.close()
self.closed = True
def commit(self):
self.connection.commit()
def rollback(self):
self.connection.rollback()
def fetchall(self, query: str, values: tuple = ()) -> list:
"""Given a query, return cursor.fetchall() rows."""
self.execute(query, values)
return self.cursor.fetchall()
def fetchone(self, query: str, values: tuple = ()):
self.execute(query, values)
return self.cursor.fetchone()
def execute(self, query: str, values: tuple = ()) -> None:
"""Given a query, cursor.execute() it."""
try:
self.cursor.execute(query, values)
except sqlite3.Error as exc:
self.connection.rollback()
raise exc
def open_db(db_name: str = "database") -> Database:
db_path = os.path.join(LNBITS_DATA_FOLDER, f"{db_name}.sqlite3")
return Database(db_path=db_path)
def open_ext_db(extension_name: str) -> Database:
return open_db(f"ext_{extension_name}")
+98
View File
@@ -0,0 +1,98 @@
from cerberus import Validator # type: ignore
from quart import g, abort, jsonify, request
from functools import wraps
from http import HTTPStatus
from typing import List, Union
from uuid import UUID
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.settings import LNBITS_ALLOWED_USERS
def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
try:
key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
g.wallet = get_wallet_for_key(key_value, key_type)
except KeyError:
return (
jsonify({"message": "`X-Api-Key` header missing."}),
HTTPStatus.BAD_REQUEST,
)
if not g.wallet:
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
return await view(**kwargs)
return wrapped_view
return wrap
def api_validate_post_request(*, schema: dict):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
if "application/json" not in request.headers["Content-Type"]:
return (
jsonify({"message": "Content-Type must be `application/json`."}),
HTTPStatus.BAD_REQUEST,
)
v = Validator(schema)
data = await request.get_json()
g.data = {key: data[key] for key in schema.keys() if key in data}
if not v.validate(g.data):
return (
jsonify({"message": f"Errors in request data: {v.errors}"}),
HTTPStatus.BAD_REQUEST,
)
return await view(**kwargs)
return wrapped_view
return wrap
def check_user_exists(param: str = "usr"):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
return await 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)
async def wrapped_view(**kwargs):
query_params = {param: request.args.get(param, type=str) for param in params}
for param, value in query_params.items():
if not value and (required is True or (required and param in required)):
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.")
if value:
try:
UUID(value, version=version)
except ValueError:
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
return await view(**kwargs)
return wrapped_view
return wrap
+11
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
from quart import Blueprint
amilk_ext: Blueprint = Blueprint("amilk", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
+6
View File
@@ -0,0 +1,6 @@
{
"name": "AMilk",
"short_description": "Assistant Faucet Milker",
"icon": "room_service",
"contributors": ["arcbtc"]
}
+44
View File
@@ -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,))
+15
View File
@@ -0,0 +1,15 @@
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
);
"""
)
+9
View File
@@ -0,0 +1,9 @@
from typing import NamedTuple
class AMilk(NamedTuple):
id: str
wallet: str
lnurl: str
atime: int
amount: int
@@ -0,0 +1,24 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
<p>
Milking faucets with software, known as "assmilking", seems at first to
be black-hat, although in fact there might be some unexplored use cases.
An LNURL withdraw gives someone the right to pull funds, which can be
done over time. An LNURL withdraw could be used outside of just faucets,
to provide money streaming and repeat payments.<br />Paste or scan an
LNURL withdraw, enter the amount for the AMilk to pull and the frequency
for it to be pulled.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -0,0 +1,250 @@
{% 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 %}
+20
View File
@@ -0,0 +1,20 @@
from quart import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.amilk import amilk_ext
from .crud import get_amilk
@amilk_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("amilk/index.html", user=g.user)
@amilk_ext.route("/<amilk_id>")
async def wall(amilk_id):
amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.")
return await render_template("amilk/wall.html", amilk=amilk)
+88
View File
@@ -0,0 +1,88 @@
import httpx
from quart import g, jsonify, request, abort
from http import HTTPStatus
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl
from lnurl.exceptions import LnurlException
from time import sleep
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.extensions.amilk import amilk_ext
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
@amilk_ext.route("/api/v1/amilk", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_amilks():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), HTTPStatus.OK
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
async def api_amilkit(amilk_id):
milk = get_amilk(amilk_id)
memo = milk.id
try:
withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse)
except LnurlException:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
payment_hash, payment_request = create_invoice(
wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo, extra={"tag": "amilk"}
)
r = httpx.get(
withdraw_res.callback.base,
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}},
)
if r.is_error:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
for i in range(10):
sleep(i)
invoice_status = check_invoice_status(milk.wallet, payment_hash)
if invoice_status.paid:
return jsonify({"paid": True}), HTTPStatus.OK
else:
continue
return jsonify({"paid": False}), HTTPStatus.OK
@amilk_ext.route("/api/v1/amilk", methods=["POST"])
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"lnurl": {"type": "string", "empty": False, "required": True},
"atime": {"type": "integer", "min": 0, "required": True},
"amount": {"type": "integer", "min": 0, "required": True},
}
)
async def api_amilk_create():
amilk = create_amilk(wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"])
return jsonify(amilk._asdict()), HTTPStatus.CREATED
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
async def api_amilk_delete(amilk_id):
amilk = get_amilk(amilk_id)
if not amilk:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
if amilk.wallet != g.wallet.id:
return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN
delete_amilk(amilk_id)
return "", HTTPStatus.NO_CONTENT
+10
View File
@@ -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 quart 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": ["benarc"]
}
+253
View File
@@ -0,0 +1,253 @@
from base64 import urlsafe_b64encode
from uuid import uuid4
from typing import List, Optional, Union
import httpx
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 = httpx.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 = httpx.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,60 @@
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
);
"""
)
+40
View File
@@ -0,0 +1,40 @@
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,122 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Diagon Alley: Decentralised Market-Stalls
</h5>
<p>
Make a list of products to sell, point your list of products at a public
indexer. Buyers browse your products on the indexer, and pay you
directly. Ratings are managed by the indexer. Your stall can be listed
in multiple indexers, even over TOR, if you wish to be anonymous.<br />
More information on the
<a href="https://github.com/lnbits/Diagon-Alley"
>Diagon Alley Protocol</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item
group="api"
dense
expand-separator
label="Get prodcuts, categorised by wallet"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</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/&lt;indexer_id&gt;</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/&lt;indexer_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"id": &lt;string&gt;, "address": &lt;string&gt;, "shippingzone":
&lt;integer&gt;, "email": &lt;string&gt;, "quantity":
&lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</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/&lt;indexer_id&gt; -d
'{"id": &lt;product_id&&gt;, "email": &lt;customer_email&gt;,
"address": &lt;customer_address&gt;, "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/&lt;checking_id&gt;</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": &lt;boolean&gt;}</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/&lt;checking_id&gt;
-H "Content-type: application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
@@ -0,0 +1,906 @@
{% 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,3 @@
<script>
console.log('{{ stall }}')
</script>
+11
View File
@@ -0,0 +1,11 @@
from quart import g, render_template
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.diagonalley import diagonalley_ext
@diagonalley_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("diagonalley/index.html", user=g.user)
+302
View File
@@ -0,0 +1,302 @@
from quart import g, jsonify, request
from http import HTTPStatus
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.diagonalley import diagonalley_ext
from .crud import (
create_diagonalleys_product,
get_diagonalleys_product,
get_diagonalleys_products,
delete_diagonalleys_product,
create_diagonalleys_indexer,
update_diagonalleys_indexer,
get_diagonalleys_indexer,
get_diagonalleys_indexers,
delete_diagonalleys_indexer,
create_diagonalleys_order,
get_diagonalleys_order,
get_diagonalleys_orders,
update_diagonalleys_product,
)
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")
async def api_diagonalley_products():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["POST"])
@diagonalley_ext.route("/api/v1/diagonalley/products<product_id>", methods=["PUT"])
@api_check_wallet_key(key_type="invoice")
@api_validate_post_request(
schema={
"product": {"type": "string", "empty": False, "required": True},
"categories": {"type": "string", "empty": False, "required": True},
"description": {"type": "string", "empty": False, "required": True},
"image": {"type": "string", "empty": False, "required": True},
"price": {"type": "integer", "min": 0, "required": True},
"quantity": {"type": "integer", "min": 0, "required": True},
}
)
async def api_diagonalley_product_create(product_id=None):
if product_id:
product = get_diagonalleys_indexer(product_id)
if not product:
return jsonify({"message": "Withdraw product does not exist."}), HTTPStatus.NOT_FOUND
if product.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw product."}), HTTPStatus.FORBIDDEN
product = update_diagonalleys_product(product_id, **g.data)
else:
product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data)
return jsonify(product._asdict()), HTTPStatus.OK if product_id else HTTPStatus.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/products/<product_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice")
async def api_diagonalley_products_delete(product_id):
product = get_diagonalleys_product(product_id)
if not product:
return jsonify({"message": "Product does not exist."}), HTTPStatus.NOT_FOUND
if product.wallet != g.wallet.id:
return jsonify({"message": "Not your Diagon Alley."}), HTTPStatus.FORBIDDEN
delete_diagonalleys_product(product_id)
return "", HTTPStatus.NO_CONTENT
###Indexers
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
async def api_diagonalley_indexers():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["POST"])
@diagonalley_ext.route("/api/v1/diagonalley/indexers<indexer_id>", methods=["PUT"])
@api_check_wallet_key(key_type="invoice")
@api_validate_post_request(
schema={
"shopname": {"type": "string", "empty": False, "required": True},
"indexeraddress": {"type": "string", "empty": False, "required": True},
"shippingzone1": {"type": "string", "empty": False, "required": True},
"shippingzone2": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
"zone1cost": {"type": "integer", "min": 0, "required": True},
"zone2cost": {"type": "integer", "min": 0, "required": True},
}
)
async def api_diagonalley_indexer_create(indexer_id=None):
if indexer_id:
indexer = get_diagonalleys_indexer(indexer_id)
if not indexer:
return jsonify({"message": "Withdraw indexer does not exist."}), HTTPStatus.NOT_FOUND
if indexer.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw indexer."}), HTTPStatus.FORBIDDEN
indexer = update_diagonalleys_indexer(indexer_id, **g.data)
else:
indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **g.data)
return jsonify(indexer._asdict()), HTTPStatus.OK if indexer_id else HTTPStatus.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice")
async def api_diagonalley_indexer_delete(indexer_id):
indexer = get_diagonalleys_indexer(indexer_id)
if not indexer:
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
if indexer.wallet != g.wallet.id:
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
delete_diagonalleys_indexer(indexer_id)
return "", HTTPStatus.NO_CONTENT
###Orders
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
async def api_diagonalley_orders():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["POST"])
@api_check_wallet_key(key_type="invoice")
@api_validate_post_request(
schema={
"id": {"type": "string", "empty": False, "required": True},
"address": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
"quantity": {"type": "integer", "empty": False, "required": True},
"shippingzone": {"type": "integer", "empty": False, "required": True},
}
)
async def api_diagonalley_order_create():
order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data)
return jsonify(order._asdict()), HTTPStatus.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/orders/<order_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice")
async def api_diagonalley_order_delete(order_id):
order = get_diagonalleys_order(order_id)
if not order:
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
if order.wallet != g.wallet.id:
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
delete_diagonalleys_indexer(order_id)
return "", HTTPStatus.NO_CONTENT
@diagonalley_ext.route("/api/v1/diagonalley/orders/paid/<order_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
async def api_diagonalleys_order_paid(order_id):
with open_ext_db("diagonalley") as db:
db.execute(
"UPDATE orders SET paid = ? WHERE id = ?",
(
True,
order_id,
),
)
return "", HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/<order_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
async def api_diagonalleys_order_shipped(order_id):
with open_ext_db("diagonalley") as db:
db.execute(
"UPDATE orders SET shipped = ? WHERE id = ?",
(
True,
order_id,
),
)
order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), HTTPStatus.OK
###List products based on indexer id
@diagonalley_ext.route("/api/v1/diagonalley/stall/products/<indexer_id>", methods=["GET"])
async def api_diagonalleys_stall_products(indexer_id):
with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
print(rows[1])
if not rows:
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
products = db.fetchone("SELECT * FROM products WHERE wallet = ?", (rows[1],))
if not products:
return jsonify({"message": "No products"}), HTTPStatus.NOT_FOUND
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), HTTPStatus.OK
###Check a product has been shipped
@diagonalley_ext.route("/api/v1/diagonalley/stall/checkshipped/<checking_id>", methods=["GET"])
async def api_diagonalleys_stall_checkshipped(checking_id):
with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
return jsonify({"shipped": rows["shipped"]}), HTTPStatus.OK
###Place order
@diagonalley_ext.route("/api/v1/diagonalley/stall/order/<indexer_id>", methods=["POST"])
@api_validate_post_request(
schema={
"id": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
"address": {"type": "string", "empty": False, "required": True},
"quantity": {"type": "integer", "empty": False, "required": True},
"shippingzone": {"type": "integer", "empty": False, "required": True},
}
)
async def api_diagonalley_stall_order(indexer_id):
product = get_diagonalleys_product(g.data["id"])
shipping = get_diagonalleys_indexer(indexer_id)
if g.data["shippingzone"] == 1:
shippingcost = shipping.zone1cost
else:
shippingcost = shipping.zone2cost
checking_id, payment_request = create_invoice(
wallet_id=product.wallet, amount=shippingcost + (g.data["quantity"] * product.price), memo=g.data["id"]
)
selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
with open_ext_db("diagonalley") as db:
db.execute(
"""
INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
selling_id,
g.data["id"],
product.wallet,
product.product,
g.data["quantity"],
g.data["shippingzone"],
g.data["address"],
g.data["email"],
checking_id,
False,
False,
),
)
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
+3
View File
@@ -0,0 +1,3 @@
<h1>Events</h1>
<h2>Events: Sell and register event tickets</h2>
Events alows you to make a wave of tickets for an event, each ticket is in the form of a unqiue QRcode, which the user presents at registration. Events comes with a shareable ticket scanner, which can be used to register attendees.
+8
View File
@@ -0,0 +1,8 @@
from quart import Blueprint
events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Events",
"short_description": "Sell and register event tickets",
"icon": "local_activity",
"contributors": ["benarc"]
}
+167
View File
@@ -0,0 +1,167 @@
from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash
from .models import Tickets, Events
#######TICKETS########
def create_ticket(payment_hash: str, wallet: str, event: str, name: str, email: str) -> Tickets:
with open_ext_db("events") as db:
db.execute(
"""
INSERT INTO ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(payment_hash, wallet, event, name, email, False, False),
)
return get_ticket(payment_hash)
def update_ticket(paid: bool, payment_hash: str) -> Tickets:
with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
if row[6] == True:
return get_ticket(payment_hash)
db.execute(
"""
UPDATE ticket
SET paid = ?
WHERE id = ?
""",
(paid, payment_hash),
)
eventdata = get_event(row[2])
sold = eventdata.sold + 1
amount_tickets = eventdata.amount_tickets - 1
db.execute(
"""
UPDATE events
SET sold = ?, amount_tickets = ?
WHERE id = ?
""",
(sold, amount_tickets, row[2]),
)
return get_ticket(payment_hash)
def get_ticket(payment_hash: str) -> Optional[Tickets]:
with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
return Tickets(**row) if row else None
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("events") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
print("scrum")
return [Tickets(**row) for row in rows]
def delete_ticket(payment_hash: str) -> None:
with open_ext_db("events") as db:
db.execute("DELETE FROM ticket WHERE id = ?", (payment_hash,))
########EVENTS#########
def create_event(
*,
wallet: str,
name: str,
info: str,
closing_date: str,
event_start_date: str,
event_end_date: str,
amount_tickets: int,
price_per_ticket: int,
) -> Events:
with open_ext_db("events") as db:
event_id = urlsafe_short_hash()
db.execute(
"""
INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
event_id,
wallet,
name,
info,
closing_date,
event_start_date,
event_end_date,
amount_tickets,
price_per_ticket,
0,
),
)
print(event_id)
return get_event(event_id)
def update_event(event_id: str, **kwargs) -> Events:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("events") as db:
db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id))
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
return Events(**row) if row else None
def get_event(event_id: str) -> Optional[Events]:
with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
return Events(**row) if row else None
def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("events") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,))
return [Events(**row) for row in rows]
def delete_event(event_id: str) -> None:
with open_ext_db("events") as db:
db.execute("DELETE FROM events WHERE id = ?", (event_id,))
########EVENTTICKETS#########
def get_event_tickets(event_id: str, wallet_id: str) -> Tickets:
with open_ext_db("events") as db:
rows = db.fetchall("SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id))
print(rows)
return [Tickets(**row) for row in rows]
def reg_ticket(ticket_id: str) -> Tickets:
with open_ext_db("events") as db:
db.execute("UPDATE ticket SET registered = ? WHERE id = ?", (True, ticket_id))
ticket = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
print(ticket[1])
rows = db.fetchall("SELECT * FROM ticket WHERE event = ?", (ticket[1],))
return [Tickets(**row) for row in rows]
+85
View File
@@ -0,0 +1,85 @@
def m001_initial(db):
db.execute(
"""
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
info TEXT NOT NULL,
closing_date TEXT NOT NULL,
event_start_date TEXT NOT NULL,
event_end_date TEXT NOT NULL,
amount_tickets INTEGER NOT NULL,
price_per_ticket INTEGER NOT NULL,
sold INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
db.execute(
"""
CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
event TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
registered BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
def m002_changed(db):
db.execute(
"""
CREATE TABLE IF NOT EXISTS ticket (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
event TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
registered BOOLEAN NOT NULL,
paid BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]:
usescsv = ""
for i in range(row[5]):
if row[7]:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
db.execute(
"""
INSERT INTO ticket (
id,
wallet,
event,
name,
email,
registered,
paid
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
row[0],
row[1],
row[2],
row[3],
row[4],
row[5],
True,
),
)
db.execute("DROP TABLE tickets")
+26
View File
@@ -0,0 +1,26 @@
from typing import NamedTuple
class Events(NamedTuple):
id: str
wallet: str
name: str
info: str
closing_date: str
event_start_date: str
event_end_date: str
amount_tickets: int
price_per_ticket: int
sold: int
time: int
class Tickets(NamedTuple):
id: str
wallet: str
event: str
name: str
email: str
registered: bool
paid: bool
time: int
@@ -0,0 +1,23 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Events: Sell and register ticket waves for an event
</h5>
<p>
Events alows you to make a wave of tickets for an event, each ticket is
in the form of a unqiue QRcode, which the user presents at registration.
Events comes with a shareable ticket scanner, which can be used to
register attendees.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a>
</small>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -0,0 +1,203 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ event_name }}</h3>
<br />
<h5 class="q-my-none">{{ event_info }}</h5>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Your name "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.email"
type="email"
label="Your email "
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
type="submit"
>Submit</q-btn
>
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card-section>
</q-card>
<q-card v-show="ticketLink.show" class="q-pa-lg">
<div class="text-center q-mb-lg">
<a :href="ticketLink.data.link" target="_blank">
<q-btn unelevated size="xl" color="deep-purple"
>Link to your ticket!</q-btn
></a
>
<br /><br />
<p>You'll be redirected in a few moments...</p>
</div>
</q-card>
</div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %}
<script>
console.log('{{ form_costpword }}')
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {
name: '',
email: ''
}
},
ticketLink: {
show: false,
data: {
link: ''
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null
}
}
},
methods: {
resetForm: function (e) {
e.preventDefault()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
dismissMsg()
clearInterval(paymentChecker)
setTimeout(function () {}, 10000)
},
Invoice: function () {
var self = this
axios
.post(
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.receive = {
show: true,
status: 'pending',
paymentReq: self.paymentReq
}
paymentChecker = setInterval(function () {
axios
.get('/events/api/v1/tickets/' + self.paymentCheck)
.then(function (res) {
if (res.data.paid) {
clearInterval(paymentChecker)
dismissMsg()
self.formDialog.data.name = ''
self.formDialog.data.email = ''
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: null
})
self.receive = {
show: false,
status: 'complete',
paymentReq: null
}
self.ticketLink = {
show: true,
data: {
link: '/events/ticket/' + res.data.ticket_id
}
}
setTimeout(function () {
window.location.href =
'/events/ticket/' + res.data.ticket_id
}, 5000)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ event_name }} error</h3>
<br />
<q-icon
name="warning"
class="text-grey"
style="font-size: 20rem"
></q-icon>
<h5 class="q-my-none">{{ event_error }}</h5>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>
@@ -0,0 +1,507 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New Event</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Events</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exporteventsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="events"
row-key="id"
:columns="eventsTable.columns"
:pagination.sync="eventsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="how_to_reg"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/register/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateformDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteEvent(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportticketsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="local_activity"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/ticket/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTicket(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Events extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "events/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendEventData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Name of event "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.info"
type="textarea"
label="Info about the event "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.closing_date"
type="date"
label="Closing for tickets "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.event_start_date"
type="date"
label="Event begins "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.event_end_date"
type="date"
label="Event ends "
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.amount_tickets"
type="number"
label="Amount of tickets "
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.price_per_ticket"
type="number"
label="Price per ticket "
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
type="submit"
>Update Event</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
:disable="formDialog.data.wallet == null || formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || formDialog.data.price_per_ticket == null"
type="submit"
>Create Event</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapEvents = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
events: [],
tickets: [],
eventsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'info', align: 'left', label: 'Info', field: 'info'},
{
name: 'event_start_date',
align: 'left',
label: 'Start date',
field: 'event_start_date'
},
{
name: 'event_end_date',
align: 'left',
label: 'End date',
field: 'event_end_date'
},
{
name: 'closing_date',
align: 'left',
label: 'Ticket close',
field: 'closing_date'
},
{
name: 'price_per_ticket',
align: 'left',
label: 'Price',
field: 'price_per_ticket'
},
{
name: 'amount_tickets',
align: 'left',
label: 'No tickets',
field: 'amount_tickets'
},
{
name: 'sold',
align: 'left',
label: 'Sold',
field: 'sold'
}
],
pagination: {
rowsPerPage: 10
}
},
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'event', align: 'left', label: 'Event', field: 'event'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
getTickets: function () {
var self = this
console.log('obj')
LNbits.api
.request(
'GET',
'/events/api/v1/tickets?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tickets = response.data.map(function (obj) {
console.log(obj)
return mapEvents(obj)
})
})
},
deleteTicket: function (ticketId) {
var self = this
var tickets = _.findWhere(this.tickets, {id: ticketId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this ticket')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/events/api/v1/tickets/' + ticketId,
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
)
.then(function (response) {
self.tickets = _.reject(self.tickets, function (obj) {
return obj.id == ticketId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
getEvents: function () {
var self = this
LNbits.api
.request(
'GET',
'/events/api/v1/events?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.events = response.data.map(function (obj) {
return mapEvents(obj)
})
})
},
sendEventData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
var data = this.formDialog.data
if (data.id) {
this.updateEvent(wallet, data)
} else {
this.createEvent(wallet, data)
}
},
createEvent: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/events/api/v1/events', wallet.inkey, data)
.then(function (response) {
self.events.push(mapEvents(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateformDialog: function (formId) {
var link = _.findWhere(this.events, {id: formId})
console.log(link.id)
this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name
this.formDialog.data.info = link.info
this.formDialog.data.closing_date = link.closing_date
this.formDialog.data.event_start_date = link.event_start_date
this.formDialog.data.event_end_date = link.event_end_date
this.formDialog.data.amount_tickets = link.amount_tickets
this.formDialog.data.price_per_ticket = link.price_per_ticket
this.formDialog.show = true
},
updateEvent: function (wallet, data) {
var self = this
console.log(data)
LNbits.api
.request(
'PUT',
'/events/api/v1/events/' + data.id,
wallet.inkey,
data
)
.then(function (response) {
self.events = _.reject(self.events, function (obj) {
return obj.id == data.id
})
self.events.push(mapEvents(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteEvent: function (eventsId) {
var self = this
var events = _.findWhere(this.events, {id: eventsId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this form link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/events/api/v1/events/' + eventsId,
_.findWhere(self.g.user.wallets, {id: events.wallet}).inkey
)
.then(function (response) {
self.events = _.reject(self.events, function (obj) {
return obj.id == eventsId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exporteventsCSV: function () {
LNbits.utils.exportCSV(this.eventsTable.columns, this.events)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getTickets()
this.getEvents()
}
}
})
</script>
{% endblock %}
@@ -0,0 +1,173 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ event_name }} Registration</h3>
<br />
<br />
<q-btn unelevated color="deep-purple" @click="showCamera" size="xl"
>Scan ticket</q-btn
>
</center>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<q-table
dense
flat
:data="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="local_activity"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/ticket/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="sendCamera.show" position="top">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
var mapEvents = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tickets: [],
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
sendCamera: {
show: false,
camera: 'auto'
}
}
},
methods: {
hoverEmail: function (tmp) {
this.tickets.data.emailtemp = tmp
},
closeCamera: function () {
this.sendCamera.show = false
},
showCamera: function () {
this.sendCamera.show = true
},
decodeQR: function (res) {
this.sendCamera.show = false
var self = this
LNbits.api
.request('GET', '/events/api/v1/register/ticket/' + res)
.then(function (response) {
self.$q.notify({
type: 'positive',
message: 'Registered!'
})
setTimeout(function () {
window.location.reload()
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getEventTickets: function () {
var self = this
console.log('obj')
LNbits.api
.request(
'GET',
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
)
.then(function (response) {
self.tickets = response.data.map(function (obj) {
return mapEvents(obj)
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
},
created: function () {
this.getEventTickets()
}
})
</script>
{% endblock %}
@@ -0,0 +1,45 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ ticket_name }} Ticket</h3>
<br />
<h5 class="q-my-none">
Bookmark, print or screenshot this page,<br />
and present it for registration!
</h5>
<br />
<qrcode
:value="'{{ ticket_id }}'"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
<br />
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
<q-icon left size="3em" name="print"></q-icon> Print</q-btn
>
</center>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
},
methods: {
printWindow: function () {
window.print()
}
}
})
</script>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More