Compare commits

..
481 Commits
Author SHA1 Message Date
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
192 changed files with 61759 additions and 5948 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
+45
View File
@@ -0,0 +1,45 @@
FLASK_APP=lnbits
FLASK_ENV=development
LNBITS_SITE_TITLE=LNbits
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="/your_custom_data_folder"
LNBITS_DISABLED_EXTENSIONS="amilk,events"
LNBITS_FORCE_HTTPS=1
LNBITS_SERVICE_FEE="0.0"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet, CLightningWallet, LnbitsWallet
LNBITS_BACKEND_WALLET_CLASS=LntxbotWallet
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
LNBITS_ENDPOINT=127.0.0.1:5000
LNBITS_INVOICE_MACAROON=LNBITS_INVOICE_MACAROON
LNBITS_ADMIN_MACAROON=LNBITS_ADMIN_MACAROON
LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009
LND_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_ADMIN_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon"
LND_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon"
LND_READ_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/read.macaroon"
LND_REST_ENDPOINT=https://localhost:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_ADMIN_MACAROON="HEXSTRING"
LND_REST_INVOICE_MACAROON="HEXSTRING"
LND_REST_READ_MACAROON="HEXSTRING"
LNPAY_API_ENDPOINT=https://lnpay.co/v1/
LNPAY_API_KEY=LNPAY_API_KEY
LNPAY_ADMIN_KEY=LNPAY_ADMIN_KEY
LNPAY_INVOICE_KEY=LNPAY_INVOICE_KEY
LNPAY_READ_KEY=LNPAY_READ_KEY
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/
LNTXBOT_ADMIN_KEY=LNTXBOT_ADMIN_KEY
LNTXBOT_INVOICE_KEY=LNTXBOT_INVOICE_KEY
OPENNODE_API_ENDPOINT=https://api.opennode.com/
OPENNODE_ADMIN_KEY=OPENNODE_ADMIN_KEY
OPENNODE_INVOICE_KEY=OPENNODE_INVOICE_KEY
+2
View File
@@ -0,0 +1,2 @@
lnbits/static/vendor/** linguist-vendored
lnbits/extensions/**/static/vendor/** linguist-vendored
+13
View File
@@ -0,0 +1,13 @@
name: mypy type checker
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master
with:
path: 'lnbits'
+29
View File
@@ -0,0 +1,29 @@
.DS_Store
._*
__pycache__
*.py[cod]
*$py.class
.mypy_cache
.vscode
*.egg
*.egg-info
.coverage
.pytest_cache
.webassets-cache
htmlcov
test-reports
*.swo
*.swp
*.pyo
*.pyc
*.env
.env
venv
*.sqlite3
.pyre*
__bundle__
+29
View File
@@ -0,0 +1,29 @@
For developers
==============
LNbits uses [Flask](http://flask.pocoo.org/).
Feel free to contribute to the project.
Application dependencies
------------------------
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
$ pipenv shell
$ pipenv install --dev
Running the server
------------------
$ flask run
There is an environment variable called `FLASK_ENV` that has to be set to `development`
if you want to run Flask in debug mode with autoreload
Style guide
-----------
Tab size is 4 spaces. Maximum line length is 120. You should run `black` before commiting any change.
$ black lnbits
[pipenv]: https://docs.pipenv.org/#install-pipenv-today
+8
View File
@@ -0,0 +1,8 @@
FROM python:3.7
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -q -r requirements.txt
COPY . /app
EXPOSE 5000
+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.
+31
View File
@@ -0,0 +1,31 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[requires]
python_version = "3.7"
[packages]
bitstring = "*"
cerberus = "*"
lnd-grpc = "*"
lnurl = "*"
flask = "*"
flask-assets = "*"
flask-compress = "*"
flask-talisman = "*"
gevent = "*"
gunicorn = "*"
pylightning = "*"
pyscss = "*"
requests = "*"
shortuuid = "*"
[dev-packages]
black = "==19.10b0"
flake8 = "*"
flake8-mypy = "*"
pytest = "*"
pytest-cov = "*"
pytest-sugar = "*"
Generated
+733
View File
@@ -0,0 +1,733 @@
{
"_meta": {
"hash": {
"sha256": "1c40bedbf1f2a9e9bccd443b57bd6dda496361df8712ddda5efcec0d00cab9f1"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"bech32": {
"hashes": [
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
],
"version": "==1.2.0"
},
"bitstring": {
"hashes": [
"sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443",
"sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf",
"sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096"
],
"index": "pypi",
"version": "==3.1.6"
},
"cerberus": {
"hashes": [
"sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"
],
"index": "pypi",
"version": "==1.3.2"
},
"certifi": {
"hashes": [
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
],
"version": "==2020.4.5.1"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
"version": "==7.1.1"
},
"flask": {
"hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
],
"index": "pypi",
"version": "==1.1.2"
},
"flask-assets": {
"hashes": [
"sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2",
"sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"
],
"index": "pypi",
"version": "==2.0"
},
"flask-compress": {
"hashes": [
"sha256:468693f4ddd11ac6a41bca4eb5f94b071b763256d54136f77957cfee635badb3"
],
"index": "pypi",
"version": "==1.4.0"
},
"flask-talisman": {
"hashes": [
"sha256:468131464a249274ed226efc21b372518f442487e58918ccab8357eaa638fd1f",
"sha256:eaa754f4b771dfbe473843391d69643b79e3a38c865790011ac5e4179c68e3ec"
],
"index": "pypi",
"version": "==0.7.0"
},
"gevent": {
"hashes": [
"sha256:0b84a8d6f088b29a74402728681c9f11864b95e49f5587a666e6fbf5c683e597",
"sha256:1ef086264e846371beb5742ebaeb148dc96adf72da2ff350ae5603421cdc2ad9",
"sha256:2070c65896f89a85b39f49427d6132f7abd047129fc4da88b3670f0ba13b0cf7",
"sha256:2fbe0bc43d8c5540153f06eece6235dda14e5f99bdd9183838396313100815d7",
"sha256:32813de352918fb652a3db805fd6e08e0a1666a1a9304eef95938c9c426f9573",
"sha256:38c45d8a3b647f56f8a68769a8ac4953be84a84735c7c7a4d7ca62022bd54036",
"sha256:3b4c4d99f87c0d04b825879c5a91fbfa2b66da7c25b8689e9bdd9f4741d5f80d",
"sha256:42cae3be36b7458f411bd589c66aaba27e4e611ec3d3621e37fd732fe383f9b6",
"sha256:4572dc7907a0ac3c39b9f0898dbdf390ae3250baaae5f7395661fb844e2e23be",
"sha256:6088bedd8b6bcdb815be322304a5d1c028ffa837d84e93b349928dadac62f354",
"sha256:8a9aba59a3268f20c7b584119215bdc589cb81500d93dad4dab428eb02f72944",
"sha256:8cca7ffd58559f8d51e5605ad73afcc6f348f9747d2fa539b336e70851b69b79",
"sha256:956e82a5d0e90f8d71efe4cecccde602cfb657cd866c58bb953c9c30ca1b3d77",
"sha256:b0aea12de542f8fcd6882087bdd5b4d7dc8bb316d28181f6b012dd0b91583285",
"sha256:b46399f6c9eccc2e6de1dc1057d362be840443e5439b06cce8b01d114ba1a7ec",
"sha256:c0b38a654c8fde5b9d9bd27ea3261aeefe36bc9244b170b6d3b11d72a2163bdb",
"sha256:c516cc5d70c3faf07f271d50930d144339c69fb80f3cac9b687aa964e518535e",
"sha256:c7a62d51c6dca84f91a91b940037523c926a516f0568f47dc1386bd1682cf4e9",
"sha256:cea28f958bc4206ae092043e0775cd7a2bb2536bcbece292732c6484c1076c01",
"sha256:d56f36eb98532d2bccc51cb0964c31e9fbd9b2282074c297dc9b006b047e2966",
"sha256:de6c0cbcb890d0a79323961d3b593a0f2f54dcb9fe38ee5167f2d514e69e3c8c",
"sha256:e0990009e7c1624f9a0f3335df1ab8d45678241c852659ac645b70ed8229097c",
"sha256:e7d23d5f32c9db6ae49c4b58585618dcafd6ad0babae251c9c8297afebc4744b",
"sha256:ee39caf14d66e619709cdfe3962bc68a234518e43ea8c811c0d67a864bc7c196"
],
"index": "pypi",
"version": "==20.4.0"
},
"googleapis-common-protos": {
"hashes": [
"sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e"
],
"version": "==1.51.0"
},
"greenlet": {
"hashes": [
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
"sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e",
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
"sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122",
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656",
"sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef"
],
"markers": "platform_python_implementation == 'CPython'",
"version": "==0.4.15"
},
"grpcio": {
"hashes": [
"sha256:085bbf7fd0070b8d65e84aa32979f17cfe624d27b5ce23955ef770c19d2d9623",
"sha256:0ae207a47ec0ad66eb1f53a27d566674d13a236c62ced409891335318ea9b8c5",
"sha256:0c130204ff5de0b9f041bf3126db0d29369d69883592e4b0d3c19868ba0ced7e",
"sha256:0ef6b380a588c2c6b29c6cfa0ba7f5d367beb33d5504bcc68658fa241ad498d2",
"sha256:16e1edb367763ea08d0994d4635ec05f4f8db9db59c39304b061097e3b93df43",
"sha256:16f5523dacae5aaeda4cf900da7e980747f663298c38c18eb4e5317704aa007a",
"sha256:181b5078cf568f37915b8a118afcef5fc9f3128c59c38998ed93e7dd793e3928",
"sha256:245564713cb4ac7bccb0f11be63781beb62299a44d8ab69031c859dbd9461728",
"sha256:271abbe28eb99fa5c70b3f272c0c66b67dab7bb11e1d29d8e616b4e0e099d29a",
"sha256:2e1b01cba26988c811c7fb91a0bca19c9afb776cc3d228993f08d324bdd0510a",
"sha256:3366bd6412c1e73acb1ee27d7f0c7d7dbee118ad8d98c957c8173691b2effeec",
"sha256:3893b39a0a17d857dc3a42fdb02a26aa53a59bfce49987187bcc0261647f1f55",
"sha256:3c7864d5ae63b787001b01b376f6315aef1a015aa9c809535235ed0ead907919",
"sha256:42c6716adf3ec1f608b2b56e885f26dd86e80d2fc1617f51fc92d1b0b649e28e",
"sha256:4bef0756b9e0df78e8d67a5b1e0e89b7daf41525d575f74e1f14a993c55b680d",
"sha256:4fe081862e58b8fbef0e479aefc9a64f8f17f53074df1085d8c1fe825a6e5df4",
"sha256:505a8d1b4ac571a51f10c4c995d5d4714f03c886604dc3c097ef5fd57bcfcf0b",
"sha256:5c2e81b6ab9768c43f2ca1c9a4c925823aad79ae95efb351007df4b92ebce592",
"sha256:70ff2df0c1795c5cf585a72d95bb458838b40bad5653c314b9067ba819e918f9",
"sha256:97b5612fc5d4bbf0490a2d80bed5eab5b59112ef1640440c1a9ac824bafa6968",
"sha256:a35f8f4a0334ed8b05db90383aecef8e49923ab430689a4360a74052f3a89cf4",
"sha256:aafe85a8210dfa1da3c46831b7f00c3735240b7b028eeba339eaea6ffdb593fb",
"sha256:c2e53eb253840f05278a8410628419ba7060815f86d48c9d83b6047de21c9956",
"sha256:c3645887db3309fc87c3db740b977d403fb265ebab292f1f6a926c4661231fd5",
"sha256:c6565cc92853af13237b2233f331efdad07339d27fe1f5f74256bfde7dc2f587",
"sha256:cbc322c5d5615e67c2a15be631f64e6c2bab8c12505bc7c150948abdaa0bdbac",
"sha256:df749ee982ec35ab76d37a1e637b10a92b4573e2b4e1f86a5fa8a1273c40a850",
"sha256:e9439d7b801c86df13c6cbb4c5a7e181c058f3c119d5e119a94a5f3090a8f060",
"sha256:f493ac4754717f25ace3614a51dd408a32b8bff3c9c0c85e9356e7e0a120a8c8",
"sha256:f80d10bdf1a306f7063046321fd4efc7732a606acdd4e6259b8a37349079b704",
"sha256:f83b0c91796eb42865451a20e82246011078ba067ea0744f7301e12a94ae2e1b"
],
"version": "==1.28.1"
},
"grpcio-tools": {
"hashes": [
"sha256:095911bd21dd2dc029b8862b44e65b41341135fd16da60ad9d0df4b8e9a0618e",
"sha256:17a6b63ee1650abbbc9306f3144d1d3bd3d088a270ca6569afcb68a8cc4a4a38",
"sha256:27fcab84ea948d965f82c7994903e8e36a20ce4ad7ef55a22c8d7c639e6c8808",
"sha256:2ffa5f6fbd26ea6c7626bf89d6ef4d64d111000a8bd4e9df69b070093de0f85d",
"sha256:491dcd98d988c0a4ed9bbb1ec6310588fc0e16b334b64119b973363a1cf96cc1",
"sha256:493b5a275ff2692942951926a259e396ff13acec72e6d93498638006b1a1bd71",
"sha256:4c980208c1620e52e86c4037058097673ee8e3bbbec34582f2538e614d0c41f4",
"sha256:55f3a86c3a808b8a5c2dc8112c45329dd912e43a6fb474da94a7e04d8bb36e1a",
"sha256:69a60e1c020547668f313b7a81d653f8c820066ab0f6ed979af760905b61d9e1",
"sha256:6f7806fe53e5979fa10ec49cbd5236cf0e57c8817ce54350bccc8ebceaeb54fb",
"sha256:8f752fa03eb2d851aac5838aa46a5b831660e9800634fad8ccd46539e9cb4663",
"sha256:9210b3575fee355a4056cff8ff7212b823371e9fa9ad1467e3c0c5cb006d0d7d",
"sha256:9b900cf90b293acd48bf258d5e416eae9540b5aa0c75cc2fe68a7e2c5f4834ae",
"sha256:addc40a47f496cafa108ba1a31838a9832223e0dcafaf424d38e6b8c79fe26ac",
"sha256:adf089aaf6e21358b12e39d9fa7c28611340d8399a918c0b72ff122ce9b7e0af",
"sha256:b22172e3ad5e849815f3e35a002fb67c52e83cd3e4299cf45760de4674c7cd04",
"sha256:b5cf457dcd6499f8c2f2bdcc486d681ca3ee673064484a31c507df6139bf00fe",
"sha256:b5ee6f089bb040d175e658911760584c7a6cb70fbdcc78bdbe6349475933483b",
"sha256:bb9b1a05917011e98e5331d129a40a41f49c5652e1b86d5fea9b3f6c98454928",
"sha256:c527db050fa619a2bd293a73db0e0af42f0bd5a4f41c043fc5da543d81f5fed6",
"sha256:c9efe6420d2ffaf9aa32d292817ddfc939b9adf28a51cef30984bab02ed35a58",
"sha256:cb7b54a96f4d9aae5d9c6b644a69ce48c12cfa3558c86ce0a62d9c8a08a83f2e",
"sha256:d661e5c4f82f9a223b9d1a6c9ec68faea060324e0d25684a6f7fb0b9622ab561",
"sha256:d6ee6f338e0099976161e78ecf91d2ab9c04be75a944ef29facc93a4aad28e1d",
"sha256:d89dec894495c51ee0a8dd0c896e4320e95bab179c3faa6a439ef5719486c35e",
"sha256:dc7ea3739aa6f5abe6e9a27f403b30c7996978176e5884c41ff3d8a153e7c35a",
"sha256:de10f8b28ef3e8a7f336e02b7c2912713bd41e0931c4fad3a6993d25c8511159",
"sha256:e05f7bfb63b844b7f9d8b50019b02168fd8982278dc0443986f3504b3cb87d2a",
"sha256:e942dd349d5be0d5ebbe96ed9cbcfb5fdb0bc7b57357754c42db8083a0ec80aa",
"sha256:edc01f022a878722ae69bff3faa8c6ec7e967d57b7e8a5f4b2f6100ad903a978",
"sha256:f61d219d06057e015d662c0b40438f119f58e514a5bebf425058a34d05ff21db"
],
"version": "==1.28.1"
},
"gunicorn": {
"hashes": [
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
],
"index": "pypi",
"version": "==20.0.4"
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
],
"version": "==2.9"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"version": "==2.11.2"
},
"lnd-grpc": {
"hashes": [
"sha256:43114aead2dc2d008751231784fc94027d60569a5e4dc86ed68032a2203fe781",
"sha256:c28208d912b28f5b0b11fa87559c6e564731549ecbc52eafe7d5dd2b2f469cf8"
],
"index": "pypi",
"version": "==0.4.0"
},
"lnurl": {
"hashes": [
"sha256:633ddedddcff4bd8b7790eba1649ec10c91fb28e690e3f6e984cb8591be3b626",
"sha256:749e0d73a8e5d41b03c734df4f22c0e022f1da4f64e600a898b56e1bb75f5dda"
],
"index": "pypi",
"version": "==0.3.3"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
"protobuf": {
"hashes": [
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
],
"version": "==3.11.3"
},
"pydantic": {
"hashes": [
"sha256:0b7aadfa1de28057656064e04d9f018d1b186fe2a8e953a2fb41545873b7cf95",
"sha256:0f61e67291b99a927816558a218a4e794db72a33621c836e63d12613a2202cd4",
"sha256:20946280c750753b3e3177c748825ef189d7ab86c514f6a0b118621110d5f0d3",
"sha256:22139ee446992c222977ac0a9269c4da2e9ecc1834f84804ebde008a4649b929",
"sha256:3c0f39e884d7a3572d5cc8322b0fe9bf66114283e22e05a5c4b8961c19588945",
"sha256:446ce773a552a2cb90065d4aa645e16fa7494369b5f0d199e4d41a992a98204d",
"sha256:475e6606873e40717cc3b0eebc7d1101cbfc774e01dadeeea24c121eb5826b86",
"sha256:66124752662de0479a9d0c17bdebdc8a889bccad8846626fb66d8669e8eafb63",
"sha256:896637b7d8e4cdc0bcee1704fcadacdd167c35ac29f02a4395fce7a033925f26",
"sha256:9af44d06db33896a2176603c9cb876df3a60297a292a24d3018956a910cc1402",
"sha256:9e46fac8a4674db0777fd0133aa56817e1481beee50971bab39dded7639f9b2b",
"sha256:ae206e103e976c40ec294cd6c8fcbfbdaced3ab9b736bc53d03fa11b5aaa1628",
"sha256:b11d0bd7ecf41098894e8777ee623c29554dbaa37e862c51bcc5a2b950d1bf77",
"sha256:d73070028f7b046a5b2e611a9799c238d7bd245f8fe30f4ad7ff29ddb63aac40",
"sha256:ddedcdf9d5c24939578449a8e099ceeec3b3d76243fc143aff63ebf6d5aade10",
"sha256:e08e21f4d5395ac17cde19de26be63fb16fb870f0cfde1481ddc22d5e2353548",
"sha256:e6239199b363bc53262bcb57f1441206d4b2d46b392eccba2213d8358d6e284a"
],
"version": "==1.5"
},
"pylightning": {
"hashes": [
"sha256:5b7df2c6f0faf17450ea3024f29f901a114d4c68da116a1b130958df510a0557",
"sha256:6a07b62b27e01baaf51165e2dfb8b7d1d47f61aef8304a9da3ca5081608a32c0"
],
"index": "pypi",
"version": "==0.0.7.3"
},
"pyscss": {
"hashes": [
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
],
"index": "pypi",
"version": "==1.3.7"
},
"requests": {
"hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"index": "pypi",
"version": "==2.23.0"
},
"shortuuid": {
"hashes": [
"sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f",
"sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"
],
"index": "pypi",
"version": "==1.0.1"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.14.0"
},
"typing-extensions": {
"hashes": [
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
],
"markers": "python_version < '3.8'",
"version": "==3.7.4.2"
},
"urllib3": {
"hashes": [
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
],
"version": "==1.25.9"
},
"webassets": {
"hashes": [
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
],
"version": "==2.0"
},
"werkzeug": {
"hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
],
"version": "==1.0.1"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
],
"index": "pypi",
"version": "==19.10b0"
},
"click": {
"hashes": [
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
"version": "==7.1.1"
},
"coverage": {
"hashes": [
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
],
"version": "==5.1"
},
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.3"
},
"flake8": {
"hashes": [
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
],
"index": "pypi",
"version": "==3.7.9"
},
"flake8-mypy": {
"hashes": [
"sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18",
"sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc"
],
"index": "pypi",
"version": "==17.8.0"
},
"importlib-metadata": {
"hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
],
"markers": "python_version < '3.8'",
"version": "==1.6.0"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": {
"hashes": [
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
],
"version": "==8.2.0"
},
"mypy": {
"hashes": [
"sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2",
"sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1",
"sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164",
"sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761",
"sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce",
"sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27",
"sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754",
"sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae",
"sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9",
"sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600",
"sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65",
"sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8",
"sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913",
"sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"
],
"version": "==0.770"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"packaging": {
"hashes": [
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
],
"version": "==20.3"
},
"pathspec": {
"hashes": [
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
"sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
],
"version": "==0.8.0"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"version": "==0.13.1"
},
"py": {
"hashes": [
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
],
"version": "==1.8.1"
},
"pycodestyle": {
"hashes": [
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"version": "==2.5.0"
},
"pyflakes": {
"hashes": [
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
"version": "==2.1.1"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"version": "==2.4.7"
},
"pytest": {
"hashes": [
"sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
"sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
],
"index": "pypi",
"version": "==5.4.1"
},
"pytest-cov": {
"hashes": [
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
],
"index": "pypi",
"version": "==2.8.1"
},
"pytest-sugar": {
"hashes": [
"sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283",
"sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab"
],
"index": "pypi",
"version": "==0.9.2"
},
"regex": {
"hashes": [
"sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b",
"sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8",
"sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3",
"sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e",
"sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683",
"sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1",
"sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142",
"sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3",
"sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468",
"sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e",
"sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3",
"sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a",
"sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f",
"sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6",
"sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156",
"sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b",
"sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db",
"sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd",
"sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a",
"sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948",
"sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"
],
"version": "==2020.4.4"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.14.0"
},
"termcolor": {
"hashes": [
"sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"
],
"version": "==1.1.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"typed-ast": {
"hashes": [
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
],
"version": "==1.4.1"
},
"typing-extensions": {
"hashes": [
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
],
"markers": "python_version < '3.8'",
"version": "==3.7.4.2"
},
"wcwidth": {
"hashes": [
"sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
"sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
],
"version": "==0.1.9"
},
"zipp": {
"hashes": [
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
],
"version": "==3.1.0"
}
}
}
+1
View File
@@ -0,0 +1 @@
web: gunicorn -b :5000 lnbits:app -k gevent
+70 -6
View File
@@ -1,11 +1,75 @@
# Cashu
## Create ecash mint for pegging in/out of ecash
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png)
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
https://lnbits.com, or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
The wallet can run on top of any lightning-network funding source, currently there is support for LND, CLightning, Lntxbot, LNpay, OpenNode, with more being added regularily.
## 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)
### Usage
Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
1. Enable extension
2. Create a Mint
3. Share wallet
(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.
![lnurl fallback](https://i.imgur.com/CPBKHIv.png)
https://github.com/btcontract/lnurl-rfc/blob/master/spec.md
Adding **/lnurlwallet?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds.
![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)
# Running LNbits locally
Download this repo
LNbits uses [Flask](http://flask.pocoo.org/).
Feel free to contribute to the project.
Application dependencies
------------------------
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
$ pipenv shell
$ pipenv install --dev
You will need to set the variables in .env.example, and rename the file to .env
![lnurl ATM](https://i.imgur.com/ri2zOe8.png)
Running the server
------------------
$ flask migrate
$ flask run
There is an environment variable called `FLASK_ENV` that has to be set to `development`
if you want to run Flask in debug mode with autoreload
[pipenv]: https://docs.pipenv.org/#install-pipenv-today
# Tip me
If you like this project and might even use or extend it, why not send some tip love!
https://paywall.link/to/f4e4e
-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": "flask migrate"
}
}
}
-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,))
View File
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'"
}
}
+94
View File
@@ -0,0 +1,94 @@
import importlib
from flask import Flask
from flask_assets import Environment, Bundle # type: ignore
from flask_compress import Compress # type: ignore
from flask_talisman import Talisman # type: ignore
from os import getenv
from werkzeug.middleware.proxy_fix import ProxyFix
from .core import core_app, migrations as core_migrations
from .helpers import ExtensionManager
from .settings import FORCE_HTTPS
disabled_extensions = getenv("LNBITS_DISABLED_EXTENSIONS", "").split(",")
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) # type: ignore
valid_extensions = [ext for ext in ExtensionManager(disabled=disabled_extensions).extensions if ext.is_valid]
# optimization & security
# -----------------------
Compress(app)
Talisman(
app,
force_https=FORCE_HTTPS,
content_security_policy={
"default-src": [
"'self'",
"'unsafe-eval'",
"'unsafe-inline'",
"blob:",
"api.opennode.co",
"fonts.googleapis.com",
"fonts.gstatic.com",
"github.com",
"avatars2.githubusercontent.com",
]
},
)
# blueprints / extensions
# -----------------------
app.register_blueprint(core_app)
for ext in valid_extensions:
try:
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
app.register_blueprint(getattr(ext_module, f"{ext.code}_ext"), url_prefix=f"/{ext.code}")
except Exception:
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.")
# filters
# -------
app.jinja_env.globals["DEBUG"] = app.config["DEBUG"]
app.jinja_env.globals["EXTENSIONS"] = valid_extensions
app.jinja_env.globals["SITE_TITLE"] = getenv("LNBITS_SITE_TITLE", "LNbits")
# assets
# ------
assets = Environment(app)
assets.url = app.static_url_path
assets.register("base_css", Bundle("scss/base.scss", filters="pyscss", output="css/base.css"))
# commands
# --------
@app.cli.command("migrate")
def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
core_migrations.migrate()
for ext in valid_extensions:
try:
ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations")
ext_migrations.migrate()
except Exception:
raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.")
# init
# ----
if __name__ == "__main__":
app.run()
+7
View File
@@ -0,0 +1,7 @@
{
"scripts": {
"dokku": {
"predeploy": "flask migrate"
}
}
}
+109
View File
@@ -0,0 +1,109 @@
# type: ignore
import bitstring
import re
from binascii import hexlify
from bech32 import bech32_decode, CHARSET
class Invoice(object):
def __init__(self):
self.payment_hash: str = None
self.amount_msat: int = 0
self.description: str = None
def decode(pr: str) -> Invoice:
""" Super naïve bolt11 decoder,
only gets payment_hash, description/description_hash and amount in msatoshi.
based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py
"""
hrp, data = bech32_decode(pr)
if not hrp:
raise ValueError("Bad bech32 checksum")
if not hrp.startswith("ln"):
raise ValueError("Does not start with ln")
data = u5_to_bitarray(data)
# Final signature 65 bytes, split it off.
if len(data) < 65 * 8:
raise ValueError("Too short to contain signature")
data = bitstring.ConstBitStream(data[: -65 * 8])
invoice = Invoice()
m = re.search("[^\d]+", hrp[2:])
if m:
amountstr = hrp[2 + m.end() :]
if amountstr != "":
invoice.amount_msat = unshorten_amount(amountstr)
# pull out date
data.read(35).uint
while data.pos != data.len:
tag, tagdata, data = pull_tagged(data)
data_length = len(tagdata) / 5
if tag == "d":
invoice.description = trim_to_bytes(tagdata).decode("utf-8")
elif tag == "h" and data_length == 52:
invoice.description = hexlify(trim_to_bytes(tagdata)).decode("ascii")
elif tag == "p" and data_length == 52:
invoice.payment_hash = hexlify(trim_to_bytes(tagdata)).decode("ascii")
return invoice
def unshorten_amount(amount: str) -> int:
""" Given a shortened amount, return millisatoshis
"""
# BOLT #11:
# The following `multiplier` letters are defined:
#
# * `m` (milli): multiply by 0.001
# * `u` (micro): multiply by 0.000001
# * `n` (nano): multiply by 0.000000001
# * `p` (pico): multiply by 0.000000000001
units = {
"p": 10 ** 12,
"n": 10 ** 9,
"u": 10 ** 6,
"m": 10 ** 3,
}
unit = str(amount)[-1]
# BOLT #11:
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
# anything except a `multiplier` in the table above.
if not re.fullmatch("\d+[pnum]?", str(amount)):
raise ValueError("Invalid amount '{}'".format(amount))
if unit in units:
return int(amount[:-1]) * 100_000_000_000 / units[unit]
else:
return int(amount) * 100_000_000_000
def pull_tagged(stream):
tag = stream.read(5).uint
length = stream.read(5).uint * 32 + stream.read(5).uint
return (CHARSET[tag], stream.read(length * 5), stream)
def trim_to_bytes(barr):
# Adds a byte if necessary.
b = barr.tobytes()
if barr.len % 8 != 0:
return b[:-1]
return b
def u5_to_bitarray(arr):
ret = bitstring.BitArray()
for a in arr:
ret += bitstring.pack("uint:5", a)
return ret
+9
View File
@@ -0,0 +1,9 @@
from flask import Blueprint
core_app: Blueprint = Blueprint("core", __name__, template_folder="templates", static_folder="static")
from .views.api import * # noqa
from .views.generic import * # noqa
from .views.lnurl import * # noqa
+208
View File
@@ -0,0 +1,208 @@
from typing import List, Optional
from uuid import uuid4
from lnbits.db import open_db
from lnbits.settings import DEFAULT_WALLET_NAME
from .models import User, Wallet, Payment
# accounts
# --------
def create_account() -> User:
with open_db() as db:
user_id = uuid4().hex
db.execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
new_account = get_account(user_id=user_id)
assert new_account, "Newly created account couldn't be retrieved"
return new_account
def get_account(user_id: str) -> Optional[User]:
with open_db() as db:
row = db.fetchone("SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,))
return User(**row) if row else None
def get_user(user_id: str) -> Optional[User]:
with open_db() as db:
user = db.fetchone("SELECT id, email FROM accounts WHERE id = ?", (user_id,))
if user:
extensions = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,))
wallets = db.fetchall(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE user = ?
""",
(user_id,),
)
return (
User(**{**user, **{"extensions": [e[0] for e in extensions], "wallets": [Wallet(**w) for w in wallets]}})
if user
else None
)
def update_user_extension(*, user_id: str, extension: str, active: int) -> None:
with open_db() as db:
db.execute(
"""
INSERT OR REPLACE INTO extensions (user, extension, active)
VALUES (?, ?, ?)
""",
(user_id, extension, active),
)
# wallets
# -------
def create_wallet(*, user_id: str, wallet_name: Optional[str] = None) -> Wallet:
with open_db() as db:
wallet_id = uuid4().hex
db.execute(
"""
INSERT INTO wallets (id, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
""",
(wallet_id, wallet_name or DEFAULT_WALLET_NAME, user_id, uuid4().hex, uuid4().hex),
)
new_wallet = get_wallet(wallet_id=wallet_id)
assert new_wallet, "Newly created wallet couldn't be retrieved"
return new_wallet
def delete_wallet(*, user_id: str, wallet_id: str) -> None:
with open_db() as db:
db.execute(
"""
UPDATE wallets AS w
SET
user = 'del:' || w.user,
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
WHERE id = ? AND user = ?
""",
(wallet_id, user_id),
)
def get_wallet(wallet_id: str) -> Optional[Wallet]:
with open_db() as db:
row = db.fetchone(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE id = ?
""",
(wallet_id,),
)
return Wallet(**row) if row else None
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
with open_db() as db:
check_field = "adminkey" if key_type == "admin" else "inkey"
row = db.fetchone(
f"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE {check_field} = ?
""",
(key,),
)
return Wallet(**row) if row else None
# wallet payments
# ---------------
def get_wallet_payment(wallet_id: str, checking_id: str) -> Optional[Payment]:
with open_db() as db:
row = db.fetchone(
"""
SELECT payhash as checking_id, amount, fee, pending, memo, time
FROM apipayments
WHERE wallet = ? AND payhash = ?
""",
(wallet_id, checking_id),
)
return Payment(**row) if row else None
def get_wallet_payments(wallet_id: str, *, include_all_pending: bool = False) -> List[Payment]:
with open_db() as db:
if include_all_pending:
clause = "pending = 1"
else:
clause = "((amount > 0 AND pending = 0) OR amount < 0)"
rows = db.fetchall(
f"""
SELECT payhash as checking_id, amount, fee, pending, memo, time
FROM apipayments
WHERE wallet = ? AND {clause}
ORDER BY time DESC
""",
(wallet_id,),
)
return [Payment(**row) for row in rows]
def delete_wallet_payments_expired(wallet_id: str, *, seconds: int = 86400) -> None:
with open_db() as db:
db.execute(
"""
DELETE
FROM apipayments WHERE wallet = ? AND pending = 1 AND time < strftime('%s', 'now') - ?
""",
(wallet_id, seconds),
)
# payments
# --------
def create_payment(
*, wallet_id: str, checking_id: str, amount: int, memo: str, fee: int = 0, pending: bool = True
) -> Payment:
with open_db() as db:
db.execute(
"""
INSERT INTO apipayments (wallet, payhash, amount, pending, memo, fee)
VALUES (?, ?, ?, ?, ?, ?)
""",
(wallet_id, checking_id, amount, int(pending), memo, fee),
)
new_payment = get_wallet_payment(wallet_id, checking_id)
assert new_payment, "Newly created payment couldn't be retrieved"
return new_payment
def update_payment_status(checking_id: str, pending: bool) -> None:
with open_db() as db:
db.execute("UPDATE apipayments SET pending = ? WHERE payhash = ?", (int(pending), checking_id,))
def delete_payment(checking_id: str) -> None:
with open_db() as db:
db.execute("DELETE FROM apipayments WHERE payhash = ?", (checking_id,))
+75
View File
@@ -0,0 +1,75 @@
from lnbits.db import open_db
def m001_initial(db):
"""
Initial LNbits tables.
"""
db.execute(
"""
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY,
email TEXT,
pass TEXT
);
"""
)
db.execute(
"""
CREATE TABLE IF NOT EXISTS extensions (
user TEXT NOT NULL,
extension TEXT NOT NULL,
active BOOLEAN DEFAULT 0,
UNIQUE (user, extension)
);
"""
)
db.execute(
"""
CREATE TABLE IF NOT EXISTS wallets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
user TEXT NOT NULL,
adminkey TEXT NOT NULL,
inkey TEXT
);
"""
)
db.execute(
"""
CREATE TABLE IF NOT EXISTS apipayments (
payhash TEXT NOT NULL,
amount INTEGER NOT NULL,
fee INTEGER NOT NULL DEFAULT 0,
wallet TEXT NOT NULL,
pending BOOLEAN NOT NULL,
memo TEXT,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE (wallet, payhash)
);
"""
)
db.execute(
"""
CREATE VIEW IF NOT EXISTS balances AS
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
SELECT wallet, SUM(amount) AS s -- incoming
FROM apipayments
WHERE amount > 0 AND pending = 0 -- don't sum pending
GROUP BY wallet
UNION ALL
SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees
FROM apipayments
WHERE amount < 0 -- do sum pending
GROUP BY wallet
)
GROUP BY wallet;
"""
)
def migrate():
with open_db() as db:
m001_initial(db)
+80
View File
@@ -0,0 +1,80 @@
from typing import List, NamedTuple, Optional
class User(NamedTuple):
id: str
email: str
extensions: List[str] = []
wallets: List["Wallet"] = []
password: Optional[str] = None
@property
def wallet_ids(self) -> List[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
class Wallet(NamedTuple):
id: str
name: str
user: str
adminkey: str
inkey: str
balance_msat: int
@property
def balance(self) -> int:
return self.balance // 1000
def get_payment(self, checking_id: str) -> Optional["Payment"]:
from .crud import get_wallet_payment
return get_wallet_payment(self.id, checking_id)
def get_payments(self, *, include_all_pending: bool = False) -> List["Payment"]:
from .crud import get_wallet_payments
return get_wallet_payments(self.id, include_all_pending=include_all_pending)
def delete_expired_payments(self, seconds: int = 86400) -> None:
from .crud import delete_wallet_payments_expired
delete_wallet_payments_expired(self.id, seconds=seconds)
class Payment(NamedTuple):
checking_id: str
pending: bool
amount: int
fee: int
memo: str
time: int
@property
def msat(self) -> int:
return self.amount
@property
def sat(self) -> int:
return self.amount // 1000
@property
def is_in(self) -> bool:
return self.amount > 0
@property
def is_out(self) -> bool:
return self.amount < 0
def set_pending(self, pending: bool) -> None:
from .crud import update_payment_status
update_payment_status(self.checking_id, pending)
def delete(self) -> None:
from .crud import delete_payment
delete_payment(self.checking_id)
+73
View File
@@ -0,0 +1,73 @@
from typing import Optional, Tuple
from lnbits.bolt11 import decode as bolt11_decode # type: ignore
from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import WALLET
from .crud import get_wallet, create_payment, delete_payment
def create_invoice(*, wallet_id: str, amount: int, memo: str) -> Tuple[str, str]:
try:
ok, checking_id, payment_request, error_message = WALLET.create_invoice(amount=amount, memo=memo)
except Exception as e:
ok, error_message = False, str(e)
if not ok:
raise Exception(error_message or "Unexpected backend error.")
amount_msat = amount * 1000
create_payment(wallet_id=wallet_id, checking_id=checking_id, amount=amount_msat, memo=memo)
return checking_id, payment_request
def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) -> str:
temp_id = f"temp_{urlsafe_short_hash()}"
try:
invoice = bolt11_decode(bolt11)
if invoice.amount_msat == 0:
raise ValueError("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.")
fee_reserve = max(1000, int(invoice.amount_msat * 0.01))
create_payment(
wallet_id=wallet_id,
checking_id=temp_id,
amount=-invoice.amount_msat,
fee=-fee_reserve,
memo=temp_id,
)
wallet = get_wallet(wallet_id)
assert wallet, "invalid wallet id"
if wallet.balance_msat < 0:
raise PermissionError("Insufficient balance.")
ok, checking_id, fee_msat, error_message = WALLET.pay_invoice(bolt11)
if ok:
create_payment(
wallet_id=wallet_id,
checking_id=checking_id,
amount=-invoice.amount_msat,
fee=fee_msat,
memo=invoice.description,
)
except Exception as e:
ok, error_message = False, str(e)
delete_payment(temp_id)
if not ok:
raise Exception(error_message or "Unexpected backend error.")
return checking_id
def check_payment(*, checking_id: str) -> str:
pass
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
});
}
}
});
+357
View File
@@ -0,0 +1,357 @@
Vue.component(VueQrcode.name, VueQrcode);
Vue.use(VueQrcodeReader);
function generateChart(canvas, payments) {
var txs = [];
var n = 0;
var data = {
labels: [],
income: [],
outcome: [],
cumulative: []
};
_.each(payments.slice(0).sort(function (a, b) {
return a.time - b.time;
}), function (tx) {
txs.push({
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
sat: tx.sat,
});
});
_.each(_.groupBy(txs, 'hour'), function (value, day) {
var income = _.reduce(value, function(memo, tx) {
return (tx.sat >= 0) ? memo + tx.sat : memo;
}, 0);
var outcome = _.reduce(value, function(memo, tx) {
return (tx.sat < 0) ? memo + Math.abs(tx.sat) : memo;
}, 0);
n = n + income - outcome;
data.labels.push(day);
data.income.push(income);
data.outcome.push(outcome);
data.cumulative.push(n);
});
new Chart(canvas.getContext('2d'), {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{
data: data.cumulative,
type: 'line',
label: 'balance',
backgroundColor: '#673ab7', // deep-purple
borderColor: '#673ab7',
borderWidth: 4,
pointRadius: 3,
fill: false
},
{
data: data.income,
type: 'bar',
label: 'in',
barPercentage: 0.75,
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
},
{
data: data.outcome,
type: 'bar',
label: 'out',
barPercentage: 0.75,
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
}
]
},
options: {
title: {
text: 'Chart.js Combo Time Scale'
},
tooltips: {
mode: 'index',
intersect:false
},
scales: {
xAxes: [{
type: 'time',
display: true,
offset: true,
time: {
minUnit: 'hour',
stepSize: 3
}
}],
},
// performance tweaks
animation: {
duration: 0
},
elements: {
line: {
tension: 0
}
}
}
});
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
receive: {
show: false,
status: 'pending',
paymentReq: null,
data: {
amount: null,
memo: ''
}
},
send: {
show: false,
invoice: null,
data: {
bolt11: ''
}
},
sendCamera: {
show: false,
camera: 'auto'
},
payments: [],
paymentsTable: {
columns: [
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
{name: 'sat', align: 'right', label: 'Amount (sat)', field: 'sat', sortable: true}
],
pagination: {
rowsPerPage: 10
},
filter: null
},
paymentsChart: {
show: false
},
disclaimerDialog: {
show: false,
location: window.location
}
};
},
computed: {
filteredPayments: function () {
var q = this.paymentsTable.filter;
if (!q || q == '') return this.payments;
return LNbits.utils.search(this.payments, q);
},
balance: function () {
if (this.payments.length) {
return _.pluck(this.payments, 'amount').reduce(function (a, b) { return a + b; }, 0) / 1000;
}
return this.g.wallet.sat;
},
fbalance: function () {
return LNbits.utils.formatSat(this.balance)
},
canPay: function () {
if (!this.send.invoice) return false;
return this.send.invoice.sat <= this.balance;
},
pendingPaymentsExist: function () {
return (this.payments)
? _.where(this.payments, {pending: 1}).length > 0
: false;
}
},
methods: {
closeCamera: function () {
this.sendCamera.show = false;
},
showCamera: function () {
this.sendCamera.show = true;
},
showChart: function () {
this.paymentsChart.show = true;
this.$nextTick(function () {
generateChart(this.$refs.canvas, this.payments);
});
},
showReceiveDialog: function () {
this.receive = {
show: true,
status: 'pending',
paymentReq: null,
data: {
amount: null,
memo: ''
},
paymentChecker: null
};
},
showSendDialog: function () {
this.send = {
show: true,
invoice: null,
data: {
bolt11: ''
},
paymentChecker: null
};
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker;
setTimeout(function () {
clearInterval(checker);
}, 10000);
},
closeSendDialog: function () {
this.sendCamera.show = false;
var checker = this.send.paymentChecker;
setTimeout(function () {
clearInterval(checker);
}, 1000);
},
createInvoice: function () {
var self = this;
this.receive.status = 'loading';
LNbits.api.createInvoice(this.g.wallet, this.receive.data.amount, this.receive.data.memo)
.then(function (response) {
self.receive.status = 'success';
self.receive.paymentReq = response.data.payment_request;
self.receive.paymentChecker = setInterval(function () {
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (response) {
if (response.data.paid) {
self.fetchPayments();
self.receive.show = false;
clearInterval(self.receive.paymentChecker);
}
});
}, 2000);
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
self.receive.status = 'pending';
});
},
decodeQR: function (res) {
this.send.data.bolt11 = res;
this.decodeInvoice();
this.sendCamera.show = false;
},
decodeInvoice: function () {
try {
var invoice = decode(this.send.data.bolt11);
} catch (error) {
this.$q.notify({
timeout: 3000,
type: 'warning',
message: error + '.',
caption: '400 BAD REQUEST',
icon: null
});
return;
}
var cleanInvoice = {
msat: invoice.human_readable_part.amount,
sat: invoice.human_readable_part.amount / 1000,
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
};
_.each(invoice.data.tags, function (tag) {
if (_.isObject(tag) && _.has(tag, 'description')) {
if (tag.description == 'payment_hash') { cleanInvoice.hash = tag.value; }
else if (tag.description == 'description') { cleanInvoice.description = tag.value; }
else if (tag.description == 'expiry') {
var expireDate = new Date((invoice.data.time_stamp + tag.value) * 1000);
cleanInvoice.expireDate = Quasar.utils.date.formatDate(expireDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ');
cleanInvoice.expired = false; // TODO
}
}
});
this.send.invoice = Object.freeze(cleanInvoice);
},
payInvoice: function () {
var self = this;
dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...',
icon: null
});
LNbits.api.payInvoice(this.g.wallet, this.send.data.bolt11).then(function (response) {
self.send.paymentChecker = setInterval(function () {
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (res) {
if (res.data.paid) {
self.send.show = false;
clearInterval(self.send.paymentChecker);
dismissPaymentMsg();
self.fetchPayments();
}
});
}, 2000);
}).catch(function (error) {
dismissPaymentMsg();
LNbits.utils.notifyApiError(error);
});
},
deleteWallet: function (walletId, user) {
LNbits.utils.confirmDialog(
'Are you sure you want to delete this wallet?'
).onOk(function () {
LNbits.href.deleteWallet(walletId, user);
});
},
fetchPayments: function (checkPending) {
var self = this;
return LNbits.api.getPayments(this.g.wallet, checkPending).then(function (response) {
self.payments = response.data.map(function (obj) {
return LNbits.map.payment(obj);
}).sort(function (a, b) {
return b.time - a.time;
});
});
},
checkPendingPayments: function () {
var dismissMsg = this.$q.notify({
timeout: 0,
message: 'Checking pending transactions...',
icon: null
});
this.fetchPayments(true).then(function () {
dismissMsg();
});
},
exportCSV: function () {
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments);
}
},
watch: {
'payments': function () {
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance]);
}
},
created: function () {
this.fetchPayments();
setTimeout(this.checkPendingPayments(), 1200);
},
mounted: function () {
if (this.$refs.disclaimer && !this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
this.disclaimerDialog.show = true;
this.$q.localStorage.set('lnbits.disclaimerShown', true);
}
}
});
+51
View File
@@ -0,0 +1,51 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="Create an invoice (incoming)">
<q-card>
<q-card-section>
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"out": false, "amount": &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>{"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 }}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>{"checking_id": &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: {{ 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;checking_id&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;checking_id&gt; -H "X-Api-Key: {{ wallet.inkey }}"</i> -H "Content-type: application/json"</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %}
{% block scripts %}
{{ window_vars(user) }}
{% assets filters='rjsmin', output='__bundle__/core/extensions.js',
'core/js/extensions.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% endblock %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-6 col-md-4 col-lg-3" v-for="extension in g.extensions" :key="extension.code">
<q-card>
<q-card-section>
<q-icon :name="extension.icon" color="grey-5" style="font-size: 4rem;"></q-icon>
{% raw %}
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
{{ extension.shortDescription }}
{% endraw %}
</q-card-section>
<q-separator></q-separator>
<q-card-actions>
<div v-if="extension.isEnabled">
<q-btn flat color="deep-purple"
type="a" :href="[extension.url, '?usr=', g.user.id].join('')">Open</q-btn>
<q-btn flat color="grey-5"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&disable=', extension.code].join('')"> Disable</q-btn>
</div>
<q-btn v-else flat color="deep-purple"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&enable=', extension.code].join('')">
Enable</q-btn>
</q-card-actions>
</q-card>
</div>
</div>
{% endblock %}
+72
View File
@@ -0,0 +1,72 @@
{% extends "public.html" %}
{% block scripts %}
{% assets filters='rjsmin', output='__bundle__/core/index.js',
'core/js/index.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% endblock %}
{% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
{% if lnurl %}
<q-btn unelevated
color="deep-purple"
@click="processing"
type="a" href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}">
Press to claim bitcoin
</q-btn>
{% else %}
<q-form @submit="createWallet" class="q-gutter-md">
<q-input filled dense
v-model="walletName"
label="Name your LNbits wallet *"
></q-input>
<q-btn unelevated
color="deep-purple"
:disable="walletName == ''"
type="submit">Add a new wallet</q-btn>
</q-form>
{% endif %}
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<h3 class="q-my-none"><strong>LN</strong>bits</h3>
<h5 class="q-my-md">Free and open-source lightning wallet</h5>
<p>Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself!</p>
<p>You can run LNbits for yourself, or easily offer a custodian solution for others.</p>
<p>Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool.</p>
<p>Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.</p>
<div class="row q-mt-md q-gutter-sm">
<q-btn outline
color="grey"
type="a" href="https://github.com/arcbtc/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
<q-btn outline
color="grey"
type="a" href="https://paywall.link/to/f4e4e" target="_blank" rel="noopener">Donate</q-btn>
</div>
</q-card-section>
</q-card>
</div>
<!-- Ads -->
<div class="col-12 col-md-3 col-lg-3">
<div class="row q-col-gutter-lg justify-center">
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn flat color="purple" label="Supporters" type="a" href="mailto:lnbits@pm.me" class="full-width"></q-btn>
<a href="https://github.com/rootzoll/raspiblitz">
<q-img contain :src="($q.dark.isActive) ? '{{ url_for('static', filename='images/raspiblitz-negative.svg') }}' : '{{ url_for('static', filename='images/raspiblitz.svg') }}'" style="height: 90px;"></q-img>
</a>
</div>
</div>
</div>
</div>
{% endblock %}
+259
View File
@@ -0,0 +1,259 @@
{% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %}
{% block styles %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.1.1/vue-qrcode-reader.min.css') }}">
{% endblock %}
{% block scripts %}
{{ window_vars(user, wallet) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
'vendor/moment@2.24.0/moment.min.js',
'vendor/chart.js@2.9.3/chart.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets filters='rjsmin', output='__bundle__/core/wallet.js',
'vendor/bolt11/utils.js',
'vendor/bolt11/decoder.js',
'vendor/vue-qrcode-reader@2.1.1/vue-qrcode-reader.min.js',
'core/js/wallet.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% endblock %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<h3 class="q-my-none"><strong>{% raw %}{{ fbalance }}{% endraw %}</strong> sat</h3>
</q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md">
<div class="col">
<q-btn unelevated
color="purple"
class="full-width"
@click="showSendDialog">Send</q-btn>
</div>
<div class="col">
<q-btn unelevated
color="deep-purple"
class="full-width"
@click="showReceiveDialog">Receive</q-btn>
</div>
</div>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Transactions</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
<!--<q-btn v-if="pendingPaymentsExist" dense flat round icon="update" color="grey" @click="checkPendingPayments">
<q-tooltip>Check pending</q-tooltip>
</q-btn>-->
<q-btn dense flat round icon="show_chart" color="grey" @click="showChart">
<q-tooltip>Show chart</q-tooltip>
</q-btn>
</div>
</div>
<q-input v-if="payments.length > 10" filled dense clearable
v-model="paymentsTable.filter"
debounce="300" placeholder="Search by memo, amount" class="q-mb-md">
</q-input>
<q-table dense flat
:data="filteredPayments"
row-key="payhash"
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props">{{ col.label }}</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width class="lnbits__q-table__icon-td">
<q-icon v-if="props.row.isPaid" size="14px"
:name="(props.row.sat < 0) ? 'call_made' : 'call_received'"
:color="(props.row.sat < 0) ? 'pink' : 'green'"></q-icon>
<q-icon v-else name="settings_ethernet" color="grey">
<q-tooltip>Pending</q-tooltip>
</q-icon>
</q-td>
<q-td key="memo" :props="props">
{{ props.row.memo }}
</q-td>
<q-td auto-width key="date" :props="props">
{{ props.row.date }}
</q-td>
<q-td auto-width key="sat" :props="props">
{{ props.row.fsat }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn flat color="grey" @click="exportCSV" class="float-right">Renew keys</q-btn>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">LNbits wallet</h6>
<strong>Wallet name: </strong><em>{{ wallet.name }}</em><br>
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br>
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br>
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "core/_api_docs.html" %}
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="remove_circle"
label="Delete wallet">
<q-card>
<q-card-section>
<p>This whole wallet will be deleted, the funds will be <strong>UNRECOVERABLE</strong>.</p>
<q-btn unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')">Delete wallet</q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</q-card-section>
</q-card>
</div>
</div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card v-if="!receive.paymentReq" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createInvoice" class="q-gutter-md">
<q-input filled dense
v-model.number="receive.data.amount"
type="number"
label="Amount (sat) *"></q-input>
<q-input filled dense
v-model.trim="receive.data.memo"
label="Memo"
placeholder="LNbits invoice"></q-input>
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn unelevated
color="deep-purple"
:disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit">Create invoice</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<q-spinner v-if="receive.status == 'loading'" color="deep-purple" size="2.55em"></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode :value="receive.paymentReq" :options="{width: 340}" class="rounded-borders"></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)">Copy invoice</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="send.show" position="top" @hide="closeSendDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="!send.invoice">
<q-form v-if="!sendCamera.show" @submit="decodeInvoice" class="q-gutter-md">
<q-input filled dense
v-model.trim="send.data.bolt11"
type="textarea"
label="Paste an invoice *">
<template v-slot:after>
<q-btn round dense flat icon="photo_camera" @click="showCamera">
<q-tooltip>Use camera to scan an invoice</q-tooltip>
</q-btn>
</template>
</q-input>
<div class="row q-mt-lg">
<q-btn unelevated
color="deep-purple"
:disable="send.data.bolt11 == ''"
type="submit">Read invoice</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
<div v-else>
<q-responsive :ratio="1">
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</div>
</div>
<div v-else>
{% raw %}
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
<q-separator class="q-my-sm"></q-separator>
<p style="word-break: break-all">
<strong>Memo:</strong> {{ send.invoice.description }}<br>
<strong>Expire date:</strong> {{ send.invoice.expireDate }}<br>
<strong>Hash:</strong> {{ send.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated
color="deep-purple"
@click="payInvoice">Send satoshis</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black">Not enough funds!</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show" position="top">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section>
<canvas ref="canvas" width="600" height="400"></canvas>
</q-card-section>
</q-card>
</q-dialog>
{% if service_fee > 0 %}
<div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-deep-purple">Warning</h6>
<p>Login functionality to be released in v0.2, for now, <strong>make sure you bookmark this page for future access to your wallet</strong>!</p>
<p>This service is in BETA, and we hold no responsibility for people losing access to funds. To encourage you to run your own LNbits installation, any balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will incur a charge of <strong>{{ service_fee }}% service fee</strong> per week.</p>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(disclaimerDialog.location.href)">Copy wallet URL</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">I understand</q-btn>
</div>
</q-card>
</q-dialog>
{% endif %}
{% endblock %}
View File
+89
View File
@@ -0,0 +1,89 @@
from flask import g, jsonify, request
from lnbits.core import core_app
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.settings import WALLET
from ..services import create_invoice, pay_invoice
@core_app.route("/api/v1/payments", methods=["GET"])
@api_check_wallet_key("invoice")
def api_payments():
if "check_pending" in request.args:
g.wallet.delete_expired_payments()
for payment in g.wallet.get_payments(include_all_pending=True):
if payment.is_out:
payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending)
else:
payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending)
return jsonify(g.wallet.get_payments()), Status.OK
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"amount": {"type": "integer", "min": 1, "required": True},
"memo": {"type": "string", "empty": False, "required": True},
}
)
def api_payments_create_invoice():
try:
checking_id, payment_request = create_invoice(
wallet_id=g.wallet.id, amount=g.data["amount"], memo=g.data["memo"]
)
except Exception as e:
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.CREATED
@api_check_wallet_key("admin")
@api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}})
def api_payments_pay_invoice():
try:
checking_id = pay_invoice(wallet_id=g.wallet.id, bolt11=g.data["bolt11"])
except ValueError as e:
return jsonify({"message": str(e)}), Status.BAD_REQUEST
except PermissionError as e:
return jsonify({"message": str(e)}), Status.FORBIDDEN
except Exception as e:
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id}), Status.CREATED
@core_app.route("/api/v1/payments", methods=["POST"])
@api_validate_post_request(schema={"out": {"type": "boolean", "required": True}})
def api_payments_create():
if g.data["out"] is True:
return api_payments_pay_invoice()
return api_payments_create_invoice()
@core_app.route("/api/v1/payments/<checking_id>", methods=["GET"])
@api_check_wallet_key("invoice")
def api_payment(checking_id):
payment = g.wallet.get_payment(checking_id)
if not payment:
return jsonify({"message": "Payment does not exist."}), Status.NOT_FOUND
elif not payment.pending:
return jsonify({"paid": True}), Status.OK
try:
if payment.is_out:
is_paid = not WALLET.get_payment_status(checking_id).pending
elif payment.is_in:
is_paid = not WALLET.get_invoice_status(checking_id).pending
except Exception:
return jsonify({"paid": False}), Status.OK
if is_paid:
payment.set_pending(False)
return jsonify({"paid": True}), Status.OK
return jsonify({"paid": False}), Status.OK
+95
View File
@@ -0,0 +1,95 @@
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
from os import path
from lnbits.core import core_app
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.helpers import Status
from lnbits.settings import SERVICE_FEE
from ..crud import (
create_account,
get_user,
update_user_extension,
create_wallet,
delete_wallet,
)
@core_app.route("/favicon.ico")
def favicon():
return send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico")
@core_app.route("/")
def home():
return render_template("core/index.html", lnurl=request.args.get("lightning", None))
@core_app.route("/extensions")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def extensions():
extension_to_enable = request.args.get("enable", type=str)
extension_to_disable = request.args.get("disable", type=str)
if extension_to_enable and extension_to_disable:
abort(Status.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
if extension_to_enable:
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
elif extension_to_disable:
update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0)
return render_template("core/extensions.html", user=get_user(g.user.id))
@core_app.route("/wallet")
@validate_uuids(["usr", "wal"])
def wallet():
user_id = request.args.get("usr", type=str)
wallet_id = request.args.get("wal", type=str)
wallet_name = request.args.get("nme", type=str)
service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE
# just wallet_name: create a new user, then create a new wallet for user with wallet_name
# just user_id: return the first user wallet or create one if none found (with default wallet_name)
# user_id and wallet_name: create a new wallet for user with wallet_name
# user_id and wallet_id: return that wallet if user is the owner
# nothing: create everything
if not user_id:
user = get_user(create_account().id)
else:
user = get_user(user_id) or abort(Status.NOT_FOUND, "User does not exist.")
if not wallet_id:
if user.wallets and not wallet_name:
wallet = user.wallets[0]
else:
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
if wallet_id not in user.wallet_ids:
abort(Status.FORBIDDEN, "Not your wallet.")
return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee)
@core_app.route("/deletewallet")
@validate_uuids(["usr", "wal"], required=True)
@check_user_exists()
def deletewallet():
wallet_id = request.args.get("wal", type=str)
user_wallet_ids = g.user.wallet_ids
if wallet_id not in user_wallet_ids:
abort(Status.FORBIDDEN, "Not your wallet.")
else:
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
user_wallet_ids.remove(wallet_id)
if user_wallet_ids:
return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0]))
return redirect(url_for("core.home"))
+57
View File
@@ -0,0 +1,57 @@
import requests
from flask import abort, redirect, request, url_for
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore
from lnurl.exceptions import LnurlException # type: ignore
from time import sleep
from lnbits.core import core_app
from lnbits.helpers import Status
from lnbits.settings import WALLET
from ..crud import create_account, get_user, create_wallet, create_payment
@core_app.route("/lnurlwallet")
def lnurlwallet():
memo = "LNbits LNURL funding"
try:
withdraw_res = handle_lnurl(request.args.get("lightning"), response_class=LnurlWithdrawResponse)
except LnurlException:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
try:
ok, checking_id, payment_request, error_message = WALLET.create_invoice(withdraw_res.max_sats, memo)
except Exception as e:
ok, error_message = False, str(e)
if not ok:
abort(Status.INTERNAL_SERVER_ERROR, error_message)
r = requests.get(
withdraw_res.callback.base,
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}},
)
if not r.ok:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
for i in range(10):
invoice_status = WALLET.get_invoice_status(checking_id)
sleep(i)
if not invoice_status.paid:
continue
break
user = get_user(create_account().id)
wallet = create_wallet(user_id=user.id)
create_payment(
wallet_id=wallet.id,
checking_id=checking_id,
amount=withdraw_res.max_sats * 1000,
memo=memo,
pending=invoice_status.pending,
)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+42
View File
@@ -0,0 +1,42 @@
import os
import sqlite3
from .settings import LNBITS_DATA_FOLDER
class Database:
def __init__(self, db_path: str):
self.path = db_path
self.connection = sqlite3.connect(db_path)
self.connection.row_factory = sqlite3.Row
self.cursor = self.connection.cursor()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.cursor.close()
self.connection.close()
def fetchall(self, query: str, values: tuple = ()) -> list:
"""Given a query, return cursor.fetchall() rows."""
self.cursor.execute(query, values)
return self.cursor.fetchall()
def fetchone(self, query: str, values: tuple = ()):
self.cursor.execute(query, values)
return self.cursor.fetchone()
def execute(self, query: str, values: tuple = ()) -> None:
"""Given a query, cursor.execute() it."""
self.cursor.execute(query, values)
self.connection.commit()
def open_db(db_name: str = "database") -> Database:
db_path = os.path.join(LNBITS_DATA_FOLDER, f"{db_name}.sqlite3")
return Database(db_path=db_path)
def open_ext_db(extension_name: str) -> Database:
return open_db(f"ext_{extension_name}")
+82
View File
@@ -0,0 +1,82 @@
from cerberus import Validator # type: ignore
from flask import g, abort, jsonify, request
from functools import wraps
from typing import List, Union
from uuid import UUID
from lnbits.core.crud import get_user, get_wallet_for_key
from .helpers import Status
def api_check_wallet_key(key_type: str = "invoice"):
def wrap(view):
@wraps(view)
def wrapped_view(**kwargs):
try:
g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type)
except KeyError:
return jsonify({"message": "`X-Api-Key` header missing."}), Status.BAD_REQUEST
if not g.wallet:
return jsonify({"message": "Wrong keys."}), Status.UNAUTHORIZED
return view(**kwargs)
return wrapped_view
return wrap
def api_validate_post_request(*, schema: dict):
def wrap(view):
@wraps(view)
def wrapped_view(**kwargs):
if "application/json" not in request.headers["Content-Type"]:
return jsonify({"message": "Content-Type must be `application/json`."}), Status.BAD_REQUEST
v = Validator(schema)
g.data = {key: request.json[key] for key in schema.keys()}
if not v.validate(g.data):
return jsonify({"message": f"Errors in request data: {v.errors}"}), Status.BAD_REQUEST
return view(**kwargs)
return wrapped_view
return wrap
def check_user_exists(param: str = "usr"):
def wrap(view):
@wraps(view)
def wrapped_view(**kwargs):
g.user = get_user(request.args.get(param, type=str)) or abort(Status.NOT_FOUND, "User not found.")
return view(**kwargs)
return wrapped_view
return wrap
def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4):
def wrap(view):
@wraps(view)
def wrapped_view(**kwargs):
query_params = {param: request.args.get(param, type=str) for param in params}
for param, value in query_params.items():
if not value and (required is True or (required and param in required)):
abort(Status.BAD_REQUEST, f"`{param}` is required.")
if value:
try:
UUID(value, version=version)
except ValueError:
abort(Status.BAD_REQUEST, f"`{param}` is not a valid UUID.")
return view(**kwargs)
return wrapped_view
return wrap
+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 flask 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,))
+21
View File
@@ -0,0 +1,21 @@
from lnbits.db import open_ext_db
def m001_initial(db):
"""
Initial amilks table.
"""
db.execute("""
CREATE TABLE IF NOT EXISTS amilks (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
lnurl TEXT NOT NULL,
atime INTEGER NOT NULL,
amount INTEGER NOT NULL
);
""")
def migrate():
with open_ext_db("amilk") as db:
m001_initial(db)
+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,16 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
<p>Milking faucets with software, known as "assmilking", seems at first to be black-hat, although in fact there might be some unexplored use cases. An LNURL withdraw gives someone the right to pull funds, which can be done over time. An LNURL withdraw could be used outside of just faucets, to provide money streaming and repeat payments.<br/>Paste or scan an LNURL withdraw, enter the amount for the AMilk to pull and the frequency for it to be pulled.<br/>
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
</q-card>
</q-card-section>
</q-card-section></q-expansion-item>
@@ -0,0 +1,230 @@
{% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true">New AMilk</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">AMilks</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table dense flat
:data="amilks"
row-key="id"
:columns="amilksTable.columns"
:pagination.sync="amilksTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="deleteAMilk(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Assistant Faucet Milker Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "amilk/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="amilkDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createAMilk" class="q-gutter-md">
<q-select filled dense emit-value v-model="amilkDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="amilkDialog.data.lnurl"
type="url"
label="LNURL Withdraw"></q-input>
<q-input filled dense
v-model.number="amilkDialog.data.amount"
type="number"
label="Amount *"></q-input>
<q-input filled dense
v-model.trim="amilkDialog.data.atime"
type="number"
label="Hit frequency (secs)"
placeholder="Frequency to be hit"></q-input>
<q-btn unelevated
color="deep-purple"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
type="submit">Create amilk</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
var mapAMilk = function (obj) {
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
obj.wall = ['/amilk/', obj.id].join('');
return obj;
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
amilks: [],
amilksTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
],
pagination: {
rowsPerPage: 10
}
},
amilkDialog: {
show: false,
data: {}
}
};
},
methods: {
getAMilks: function () {
var self = this;
LNbits.api.request(
'GET',
'/amilk/api/v1/amilk?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
self.amilks = response.data.map(function (obj) {
response.data.forEach(MILK);
function MILK(item){
window.setInterval(function(){
LNbits.api.request(
'GET',
'/amilk/api/v1/amilk/milk/' + item.id,
"Lorem"
).then(function (response) {
self.amilks = response.data.map(function (obj) {
return mapAMilk(obj);
});
});
},
item.atime*1000);
}
return mapAMilk(obj);
});
});
},
createAMilk: function () {
var data = {
lnurl: this.amilkDialog.data.lnurl,
atime: parseInt(this.amilkDialog.data.atime),
amount: this.amilkDialog.data.amount
};
var self = this;
console.log(this.amilkDialog.data.wallet);
LNbits.api.request(
'POST',
'/amilk/api/v1/amilk',
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet}).inkey,
data
).then(function (response) {
self.amilks.push(mapAMilk(response.data));
self.amilkDialog.show = false;
self.amilkDialog.data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deleteAMilk: function (amilkId) {
var self = this;
var amilk = _.findWhere(this.amilks, {id: amilkId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this AMilk link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/amilk/api/v1/amilks/' + amilkId,
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
).then(function (response) {
self.amilks = _.reject(self.amilks, function (obj) { return obj.id == amilkId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportCSV: function () {
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks);
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getAMilks();
}
}
});
</script>
{% endblock %}
+21
View File
@@ -0,0 +1,21 @@
from flask import g, abort, render_template
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.amilk import amilk_ext
from lnbits.helpers import Status
from .crud import get_amilk
@amilk_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def index():
return render_template("amilk/index.html", user=g.user)
@amilk_ext.route("/<amilk_id>")
def wall(amilk_id):
amilk = get_amilk(amilk_id) or abort(Status.NOT_FOUND, "AMilk does not exist.")
return render_template("amilk/wall.html", amilk=amilk)
+94
View File
@@ -0,0 +1,94 @@
from flask import g, jsonify, request
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.extensions.amilk import amilk_ext
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
from lnbits.core.services import create_invoice
from flask import abort, redirect, request, url_for
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl
from lnurl.exceptions import LnurlException
from time import sleep
import requests
from lnbits.settings import WALLET
@amilk_ext.route("/api/v1/amilk", methods=["GET"])
@api_check_wallet_key("invoice")
def api_amilks():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), Status.OK
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
def api_amilkit(amilk_id):
milk = get_amilk(amilk_id)
memo = milk.id
try:
withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse)
except LnurlException:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
print(withdraw_res.max_sats)
try:
checking_id, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo)
#print(payment_request)
except Exception as e:
error_message = False, str(e)
r = requests.get(
withdraw_res.callback.base,
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}},
)
if not r.ok:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
for i in range(10):
invoice_status = WALLET.get_invoice_status(checking_id)
sleep(i)
if not invoice_status.paid:
continue
else:
return jsonify({"paid": False}), Status.OK
break
return jsonify({"paid": True}), Status.OK
@amilk_ext.route("/api/v1/amilk", methods=["POST"])
@api_check_wallet_key("invoice")
@api_validate_post_request(schema={
"lnurl": {"type": "string", "empty": False, "required": True},
"atime": {"type": "integer", "min": 0, "required": True},
"amount": {"type": "integer", "min": 0, "required": True},
})
def api_amilk_create():
amilk = create_amilk(wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"])
return jsonify(amilk._asdict()), Status.CREATED
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
def api_amilk_delete(amilk_id):
amilk = get_amilk(amilk_id)
if not amilk:
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
if amilk.wallet != g.wallet.id:
return jsonify({"message": "Not your amilk."}), Status.FORBIDDEN
delete_amilk(amilk_id)
return "", Status.NO_CONTENT
+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 flask import Blueprint
diagonalley_ext: Blueprint = Blueprint("diagonalley", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
@@ -0,0 +1,6 @@
{
"name": "Diagon Alley",
"short_description": "Movable anonymous market stand",
"icon": "add_shopping_cart",
"contributors": ["eillarra"]
}
+185
View File
@@ -0,0 +1,185 @@
from base64 import urlsafe_b64encode
from uuid import uuid4
from typing import List, Optional, Union
import requests
from lnbits.db import open_ext_db
from lnbits.settings import WALLET
from .models import Products, Orders, Indexers
import re
regex = re.compile(
r'^(?:http|ftp)s?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
###Products
def create_diagonalleys_product(*, wallet_id: str, product: str, categories: str, description: str, image: str, price: int, quantity: int) -> Products:
with open_ext_db("diagonalley") as db:
product_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
db.execute(
"""
INSERT INTO products (id, wallet, product, categories, description, image, price, quantity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(product_id, wallet_id, product, categories, description, image, price, quantity),
)
return get_diagonalleys_product(product_id)
def update_diagonalleys_product(product_id: str, **kwargs) -> Optional[Indexers]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("diagonalley") as db:
db.execute(f"UPDATE products SET {q} WHERE id = ?", (*kwargs.values(), product_id))
row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,))
return get_diagonalleys_indexer(product_id)
def get_diagonalleys_product(product_id: str) -> Optional[Products]:
with open_ext_db("diagonalley") as db:
row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,))
return Products(**row) if row else None
def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Products]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM products WHERE wallet IN ({q})", (*wallet_ids,))
return [Products(**row) for row in rows]
def delete_diagonalleys_product(product_id: str) -> None:
with open_ext_db("diagonalley") as db:
db.execute("DELETE FROM products WHERE id = ?", (product_id,))
###Indexers
def create_diagonalleys_indexer(wallet_id: str, shopname: str, indexeraddress: str, shippingzone1: str, shippingzone2: str, zone1cost: int, zone2cost: int, email: str) -> Indexers:
with open_ext_db("diagonalley") as db:
indexer_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
db.execute(
"""
INSERT INTO indexers (id, wallet, shopname, indexeraddress, online, rating, shippingzone1, shippingzone2, zone1cost, zone2cost, email)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(indexer_id, wallet_id, shopname, indexeraddress, False, 0, shippingzone1, shippingzone2, zone1cost, zone2cost, email),
)
return get_diagonalleys_indexer(indexer_id)
def update_diagonalleys_indexer(indexer_id: str, **kwargs) -> Optional[Indexers]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("diagonalley") as db:
db.execute(f"UPDATE indexers SET {q} WHERE id = ?", (*kwargs.values(), indexer_id))
row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
return get_diagonalleys_indexer(indexer_id)
def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]:
with open_ext_db("diagonalley") as db:
roww = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
try:
x = requests.get(roww["indexeraddress"] + "/" + roww["ratingkey"])
if x.status_code == 200:
print(x)
print("poo")
with open_ext_db("diagonalley") as db:
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (True, indexer_id,))
else:
with open_ext_db("diagonalley") as db:
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (False, indexer_id,))
except:
print("An exception occurred")
with open_ext_db("diagonalley") as db:
row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
return Indexers(**row) if row else None
def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexers]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,))
for r in rows:
try:
x = requests.get(r["indexeraddress"] + "/" + r["ratingkey"])
if x.status_code == 200:
with open_ext_db("diagonalley") as db:
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (True, r["id"],))
else:
with open_ext_db("diagonalley") as db:
db.execute("UPDATE indexers SET online = ? WHERE id = ?", (False, r["id"],))
except:
print("An exception occurred")
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,))
return [Indexers(**row) for row in rows]
def delete_diagonalleys_indexer(indexer_id: str) -> None:
with open_ext_db("diagonalley") as db:
db.execute("DELETE FROM indexers WHERE id = ?", (indexer_id,))
###Orders
def create_diagonalleys_order(*, productid: str, wallet: str, product: str, quantity: int, shippingzone: str, address: str, email: str, invoiceid: str, paid: bool, shipped: bool) -> Indexers:
with open_ext_db("diagonalley") as db:
order_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
db.execute(
"""
INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(order_id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, False, False),
)
return get_diagonalleys_order(order_id)
def get_diagonalleys_order(order_id: str) -> Optional[Orders]:
with open_ext_db("diagonalley") as db:
row = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
return Orders(**row) if row else None
def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,))
for r in rows:
PAID = WALLET.get_invoice_status(r["invoiceid"]).paid
if PAID:
with open_ext_db("diagonalley") as db:
db.execute("UPDATE orders SET paid = ? WHERE id = ?", (True, r["id"],))
rows = db.fetchall(f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,))
return [Orders(**row) for row in rows]
def delete_diagonalleys_order(order_id: str) -> None:
with open_ext_db("diagonalley") as db:
db.execute("DELETE FROM orders WHERE id = ?", (order_id,))
@@ -0,0 +1,64 @@
from lnbits.db import open_ext_db
def m001_initial(db):
"""
Initial products table.
"""
db.execute("""
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
product TEXT NOT NULL,
categories TEXT NOT NULL,
description TEXT NOT NULL,
image TEXT NOT NULL,
price INTEGER NOT NULL,
quantity INTEGER NOT NULL
);
""")
"""
Initial indexers table.
"""
db.execute("""
CREATE TABLE IF NOT EXISTS indexers (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
shopname TEXT NOT NULL,
indexeraddress TEXT NOT NULL,
online BOOLEAN NOT NULL,
rating INTEGER NOT NULL,
shippingzone1 TEXT NOT NULL,
shippingzone2 TEXT NOT NULL,
zone1cost INTEGER NOT NULL,
zone2cost INTEGER NOT NULL,
email TEXT NOT NULL
);
""")
"""
Initial orders table.
"""
db.execute("""
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
productid TEXT NOT NULL,
wallet TEXT NOT NULL,
product TEXT NOT NULL,
quantity INTEGER NOT NULL,
shippingzone INTEGER NOT NULL,
address TEXT NOT NULL,
email TEXT NOT NULL,
invoiceid TEXT NOT NULL,
paid BOOLEAN NOT NULL,
shipped BOOLEAN NOT NULL
);
""")
def migrate():
with open_ext_db("diagonalley") as db:
m001_initial(db)
+39
View File
@@ -0,0 +1,39 @@
from typing import NamedTuple
class Indexers(NamedTuple):
id: str
wallet: str
shopname: str
indexeraddress: str
online: bool
rating: str
shippingzone1: str
shippingzone2: str
zone1cost: int
zone2cost: int
email: str
class Products(NamedTuple):
id: str
wallet: str
product: str
categories: str
description: str
image: str
price: int
quantity: int
class Orders(NamedTuple):
id: str
productid: str
wallet: str
product: str
quantity: int
shippingzone: int
address: str
email: str
invoiceid: str
paid: bool
shipped: bool
@@ -0,0 +1,64 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Diagon Alley: Movable, anonymous market stall</h5>
<p>Make a list of products to sell, point your list of products at a public indexer. Buyers browse your products on the indexer, and pay you directly. Ratings are managed by the indexer. Your stall can be listed in multiple indexers, even over TOR, if you wish to be anonymous.<br/>
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
</q-card>
</q-card-section>
</q-card-section>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="Get prodcuts, categorised by wallet">
<q-card>
<q-card-section>
<code><span class="text-light-blue">GET</span> /api/v1/diagonalley/stall/products/&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,660 @@
{% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="productDialog.show = true">New Product</q-btn>
<q-btn unelevated color="deep-purple" @click="indexerDialog.show = true">New Indexer
<q-tooltip>
Frontend shop your stall will list its products in
</q-tooltip></q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Products</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportProductsCSV">Export to CSV</q-btn>
</div>
</div>
<q-table dense flat
:data="products"
row-key="id"
:columns="productsTable.columns"
:pagination.sync="productsTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn unelevated dense size="xs" icon="add_shopping_cart" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.wallet" target="_blank"></q-btn>
<q-tooltip>
Link to pass to stall indexer
</q-tooltip>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="openProductUpdateDialog(props.row.id)" icon="edit" color="light-blue"></q-btn>
<q-btn flat dense size="xs" @click="deleteProduct(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Indexers</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportIndexersCSV">Export to CSV</q-btn>
</div>
</div>
<q-table dense flat
:data="indexers"
row-key="id"
:columns="indexersTable.columns"
:pagination.sync="indexersTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="openIndexerUpdateDialog(props.row.id)" icon="edit" color="light-blue"></q-btn>
<q-btn flat dense size="xs" @click="deleteIndexer(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Orders</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportOrdersCSV">Export to CSV</q-btn>
</div>
</div>
<q-table dense flat
:data="orders"
row-key="id"
:columns="ordersTable.columns"
:pagination.sync="ordersTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="shipOrder(props.row.id)" icon="add_shopping_cart" color="green">
<q-tooltip>
Product shipped?
</q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="deleteOrder(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Diagon Alley Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "diagonalley/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="productDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendProductFormData" class="q-gutter-md">
<q-select filled dense emit-value v-model="productDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="productDialog.data.product"
label="Product"></q-input>
<q-input filled dense
v-model.trim="productDialog.data.categories"
placeholder="cakes, guns, wool, drugs"
label="Categories seperated by comma"></q-input>
<q-input filled dense
v-model.trim="productDialog.data.description"
label="Description"></q-input>
<q-input filled dense
v-model.trim="productDialog.data.image"
label="Image" placeholder="Imagur link (max 500/500px)"></q-input>
<q-input filled dense
v-model.number="productDialog.data.price"
type="number"
label="Price"></q-input>
<q-input filled dense
v-model.number="productDialog.data.quantity"
type="number"
label="Quantity"
></q-input>
<div class="row q-mt-lg">
<q-btn v-if="productDialog.data.id" unelevated
color="deep-purple"
type="submit">Update Product</q-btn>
<q-btn v-else unelevated
color="deep-purple"
:disable="productDialog.data.image == null
|| productDialog.data.product == null
|| productDialog.data.description == null
|| productDialog.data.quantity == null"
type="submit">Create Product</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="indexerDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendIndexerFormData" class="q-gutter-md">
<q-select filled dense emit-value v-model="indexerDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="indexerDialog.data.shopname"
label="Shop Name"></q-input>
<q-input filled dense
v-model.trim="indexerDialog.data.indexeraddress"
label="Shop link (LNbits will point to)"></q-input>
<q-select
filled dense
v-model.trim="indexerDialog.data.shippingzone1"
multiple
:options="shippingoptions"
label="Shipping Zone 1"
></q-select>
<q-input filled dense
v-model.number="indexerDialog.data.zone1cost"
type="number"
label="Zone 1 Cost"></q-input>
<q-select
filled dense
v-model.trim="indexerDialog.data.shippingzone2"
multiple
:options="shippingoptions"
label="Shipping Zone 2"
></q-select>
<q-input filled dense
v-model.number="indexerDialog.data.zone2cost"
type="number"
label="Zone 2 Cost"></q-input>
<q-input filled dense
v-model.trim="indexerDialog.data.email"
label="Email to share with customers"></q-input>
<div class="row q-mt-lg">
<q-btn v-if="indexerDialog.data.id" unelevated
color="deep-purple"
type="submit">Update Indexer</q-btn>
<q-btn v-else unelevated
color="deep-purple"
:disable="indexerDialog.data.shopname == null
|| indexerDialog.data.shippingzone1 == null
|| indexerDialog.data.indexeraddress == null
|| indexerDialog.data.zone1cost == null
|| indexerDialog.data.shippingzone2 == null
|| indexerDialog.data.zone2cost == null
|| indexerDialog.data.email == null"
type="submit">Create Indexer</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
var mapDiagonAlley = function (obj) {
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
obj.wall = ['/diagonalley/', obj.id].join('');
obj._data = _.clone(obj);
return obj;
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
products: [],
orders: [],
indexers: [],
shippedModel: false,
shippingoptions: [
'Australia', 'Austria', 'Belgium', 'Brazil', 'Canada', 'Denmark', 'Finland', 'France*', 'Germany', 'Greece', 'Hong Kong', 'Hungary', 'Ireland', 'Indonesia', 'Israel', 'Italy', 'Japan', 'Kazakhstan', 'Korea', 'Luxembourg', 'Malaysia', 'Mexico', 'Netherlands', 'New Zealand', 'Norway', 'Poland', 'Portugal', 'Russia', 'Saudi Arabia', 'Singapore', 'Spain', 'Sweden', 'Switzerland', 'Thailand', 'Turkey', 'Ukraine', 'United Kingdom**', 'United States***', 'Vietnam', 'China'
],
label: '',
indexersTable: {
columns: [
{name: 'shopname', align: 'left', label: 'Shop', field: 'shopname'},
{name: 'indexeraddress', align: 'left', label: 'Address', field: 'indexeraddress'},
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'rating', align: 'left', label: 'Your Rating', field: 'rating'},
{name: 'email', align: 'left', label: 'Your email', field: 'email'},
{name: 'online', align: 'left', label: 'Online', field: 'online'}
],
pagination: {
rowsPerPage: 10
}
},
ordersTable: {
columns: [
{name: 'product', align: 'left', label: 'Product', field: 'product'},
{name: 'quantity', align: 'left', label: 'Quantity', field: 'quantity'},
{name: 'address', align: 'left', label: 'Address', field: 'address'},
{name: 'invoiceid', align: 'left', label: 'InvoiceID', field: 'invoiceid'},
{name: 'paid', align: 'left', label: 'Paid', field: 'paid'},
{name: 'shipped', align: 'left', label: 'Shipped', field: 'shipped'}
],
pagination: {
rowsPerPage: 10
}
},
productsTable: {
columns: [
{name: 'product', align: 'left', label: 'Product', field: 'product'},
{name: 'description', align: 'left', label: 'Description', field: 'description'},
{name: 'categories', align: 'left', label: 'Categories', field: 'categories'},
{name: 'image', align: 'left', label: 'Image', field: 'image'},
{name: 'price', align: 'left', label: 'Price', field: 'price'},
{name: 'quantity', align: 'left', label: 'Quantity', field: 'quantity'},
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'}
],
pagination: {
rowsPerPage: 10
}
},
productDialog: {
show: false,
data: {}
},
orderDialog: {
show: false,
data: {}
},
indexerDialog: {
show: false,
data: {}
},
};
},
methods: {
getIndexers: function () {
var self = this;
LNbits.api.request(
'GET',
'/diagonalley/api/v1/diagonalley/indexers?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
self.indexers = response.data.map(function (obj) {
return mapDiagonAlley(obj);
});
});
},
openIndexerUpdateDialog: function (linkId) {
var link = _.findWhere(this.indexers, {id: linkId});
this.indexerDialog.data = _.clone(link._data);
this.indexerDialog.show = true;
},
sendIndexerFormData: function () {
if (this.indexerDialog.data.id){}
else{
var data = {
shopname: this.indexerDialog.data.shopname,
indexeraddress: this.indexerDialog.data.indexeraddress,
shippingzone1: this.indexerDialog.data.shippingzone1.join(', '),
zone1cost: this.indexerDialog.data.zone1cost,
shippingzone2: this.indexerDialog.data.shippingzone2.join(', '),
zone2cost: this.indexerDialog.data.zone2cost,
email: this.indexerDialog.data.email
};}
if (this.indexerDialog.data.id) { this.updateIndexer(this.indexerDialog.data); }
else { this.createIndexer(data); }
},
updateIndexer: function (data) {
var self = this;
LNbits.api.request(
'PUT',
'/diagonalley/api/v1/diagonalley/indexers' + data.id,
_.findWhere(this.g.user.wallets, {id: this.indexerDialog.data.wallet}).inkey,
_.pick(data, 'shopname', 'indexeraddress', 'shippingzone1', 'zone1cost', 'shippingzone2', 'zone2cost', 'email')
).then(function (response) {
self.indexers = _.reject(self.indexers, function (obj) { return obj.id == data.id; });
self.indexers.push(mapDiagonAlley(response.data));
self.indexerDialog.show = false;
self.indexerDialog.data = {};
data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
createIndexer: function (data) {
var self = this;
LNbits.api.request(
'POST',
'/diagonalley/api/v1/diagonalley/indexers',
_.findWhere(this.g.user.wallets, {id: this.indexerDialog.data.wallet}).inkey,
data
).then(function (response) {
self.indexers.push(mapDiagonAlley(response.data));
self.indexerDialog.show = false;
self.indexerDialog.data = {};
data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deleteIndexer: function (indexerId) {
var self = this;
var indexer = _.findWhere(this.indexers, {id: indexerId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this Indexer link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/diagonalley/api/v1/diagonalley/indexers/' + indexerId,
_.findWhere(self.g.user.wallets, {id: indexer.wallet}).inkey
).then(function (response) {
self.indexers = _.reject(self.indexers, function (obj) { return obj.id == indexerId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportIndexersCSV: function () {
LNbits.utils.exportCSV(this.indexersTable.columns, this.indexers);
},
getOrders: function () {
var self = this;
LNbits.api.request(
'GET',
'/diagonalley/api/v1/diagonalley/orders?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
self.orders = response.data.map(function (obj) {
return mapDiagonAlley(obj);
});
});
},
createOrder: function () {
var data = {
address: this.orderDialog.data.address,
email: this.orderDialog.data.email,
quantity: this.orderDialog.data.quantity,
shippingzone: this.orderDialog.data.shippingzone
};
var self = this;
LNbits.api.request(
'POST',
'/diagonalley/api/v1/diagonalley/orders',
_.findWhere(this.g.user.wallets, {id: this.orderDialog.data.wallet}).inkey,
data
).then(function (response) {
self.orders.push(mapDiagonAlley(response.data));
self.orderDialog.show = false;
self.orderDialog.data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deleteOrder: function (orderId) {
var self = this;
var order = _.findWhere(this.orders, {id: orderId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this order link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/diagonalley/api/v1/diagonalley/orders/' + orderId,
_.findWhere(self.g.user.wallets, {id: order.wallet}).inkey
).then(function (response) {
self.orders = _.reject(self.orders, function (obj) { return obj.id == orderId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
shipOrder: function (order_id){
var self = this;
LNbits.api.request(
'GET',
'/diagonalley/api/v1/diagonalley/orders/shipped/' + order_id,
this.g.user.wallets[0].inkey
).then(function (response) {
self.orders = response.data.map(function (obj) {
return mapDiagonAlley(obj);
});
});
},
exportOrdersCSV: function () {
LNbits.utils.exportCSV(this.ordersTable.columns, this.orders);
},
getProducts: function () {
var self = this;
LNbits.api.request(
'GET',
'/diagonalley/api/v1/diagonalley/products?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
self.products = response.data.map(function (obj) {
return mapDiagonAlley(obj);
});
});
},
openProductUpdateDialog: function (linkId) {
var link = _.findWhere(this.products, {id: linkId});
this.productDialog.data = _.clone(link._data);
this.productDialog.show = true;
},
sendProductFormData: function () {
if (this.productDialog.data.id){}
else{
var data = {
product: this.productDialog.data.product,
categories: this.productDialog.data.categories,
description: this.productDialog.data.description,
image: this.productDialog.data.image,
price: this.productDialog.data.price,
quantity: this.productDialog.data.quantity
};}
if (this.productDialog.data.id) {
this.updateProduct(this.productDialog.data); }
else { this.createProduct(data); }
},
updateProduct: function (data) {
var self = this;
LNbits.api.request(
'PUT',
'/diagonalley/api/v1/diagonalley/products' + data.id,
_.findWhere(this.g.user.wallets, {id: this.productDialog.data.wallet}).inkey,
_.pick(data, 'shopname', 'indexeraddress', 'shippingzone1', 'zone1cost', 'shippingzone2', 'zone2cost', 'email')
).then(function (response) {
self.products = _.reject(self.products, function (obj) { return obj.id == data.id; });
self.products.push(mapDiagonAlley(response.data));
self.productDialog.show = false;
self.productDialog.data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
createProduct: function (data) {
var self = this;
LNbits.api.request(
'POST',
'/diagonalley/api/v1/diagonalley/products',
_.findWhere(this.g.user.wallets, {id: this.productDialog.data.wallet}).inkey,
data
).then(function (response) {
self.products.push(mapDiagonAlley(response.data));
self.productDialog.show = false;
self.productDialog.data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deleteProduct: function (productId) {
var self = this;
var product = _.findWhere(this.products, {id: productId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this products link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/diagonalley/api/v1/diagonalley/products/' + productId,
_.findWhere(self.g.user.wallets, {id: product.wallet}).inkey
).then(function (response) {
self.products = _.reject(self.products, function (obj) { return obj.id == productId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportProductsCSV: function () {
LNbits.utils.exportCSV(this.productsTable.columns, this.products);
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getProducts();
this.getOrders();
this.getIndexers();
}
}
});
</script>
{% endblock %}
@@ -0,0 +1 @@
<script>console.log("{{ stall }}")</script>
+14
View File
@@ -0,0 +1,14 @@
from flask import g, abort, render_template, jsonify
import json
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.diagonalley import diagonalley_ext
from lnbits.helpers import Status
from lnbits.db import open_ext_db
@diagonalley_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def index():
return render_template("diagonalley/index.html", user=g.user)
+251
View File
@@ -0,0 +1,251 @@
from flask import g, jsonify, request
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.extensions.diagonalley import diagonalley_ext
from .crud import create_diagonalleys_product,get_diagonalleys_product,get_diagonalleys_products,delete_diagonalleys_product,create_diagonalleys_indexer,update_diagonalleys_indexer,get_diagonalleys_indexer,get_diagonalleys_indexers,delete_diagonalleys_indexer,create_diagonalleys_order,get_diagonalleys_order,get_diagonalleys_orders,delete_diagonalleys_order
from lnbits.core.services import create_invoice
from base64 import urlsafe_b64encode
from uuid import uuid4
from lnbits.db import open_ext_db
###Products
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalley_products():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), Status.OK
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["POST"])
@diagonalley_ext.route("/api/v1/diagonalley/products<product_id>", methods=["PUT"])
@api_check_wallet_key(key_type="invoice")
@api_validate_post_request(schema={
"product": {"type": "string", "empty": False, "required": True},
"categories": {"type": "string", "empty": False, "required": True},
"description": {"type": "string", "empty": False, "required": True},
"image": {"type": "string", "empty": False, "required": True},
"price": {"type": "integer", "min": 0, "required": True},
"quantity": {"type": "integer", "min": 0, "required": True}
})
def api_diagonalley_product_create(product_id=None):
if product_id:
product = get_diagonalleys_indexer(product_id)
if not product:
return jsonify({"message": "Withdraw product does not exist."}), Status.NOT_FOUND
if product.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw product."}), Status.FORBIDDEN
product = update_diagonalleys_product(product_id, **g.data)
else:
product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data)
return jsonify(product._asdict()), Status.OK if product_id else Status.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/products/<product_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalley_products_delete(product_id):
product = get_diagonalleys_product(product_id)
if not product:
return jsonify({"message": "Product does not exist."}), Status.NOT_FOUND
if product.wallet != g.wallet.id:
return jsonify({"message": "Not your Diagon Alley."}), Status.FORBIDDEN
delete_diagonalleys_product(product_id)
return "", Status.NO_CONTENT
###Indexers
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalley_indexers():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), Status.OK
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["POST"])
@diagonalley_ext.route("/api/v1/diagonalley/indexers<indexer_id>", methods=["PUT"])
@api_check_wallet_key(key_type="invoice")
@api_validate_post_request(schema={
"shopname": {"type": "string", "empty": False, "required": True},
"indexeraddress": {"type": "string", "empty": False, "required": True},
"shippingzone1": {"type": "string", "empty": False, "required": True},
"shippingzone2": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
"zone1cost": {"type": "integer", "min": 0, "required": True},
"zone2cost": {"type": "integer", "min": 0, "required": True}
})
def api_diagonalley_indexer_create(indexer_id=None):
if indexer_id:
indexer = get_diagonalleys_indexer(indexer_id)
if not indexer:
return jsonify({"message": "Withdraw indexer does not exist."}), Status.NOT_FOUND
if indexer.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw indexer."}), Status.FORBIDDEN
indexer = update_diagonalleys_indexer(indexer_id, **g.data)
else:
indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **g.data)
return jsonify(indexer._asdict()), Status.OK if indexer_id else Status.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalley_indexer_delete(indexer_id):
indexer = get_diagonalleys_indexer(indexer_id)
if not indexer:
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
if indexer.wallet != g.wallet.id:
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
delete_diagonalleys_indexer(indexer_id)
return "", Status.NO_CONTENT
###Orders
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalley_orders():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), Status.OK
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["POST"])
@api_check_wallet_key(key_type="invoice")
@api_validate_post_request(schema={
"id": {"type": "string", "empty": False, "required": True},
"address": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
"quantity": {"type": "integer", "empty": False, "required": True},
"shippingzone": {"type": "integer", "empty": False, "required": True},
})
def api_diagonalley_order_create():
order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data)
return jsonify(order._asdict()), Status.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/orders/<order_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalley_order_delete(order_id):
order = get_diagonalleys_order(order_id)
if not order:
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
if order.wallet != g.wallet.id:
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
delete_diagonalleys_indexer(order_id)
return "", Status.NO_CONTENT
@diagonalley_ext.route("/api/v1/diagonalley/orders/paid/<order_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalleys_order_paid(order_id):
with open_ext_db("diagonalley") as db:
db.execute("UPDATE orders SET paid = ? WHERE id = ?", (True, order_id,))
return "", Status.OK
@diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/<order_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
def api_diagonalleys_order_shipped(order_id):
with open_ext_db("diagonalley") as db:
db.execute("UPDATE orders SET shipped = ? WHERE id = ?", (True, order_id,))
order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), Status.OK
###List products based on indexer id
@diagonalley_ext.route("/api/v1/diagonalley/stall/products/<indexer_id>", methods=["GET"])
def api_diagonalleys_stall_products(indexer_id):
with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
print(rows[1])
if not rows:
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
products = db.fetchone("SELECT * FROM products WHERE wallet = ?", (rows[1],))
if not products:
return jsonify({"message": "No products"}), Status.NOT_FOUND
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), Status.OK
###Check a product has been shipped
@diagonalley_ext.route("/api/v1/diagonalley/stall/checkshipped/<checking_id>", methods=["GET"])
def api_diagonalleys_stall_checkshipped(checking_id):
with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
return jsonify({"shipped": rows["shipped"]}), Status.OK
###Place order
@diagonalley_ext.route("/api/v1/diagonalley/stall/order/<indexer_id>", methods=["POST"])
@api_validate_post_request(schema={
"id": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
"address": {"type": "string", "empty": False, "required": True},
"quantity": {"type": "integer", "empty": False, "required": True},
"shippingzone": {"type": "integer", "empty": False, "required": True},
})
def api_diagonalley_stall_order(indexer_id):
product = get_diagonalleys_product(g.data["id"])
shipping = get_diagonalleys_indexer(indexer_id)
if g.data["shippingzone"] == 1:
shippingcost = shipping.zone1cost
else:
shippingcost = shipping.zone2cost
checking_id, payment_request = create_invoice(wallet_id=product.wallet, amount=shippingcost + (g.data["quantity"] * product.price), memo=g.data["id"])
selling_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8')
with open_ext_db("diagonalley") as db:
db.execute(
"""
INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(selling_id ,g.data["id"] , product.wallet, product.product, g.data["quantity"], g.data["shippingzone"], g.data["address"], g.data["email"], checking_id, False, False),
)
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.OK
+8
View File
@@ -0,0 +1,8 @@
<h1> LNEVENTS</h1>
<h2>Create/sell tickets for an event</h2>
<p>Events is an easy way to create/sell tickets for an event.
It is advised to setup a specific wallet in lnbits for the event.</p>
<img src="https://i.imgur.com/qHi7ExL.png">
+8
View File
@@ -0,0 +1,8 @@
from flask import Blueprint
events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
@@ -0,0 +1,6 @@
{
"name": "Events",
"short_description": "LN tickets for events.",
"icon": "local_activity",
"contributors": ["arcbtc"]
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Events",
"short_description": "LN tickets for events.",
"icon": "local_activity",
"contributors": ["arcbtc"]
}
+5
View File
@@ -0,0 +1,5 @@
from lnbits.db import open_ext_db
def migrate():
print("pending")
+26
View File
@@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS events (
key INTEGER PRIMARY KEY AUTOINCREMENT,
usr TEXT,
wal TEXT,
walnme TEXT,
walinvkey INTEGER,
uni TEXT,
tit TEXT,
cldate TEXT,
notickets INTEGER,
sold INTEGER DEFAULT 0,
prtick INTEGER,
descr TEXT,
unireg TEXT
);
CREATE TABLE IF NOT EXISTS eventssold (
key INTEGER PRIMARY KEY AUTOINCREMENT,
uni TEXT,
email TEXT,
name TEXT,
hash TEXT,
paid INTEGER DEFAULT 0,
reg INTEGER DEFAULT 0
);
@@ -0,0 +1,570 @@
<!-- @format -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>LNBits Wallet</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport"
/>
<!-- Bootstrap 3.3.2 -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
/>
<!-- FontAwesome 4.3.0 -->
<link
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Ionicons 2.0.0 -->
<link
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Theme style -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
/>
<!-- AdminLTE Skins. Choose a skin from the css/skins
folder instead of downloading all of them to reduce the load. -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
/>
<!-- Morris chart -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
/>
<!-- jvectormap -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
/>
<!-- bootstrap wysihtml5 - text editor -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
/>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<style>
.small-box > .small-box-footer {
text-align: left;
padding-left: 10px;
}
#loadingMessage {
text-align: center;
padding: 40px;
background-color: #eee;
}
#canvas {
width: 100%;
}
#output {
margin-top: 20px;
background: #eee;
padding: 10px;
padding-bottom: 0;
}
#output div {
padding-bottom: 10px;
word-wrap: break-word;
}
#noQRFound {
text-align: center;
}
</style>
<!-- jQuery 2.1.3 -->
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
<!-- jQuery UI 1.11.2 -->
<script
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
type="text/javascript"
></script>
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
<script>
$.widget.bridge('uibutton', $.ui.button)
</script>
<!-- Bootstrap 3.3.2 JS -->
<script
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
type="text/javascript"
></script>
<!-- Morris.js charts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
type="text/javascript"
></script>
<!-- Sparkline -->
<script
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
type="text/javascript"
></script>
<!-- jvectormap -->
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
type="text/javascript"
></script>
<!-- jQuery Knob Chart -->
<script
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
type="text/javascript"
></script>
<!-- Bootstrap WYSIHTML5 -->
<script
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
type="text/javascript"
></script>
<!-- Slimscroll -->
<script
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
type="text/javascript"
></script>
<!-- FastClick -->
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
<!-- AdminLTE App -->
<script
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
<script
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE for demo purposes -->
<script
src="{{ url_for('static', filename='dist/js/demo.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
type="text/javascript"
></script>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
type="text/javascript"
></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background:
#1f2234;
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color:
#3e355a;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
<!-- Header Navbar: style can be found in header.less -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a
href="#"
class="sidebar-toggle"
data-toggle="offcanvas"
role="button"
>
<span class="sr-only">Toggle navigation</span>
</a>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu">
{% block messages %}{% endblock %}
</li>
</ul>
</div>
</nav>
</header>
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;">
</section>
<!-- /.sidebar -->
</aside>
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
LNBits Events
<small>Lightning powered tickets</small>
</h1>
</section>
<!-- Main content -->
<section class="content"><br/><br/>
<center><h1 style="font-size:500%">{{ nme }}</h1></center>
<center><h2 style="width:55%;word-wrap: break-word;" >{{ descr }}</h2></center>
<div id="theform">
<br/><br/><br/>
<center>
<form role="form">
<div class="form-group" style="width:300px;">
<input id="Nam" type="text" class="form-control" placeholder="Name"></input>
<input id="Ema" type="text" class="form-control" placeholder="Email"></input>
</div>
<button onclick="submitforticket()" type="button" class="btn btn-info">Go to payment</button><p style="color:red;" id="error"></p>
</form>
</div>
</center>
<center><br/><br/> <div id="qrcode" style="width: 340px;"></div><br/><br/>
<div style="width:55%;word-wrap: break-word;" id="qrcodetxt"></div> <br/></center>
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
</div>
</body>
<script>
function postAjax(url, data, thekey, success) {
var params =
typeof data == 'string'
? data
: Object.keys(data)
.map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
})
.join('&')
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('POST', url)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(params)
return xhr
}
function getAjax(url, thekey, success) {
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
return xhr
}
function submitforticket(){
nam = document.getElementById('Nam').value
ema = document.getElementById('Ema').value
postAjax(
"{{ url_for('events.api_getticket') }}?ema=" + ema,
JSON.stringify({"unireg": "{{wave }}", "name": nam}),
"filla",
function(data) {
theinvoice = JSON.parse(data).pay_req
thehash = JSON.parse(data).payment_hash
new QRCode(document.getElementById('qrcode'), {
text: theinvoice,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
document.getElementById('theform').innerHTML = ""
document.getElementById("qrcode").style.backgroundColor = "white";
document.getElementById("qrcode").style.padding = "20px";
document.getElementById('qrcodetxt').innerHTML = theinvoice + "<br/><br/>"
var refreshId = setInterval(function(){
getAjax('/api/v1/invoice/' + thehash, "{{wave}}", function(datab) {
console.log(JSON.parse(datab).PAID)
if (JSON.parse(datab).PAID == 'TRUE') {
location.replace("{{ url_for('events.ticket') }}?hash="+thehash + "&unireg={{wave}}")
clearInterval(refreshId)
}
})}, 3000);
})
}
</script>
</html>
@@ -0,0 +1,480 @@
<!-- @format -->
{% extends "legacy.html" %} {% block messages %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-bell-o"></i>
<span class="label label-danger">!</span>
</a>
<ul class="dropdown-menu">
<li class="header"><b>Instant wallet, bookmark to save</b></li>
<li></li>
</ul>
{% endblock %}
{% block menuitems %}
<li class="treeview">
<a href="#">
<i class="fa fa-bitcoin"></i> <span>Wallets</span>
<i class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
{% for w in user_wallets %}
<li>
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"><i class="fa fa-bolt"></i> {{ w.name }}</a>
</li>
{% endfor %}
<li><a onclick="sidebarmake()">Add a wallet +</a></li>
<div id="sidebarmake"></div>
</ul>
</li>
<li class="active treeview">
<a href="#">
<i class="fa fa-th"></i> <span>Extensions</span>
<i class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
{% for extension in EXTENSIONS %}
{% if extension.code in user_ext %}
<li>
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"><i class="fa fa-plus"></i> {{ extension.name }}</a>
</li>
{% endif %}
{% endfor %}
<li>
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
</ul>
</li>
{% endblock %}
{% block body %}
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Events
<small>bitcoin tickets</small>
</h1>
<ol class="breadcrumb">
<li>
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
</li>
<li>
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
</li>
<li>
<i class="active" class="fa fa-dashboard">Lightning tickets</i>
</li>
</ol>
<br /><br />
</section>
<style>
.datepicker-days{
background-color: #1f2234;
}
</style>
<!-- Main content -->
<section class="content">
<!-- Small boxes (Stat box) -->
<div class="row">
<div class="col-md-6">
<!-- general form elements -->
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title"> Make a ticket wave</h3>
</div><!-- /.box-header -->
<!-- form start -->
<form role="form">
<div class="box-body">
<div class="form-group">
<label for="exampleInputEmail1">Ticket title</label>
<input id="tit" type="text" pattern="^[A-Za-z]+$" class="form-control" >
</div>
<div class="form-group">
<label>Description of event</label>
<textarea id="descr" class="form-control" rows="2"></textarea>
</div>
<!-- select -->
<div class="form-group">
<label>Select a wallet</label>
<select id="wal" class="form-control">
<option></option>
{% for w in user_wallets %}
<option>{{w.name}}-{{w.id}}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="nooftickets">No. of tickets</label>
<input id="notickets" type="number" class="form-control" placeholder="10" max="86400"></input>
</div>
<div class="form-group">
<label>Close date:</label>
<div class="form-group">
<input type="text" class="form-control" id="datepicker"></input>
</div>
<div class="form-group">
<label for="prpertick">Price per ticket</label>
<input id="prtickets" type="number" class="form-control" placeholder="10"></input>
</div>
</div><!-- /.box-body -->
<div class="box-footer">
<button onclick="postev()" type="button" class="btn btn-info">Create Wave</button><p style="color:red;" id="error"></p>
</div>
</form>
</div></div></div>
<div class="col-md-6">
<!-- general form elements -->
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title">Select a link</h3>
</div><!-- /.box-header -->
<form role="form">
<div class="box-body">
<div class="form-group">
<select class="form-control" id="waveselect" onchange="drawwithdraw()">
<option value="none" selected>
Select an Option
</option>
{% for w in user_ev %}
<option id="{{w.uni}}" value="{{w.tit}}-{{w.unireg}}-{{w.uni}}">{{w.tit}}-{{w.unireg}}-{{w.uni}}</option>
{% endfor %}
</select>
</div>
<center> <br/><div id="qrcode" style="width:340px" ></div><br/><div style="width:75%;word-wrap: break-word;" id="qrcodetxt" ></div></center>
</div>
</form>
</div><!-- /.box -->
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="box">
<div class="box-header">
<h3 class="box-title">Ticket Waves<b id="withdraws"></b></h3>
</div>
<!-- /.box-header -->
<div class="box-body no-padding">
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
<tr>
<th>Title</th>
<th style="width:15%">Amt</th>
<th style="width:15%">Sold</th>
<th style="width:15%">Closing</th>
<th style="width:15%">Price</th>
<th style="width:15%">Wallet</th>
<th style="width:10%">Edit</th>
<th style="width:10%">Del</th>
</tr>
<tbody id="ticketwaves"></tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</div>
<div id="editlink"></div>
<!-- /.content -->
</section>
<script>
//Date picker
$('#datepicker').datepicker({
autoclose: true
})
window.user = {{ user | tojson | safe }}
window.user_wallets = {{ user_wallets | tojson | safe }}
window.user_ext = {{ user_ext | tojson | safe }}
window.user_ev = {{ user_ev | tojson | safe }}
const user_ev = window.user_ev
console.log(user_ev)
function drawChart(user_ev) {
var transactionsHTML = ''
for (var i = 0; i < user_ev.length; i++) {
var ev = user_ev[i]
transactionsHTML =
"<tr><td style='width: 50%'>" +
ev.tit +
'</td><td>' +
ev.notickets +
'</td><td>' +
ev.sold +
'</td><td>' +
ev.cldate +
'</td><td>' +
ev.prtick +
'</td><td>' +
"<a href='{{ url_for('wallet') }}?usr="+ ev.usr +"'>" + ev.wal.substring(0, 4) + "...</a>" +
'</td><td>' +
"<i onclick='editlink("+ i +")'' class='fa fa-edit'></i>" +
'</td><td>' +
"<b><a style='color:red;' href='" + "{{ url_for('events.index') }}?del=" + ev.uni + "&usr=" + ev.usr +"'>" + "<i class='fa fa-trash'></i>" + "</a></b>" +
'</td></tr>' +
transactionsHTML
document.getElementById('ticketwaves').innerHTML = transactionsHTML
}
}
if (user_ev.length) {
drawChart(user_ev)
}
function postev(){
wal = document.getElementById('wal').value
tit = document.getElementById('tit').value
cldate = document.getElementById('datepicker').value
notickets = document.getElementById('notickets').value
prtickets = document.getElementById('prtickets').value
descr = document.getElementById('descr').value
if (tit == "") {
document.getElementById("error").innerHTML = "Only use letters in title"
return amt
}
if (wal == "") {
document.getElementById("error").innerHTML = "No wallet selected"
return amt
}
if (cldate == "") {
document.getElementById("error").innerHTML = "No date selected"
return amt
}
if (isNaN(notickets) || notickets < 1) {
document.getElementById("error").innerHTML = "Must be more than 1"
return amt
}
if (isNaN(prtickets) || prtickets < 10) {
document.getElementById("error").innerHTML = "Must be higher 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": notickets,"cldate": cldate, "prtickets": prtickets, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
}
function editlink(evnum){
evdetails = user_ev[evnum]
console.log(evdetails.descr)
wallpick = ""
checkbox = ""
if (evdetails.uniq == 1){
checkbox = "checked"}
document.getElementById('editlink').innerHTML = "<div class='row'>"+
"<div class='col-md-6'>"+
" <!-- general form elements -->"+
"<div class='box box-primary' style='min-height: 300px;'>"+
"<div class='box-header'>"+
"<h3 class='box-title'> Edit: <i id='unid'>" + evdetails.tit + "-" + evdetails.uni + "-" + evdetails.unireg + "</i> </h3>"+
"<div class='box-tools pull-right'>" +
"<button class='btn btn-box-tool' data-widget='remove'><i class='fa fa-times'></i></button>" +
"</div>" +
" </div><!-- /.box-header -->"+
" <!-- form start -->"+
"<form role='form'>"+
"<div class='box-body'>"+
"<div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Link title</label>"+
"<input id='edittit' type='text' class='form-control' value='"+
evdetails.tit +
"'></input> </div>"+
" </div>"+
"<div class='col-sm-1 col-md-8'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Description of event</label>"+
"<textarea id='editdescr' type='textarea' rows='1' class='form-control'>"+evdetails.descr+"</textarea> </div>"+
" </div>"+
" <div class='col-sm-4 col-md-4'>"+
" <!-- select -->"+
" <div class='form-group'>"+
" <label>Select a wallet</label>"+
"<select id='editwal' class='form-control'>"+
" <option>" + evdetails.walnme + "-" + evdetails.wal + "</option>"+
" {% for w in user_wallets %}"+
" <option>{{w.name}}-{{w.id}}</option>"+
" {% endfor %}"+
" </select>"+
" </div>"+
" </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
" <label for='exampleInputPassword1'>No of tickets:</label>"+
" <input id='editnooftickets' type='number' class='form-control' placeholder='0' max='86400' value='"+
evdetails.notickets +
"'></input>"+
"</div> </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Price per ticket:</label>"+
" <input id='editprtick' type='number' class='form-control' placeholder='1' value='"+
evdetails.prtick +
"'></input>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
" <div class='input-group date'>"+
" <label for='exampleInputEmail1'>Close date:</label>"+
" <input id='datepicker2' type='text' class='form-control' placeholder='1' value='"+
evdetails.cldate +
"'></input>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
"</div><!-- /.box-body -->"+
" </div><br/>"+
" <div class='box-footer'>"+
" <div class='col-sm-3 col-md-4'>"+
"<button onclick='editlinkcont()' type='button' style='margin: 24px;' class='btn btn-info'>Edit link(s)</button>"+
"</div>"+
"<p style='color:red;' id='error2'>.</p>"+
" </div></form></div><!-- /.box --></div></div>"
}
//Date picker
$('#datepicker2').datepicker({
autoclose: true
})
function editlinkcont(){
unid = document.getElementById('unid').innerHTML
wal = document.getElementById('editwal').value
tit = document.getElementById('edittit').value
nooftickets = document.getElementById('editnooftickets').value
prtick = document.getElementById('editprtick').value
cldate = document.getElementById('datepicker2').value
descr = document.getElementById('editdescr').value
uni = unid.split("-")[1]
if (tit == "") {
document.getElementById("error2").innerHTML = "Only use letters in title"
return amt
}
if (wal == "") {
document.getElementById("error2").innerHTML = "No wallet selected"
return amt
}
if (isNaN(nooftickets) || nooftickets < 1 || nooftickets > 1000000) {
document.getElementById("error2").innerHTML = "No. of tickets must be between 1 - 1000000"
return amt
}
if (isNaN(prtick) || prtick < 10 ) {
document.getElementById("error2").innerHTML = "Ticket pricket must be higher than 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": nooftickets,"cldate": cldate, "prtickets": prtick, "id": unid, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
}
//draws withdraw QR code
function drawwithdraw() {
document.getElementById("qrcode").innerHTML = "";
walname = document.getElementById("waveselect").value
thewave = walname.split("-");
console.log(window.location.hostname + "-" + thewave[1])
toencode = "/events/wave/" + thewave[1]
toreg = "/events/registration/" + thewave[2]
new QRCode(document.getElementById('qrcode'), {
text: toencode,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
document.getElementById('qrcodetxt').innerHTML = "<a href='" + toencode + "'><h2>Payment link</a> , " +
"<a href='" + toreg + "'>Registration</h2></a>"
document.getElementById("qrcode").style.backgroundColor = "white";
document.getElementById("qrcode").style.padding = "20px";
}
</script>
</div>
{% endblock %}
@@ -0,0 +1,690 @@
<!-- @format -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>LNBits Wallet</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport"
/>
<!-- Bootstrap 3.3.2 -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
/>
<!-- FontAwesome 4.3.0 -->
<link
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Ionicons 2.0.0 -->
<link
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Theme style -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
/>
<!-- AdminLTE Skins. Choose a skin from the css/skins
folder instead of downloading all of them to reduce the load. -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
/>
<!-- Morris chart -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
/>
<!-- jvectormap -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
/>
<!-- bootstrap wysihtml5 - text editor -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
/>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<style>
.small-box > .small-box-footer {
text-align: left;
padding-left: 10px;
}
#loadingMessage {
text-align: center;
padding: 40px;
background-color: #eee;
}
#canvas {
width: 100%;
}
#output {
margin-top: 20px;
background: #eee;
padding: 10px;
padding-bottom: 0;
}
#output div {
padding-bottom: 10px;
word-wrap: break-word;
}
#noQRFound {
text-align: center;
}
</style>
<!-- jQuery 2.1.3 -->
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
<!-- jQuery UI 1.11.2 -->
<script
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
type="text/javascript"
></script>
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
<script>
$.widget.bridge('uibutton', $.ui.button)
</script>
<!-- Bootstrap 3.3.2 JS -->
<script
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
type="text/javascript"
></script>
<!-- Morris.js charts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
type="text/javascript"
></script>
<!-- Sparkline -->
<script
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
type="text/javascript"
></script>
<!-- jvectormap -->
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
type="text/javascript"
></script>
<!-- jQuery Knob Chart -->
<script
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
type="text/javascript"
></script>
<!-- Bootstrap WYSIHTML5 -->
<script
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
type="text/javascript"
></script>
<!-- Slimscroll -->
<script
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
type="text/javascript"
></script>
<!-- FastClick -->
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
<!-- AdminLTE App -->
<script
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
<script
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE for demo purposes -->
<script
src="{{ url_for('static', filename='dist/js/demo.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
type="text/javascript"
></script>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
type="text/javascript"
></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background:
#1f2234;
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color:
#3e355a;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
<!-- Header Navbar: style can be found in header.less -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a
href="#"
class="sidebar-toggle"
data-toggle="offcanvas"
role="button"
>
<span class="sr-only">Toggle navigation</span>
</a>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu">
{% block messages %}{% endblock %}
</li>
</ul>
</div>
</nav>
</header>
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;">
<!-- Sidebar user panel -->
</section>
<!-- /.sidebar -->
</aside>
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
LNBits Events
<small>Lightning powered tickets</small>
</h1>
</section>
<!-- Main content -->
<section class="content"><br/><br/>
<center><h1 style="font-size:500%">{{ user_ev[0][6] }}</h1></center>
<br/><br/><br/>
<div class='modal fade sends' tabindex='-1' role='dialog' aria-labelledby='myLargeModalLabel' aria-hidden='true'>
<div class='modal-dialog' >
<div id='scantickets' style='padding: 0 10px 0 10px;'>
</div></div></div>
<center>
<button
onclick="scanQRsend()"
class="btn btn-block btn-primary btn-lg"
data-toggle="modal"
data-target=".sends"
style="width:300px;"
>
Scan ticket
</button>
</center>
<div id="scantickets"></div>
<br/><br/><br/>
<div id="qrcodetxt"></div> <br/></center>
<br/><br/><br/>
<center>
<div class="row" style="width:80%;margin-top:80px">
<style>
.ema, button:focus .txt {
display: none;
}
button:focus .ema {
display: block;
}
</style>
<div class="box">
<div class="box-header">
<h3 class="box-title">Attendees<b id="withdraws"></b></h3>
</div>
<!-- /.box-header -->
<div class="box-body no-padding">
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
<tr>
<th style="width:20%">Name</th>
<th style="width:20%">Email</th>
<th style="width:50%">Ticket</th>
<th style="width:10%">Registered</th>
</tr>
<tbody id="ticketwaves"></tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</center>
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
</div>
</body>
<script>
window.user_ev = {{ user_ev | tojson | safe }}
window.user_ev_sold = {{ user_ev_sold | tojson | safe }}
console.log(user_ev)
console.log(user_ev_sold)
function drawChart(user_ev_sold) {
var transactionsHTML = ''
for (var i = 0; i < user_ev_sold.length; i++) {
var ev = user_ev_sold[i]
transactionsHTML =
"<tr><td>" +
ev.name +
'</td><td>' +
'<button style="background-color: #333646;padding: 0;border: none;background: none;" class="lost"><span class="txt">xxxxxx</span>' +
'<span class="ema">' + ev.email + '</span></button>'+
'</td><td>' +
ev.hash +
'</td><td>' +
ev.reg +
'</td></tr>' +
transactionsHTML
document.getElementById('ticketwaves').innerHTML = transactionsHTML
}
}
if (user_ev_sold.length) {
drawChart(user_ev_sold)
}
function postAjax(url, data, thekey, success) {
var params =
typeof data == 'string'
? data
: Object.keys(data)
.map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
})
.join('&')
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('POST', url)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(params)
return xhr
}
function getAjax(url, thekey, success) {
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
return xhr
}
function scanQRsend() {
document.getElementById('scantickets').innerHTML =
"<div class='modal-content'>"+
"<br/><div id='registered'><div id='loadingMessage'>🎥 Unable to access video stream (please make sure you have a webcam enabled)</div>" +
"<canvas id='canvas' hidden></canvas><div id='output' hidden><div id='outputMessage'></div>" +
"<br/><span id='outputData'></span></div></div><div class='modal-footer'>"+
"<button type='submit' class='btn btn-primary' onclick='cancelsend()'>Cancel</button><br/><br/></div>"
var video = document.createElement('video')
var canvasElement = document.getElementById('canvas')
var canvas = canvasElement.getContext('2d')
var loadingMessage = document.getElementById('loadingMessage')
var outputContainer = document.getElementById('output')
var outputMessage = document.getElementById('outputMessage')
var outputData = document.getElementById('outputData')
// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices
.getUserMedia({video: {facingMode: 'environment'}})
.then(function(stream) {
video.srcObject = stream
video.setAttribute('playsinline', true) // required to tell iOS safari we don't want fullscreen
video.play()
requestAnimationFrame(tick)
})
function tick() {
loadingMessage.innerText = '⌛ Loading video...'
if (video.readyState === video.HAVE_ENOUGH_DATA) {
loadingMessage.hidden = true
canvasElement.hidden = false
outputContainer.hidden = false
canvasElement.height = video.videoHeight
canvasElement.width = video.videoWidth
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height)
var imageData = canvas.getImageData(
0,
0,
canvasElement.width,
canvasElement.height
)
var code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert'
})
if (code) {
thehash = code.data
document.getElementById('registered').innerHTML = "<h1 style='color:green;font-size:300%;'>Registered!</h1>"
outputMessage.hidden = true
outputData.parentElement.hidden = false
outputData.innerText = JSON.stringify(code.data)
getAjax("{{ url_for('events.api_checkticket') }}?thehash=" + thehash, "filla", function(datab) {
if (JSON.parse(datab).status == 'TRUE') {
location.reload()
}
})
} else {
outputMessage.hidden = false
outputData.parentElement.hidden = true
}
}
requestAnimationFrame(tick)
}
}
function cancelsend() {
location.reload();
}
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = value;
});
return vars;
}
var name = getUrlVars()["name"];
var thehash = getUrlVars()["thehash"];
console.log(thehash)
if(thehash != null){
document.getElementById('qrcodetxt').innerHTML = "<center><h1>" + name + " is registered!</h1></center>"
}
</script>
</html>
@@ -0,0 +1,474 @@
<!-- @format -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>LNBits Wallet</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport"
/>
<!-- Bootstrap 3.3.2 -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
/>
<!-- FontAwesome 4.3.0 -->
<link
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Ionicons 2.0.0 -->
<link
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Theme style -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
/>
<!-- AdminLTE Skins. Choose a skin from the css/skins
folder instead of downloading all of them to reduce the load. -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
/>
<!-- Morris chart -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
/>
<!-- jvectormap -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
/>
<!-- bootstrap wysihtml5 - text editor -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
/>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<style>
.small-box > .small-box-footer {
text-align: left;
padding-left: 10px;
}
#loadingMessage {
text-align: center;
padding: 40px;
background-color: #eee;
}
#canvas {
width: 100%;
}
#output {
margin-top: 20px;
background: #eee;
padding: 10px;
padding-bottom: 0;
}
#output div {
padding-bottom: 10px;
word-wrap: break-word;
}
#noQRFound {
text-align: center;
}
</style>
<!-- jQuery 2.1.3 -->
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
<!-- jQuery UI 1.11.2 -->
<script
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
type="text/javascript"
></script>
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
<script>
$.widget.bridge('uibutton', $.ui.button)
</script>
<!-- Bootstrap 3.3.2 JS -->
<script
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
type="text/javascript"
></script>
<!-- Morris.js charts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
type="text/javascript"
></script>
<!-- Sparkline -->
<script
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
type="text/javascript"
></script>
<!-- jvectormap -->
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
type="text/javascript"
></script>
<!-- jQuery Knob Chart -->
<script
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
type="text/javascript"
></script>
<!-- Bootstrap WYSIHTML5 -->
<script
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
type="text/javascript"
></script>
<!-- Slimscroll -->
<script
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
type="text/javascript"
></script>
<!-- FastClick -->
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
<!-- AdminLTE App -->
<script
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
<script
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE for demo purposes -->
<script
src="{{ url_for('static', filename='dist/js/demo.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
type="text/javascript"
></script>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
type="text/javascript"
></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background:
#1f2234;
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color:
#3e355a;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
<!-- Header Navbar: style can be found in header.less -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a
href="#"
class="sidebar-toggle"
data-toggle="offcanvas"
role="button"
>
<span class="sr-only">Toggle navigation</span>
</a>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu">
{% block messages %}{% endblock %}
</li>
</ul>
</div>
</nav>
</header>
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;">
</section>
<!-- /.sidebar -->
</aside>
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
LNBits Events
<small>Lightning powered tickets</small>
</h1>
</section>
<!-- Main content -->
<section class="content"><br/><br/>
<center><h2 style="width:70%;font-size:400%" >Bookmark/Screenshot this page. <br/>It is your ticket!</h2></center>
<center> <div style="width:340px;background-color:white;padding:20px"id="qrcode"></div></center>
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
</div>
</body>
<script>
new QRCode(document.getElementById('qrcode'), {
text: "{{ticket}}",
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
</script>
</html>
+174
View File
@@ -0,0 +1,174 @@
import uuid
from flask import jsonify, render_template, request, redirect, url_for
from datetime import datetime
from lnbits.db import open_db, open_ext_db
from lnbits.extensions.events import events_ext
@events_ext.route("/")
def index():
"""Main events link page."""
usr = request.args.get("usr")
if usr:
if not len(usr) > 20:
return redirect(url_for("home"))
# Get all the data
with open_db() as db:
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
user_ext = [v[0] for v in user_ext]
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
# If del is selected by user from events page, the event link is to be deleted
evdel = request.args.get("del")
if evdel:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (evdel,))
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (evdel,))
if user_ev[0][9] > 0:
events_ext_db.execute("DELETE FROM eventssold WHERE uni = ?", (user_ev[0][12],))
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
print(user_ext)
return render_template(
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
)
@events_ext.route("/create", methods=["GET", "POST"])
def create():
"""."""
data = request.json
tit = data["tit"]
wal = data["wal"]
cldate = data["cldate"]
notickets = data["notickets"]
prtick = data["prtickets"]
usr = data["usr"]
descr = data["descr"]
wall = wal.split("-")
# Form validation
if (
not tit.replace(" ", "").isalnum()
or wal == ""
or int(notickets) < 0
or int(prtick) < 0
):
return jsonify({"ERROR": "FORM ERROR"}), 401
# If id that means its a link being edited, delete the record first
if "id" in data:
unid = data["id"].split("-")
uni = unid[1]
unireg = unid[2]
with open_ext_db("events") as events_ext_db:
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (unid[1],))
else:
uni = uuid.uuid4().hex
unireg = uuid.uuid4().hex
with open_db() as dbb:
user_wallets = dbb.fetchall("SELECT * FROM wallets WHERE user = ? AND id = ?", (usr, wall[1],))
if not user_wallets:
return jsonify({"ERROR": "NO WALLET USER"}), 401
with open_db() as db:
user_ext = db.fetchall("SELECT * FROM extensions WHERE user = ?", (usr,))
user_ext = [v[0] for v in user_ext]
# Add to DB
with open_ext_db("events") as events_ext_db:
events_ext_db.execute(
"""
INSERT OR IGNORE INTO events
(usr, wal, walnme, walinvkey, uni, tit, cldate, notickets, prtick, descr, unireg)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
usr,
wall[1],
user_wallets[0][1],
user_wallets[0][4],
uni,
tit,
cldate,
notickets,
prtick,
descr,
unireg,
),
)
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
if not user_ev:
return jsonify({"ERROR": "NO WALLET USER"}), 401
return render_template(
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
)
@events_ext.route("/wave/<wave>/", methods=["GET", "POST"])
def wave(wave):
"""."""
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (wave,))
if not user_ev:
return jsonify({"ERROR": "NO RECORD"}), 401
return render_template(
"events/display.html", wave=wave, nme=user_ev[0][6], descr=user_ev[0][11]
)
@events_ext.route("/registration/<wave>", methods=["GET", "POST"])
def registration(wave):
"""."""
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (wave,))
user_ev_sold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE uni = ? AND paid = 1", (user_ev[0][12],))
if not user_ev:
return jsonify({"ERROR": "NO RECORD"}), 401
return render_template(
"events/registration.html", user_ev=user_ev, user_ev_sold=user_ev_sold
)
@events_ext.route("/ticket/", methods=["GET"])
def ticket():
"""."""
thehash = request.args.get("hash")
unireg = request.args.get("unireg")
#Double check the payment has cleared
with open_db() as db:
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (thehash,))
if not payment:
return jsonify({"status": "ERROR", "reason":"NO RECORD OF PAYMENT"}), 400
if payment[0][4] == 1:
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 400
#Update databases
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
updatesold = user_ev[0][9] + 1
events_ext_db.execute("UPDATE events SET sold = ? WHERE unireg = ?", (updatesold, unireg,))
events_ext_db.execute("UPDATE eventssold SET paid = 1 WHERE hash = ?", (thehash,))
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
if not eventssold:
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
return render_template(
"events/ticket.html", name=eventssold[0][3], ticket=thehash
)
+65
View File
@@ -0,0 +1,65 @@
import uuid
import json
import requests
from flask import jsonify, request, url_for
from datetime import datetime
from lnbits.db import open_db, open_ext_db
from lnbits.extensions.events import events_ext
@events_ext.route("/api/v1/getticket/", methods=["GET","POST"])
def api_getticket():
"""."""
data = request.json
unireg = data["unireg"]
name = data["name"]
email = request.args.get("ema")
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
header = {"Content-Type": "application/json", "X-Api-Key": user_ev[0][4]}
data = {"value": str(user_ev[0][10]), "memo": user_ev[0][6]}
print(url_for("api_invoices", _external=True))
r = requests.post(url=url_for("api_invoices", _external=True), headers=header, data=json.dumps(data))
r_json = r.json()
if "ERROR" in r_json:
return jsonify({"status": "ERROR", "reason": r_json["ERROR"]}), 400
events_ext_db.execute(
"""
INSERT OR IGNORE INTO eventssold
(uni, email, name, hash)
VALUES (?, ?, ?, ?)
""",
(
unireg,
email,
name,
r_json["payment_hash"]
),
)
return jsonify({"status": "TRUE", "pay_req": r_json["pay_req"], "payment_hash": r_json["payment_hash"]}), 200
@events_ext.route("/api/v1/checkticket/", methods=["GET"])
def api_checkticket():
"""."""
thehash = request.args.get("thehash")
#Check databases
with open_ext_db("events") as events_ext_db:
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
if not eventssold:
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
if eventssold[0][4] == 0:
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 200
with open_ext_db("events") as events_ext_db:
events_ext_db.execute("UPDATE eventssold SET reg = 1 WHERE hash = ?", (thehash,))
return jsonify({"status": "TRUE", "name": eventssold[0][3]}), 200
+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 flask import Blueprint
example_ext: Blueprint = Blueprint("example", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Build your own!",
"short_description": "Join us, make an extension",
"icon": "info",
"contributors": ["github_username"]
}
+5
View File
@@ -0,0 +1,5 @@
from lnbits.db import open_ext_db
def migrate():
print("pending")
@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %}
{% block page %}
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5>
<q-list>
<q-item v-for="tool in tools" :key="tool.name" tag="a" :href="tool.url" target="_blank">
{% raw %} <!-- with raw Flask won't try to interpret the Vue moustaches -->
<q-item-section>
<q-item-label>{{ tool.name }}</q-item-label>
<q-item-label caption>{{ tool.language }}</q-item-label>
</q-item-section>
{% endraw %}
</q-item>
</q-list>
<q-separator class="q-my-lg"></q-separator>
<p>A magical "g" is always available, with info about the user, wallets and extensions:</p>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
</q-card-section>
</q-card>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tools: []
};
},
created: function () {
var self = this;
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data;
});
}
});
</script>
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
from flask import g, render_template
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.example import example_ext
@example_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def index():
return render_template("example/index.html", user=g.user)
+37
View File
@@ -0,0 +1,37 @@
# views_api.py is for you API endpoints that could be hit by another service
# add your dependencies here
# import json
# import requests
from flask import jsonify
from lnbits.helpers import Status
from lnbits.extensions.example import example_ext
# add your endpoints here
@example_ext.route("/api/v1/tools", methods=["GET"])
def api_example():
"""Try to add descriptions for others."""
tools = [
{
"name": "Flask",
"url": "https://flask.palletsprojects.com/",
"language": "Python",
},
{
"name": "Vue.js",
"url": "https://vuejs.org/",
"language": "JavaScript",
},
{
"name": "Quasar Framework",
"url": "https://quasar.dev/",
"language": "JavaScript",
}
]
return jsonify(tools), Status.OK
+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 flask import Blueprint
paywall_ext: Blueprint = Blueprint("paywall", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Paywall",
"short_description": "Create paywalls for content",
"icon": "vpn_lock",
"contributors": ["eillarra"]
}
+43
View File
@@ -0,0 +1,43 @@
from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash
from .models import Paywall
def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall:
with open_ext_db("paywall") as db:
paywall_id = urlsafe_short_hash()
db.execute(
"""
INSERT INTO paywalls (id, wallet, secret, url, memo, amount)
VALUES (?, ?, ?, ?, ?, ?)
""",
(paywall_id, wallet_id, urlsafe_short_hash(), url, memo, amount),
)
return get_paywall(paywall_id)
def get_paywall(paywall_id: str) -> Optional[Paywall]:
with open_ext_db("paywall") as db:
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
return Paywall(**row) if row else None
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("paywall") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
return [Paywall(**row) for row in rows]
def delete_paywall(paywall_id: str) -> None:
with open_ext_db("paywall") as db:
db.execute("DELETE FROM paywalls WHERE id = ?", (paywall_id,))
+25
View File
@@ -0,0 +1,25 @@
from lnbits.db import open_ext_db
def m001_initial(db):
"""
Initial paywalls table.
"""
db.execute(
"""
CREATE TABLE IF NOT EXISTS paywalls (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
secret TEXT NOT NULL,
url TEXT NOT NULL,
memo TEXT NOT NULL,
amount INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
def migrate():
with open_ext_db("paywall") as db:
m001_initial(db)
+11
View File
@@ -0,0 +1,11 @@
from typing import NamedTuple
class Paywall(NamedTuple):
id: str
wallet: str
secret: str
url: str
memo: str
amount: int
time: int
@@ -0,0 +1,28 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="List paywalls">
<q-card>
<q-card-section>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a paywall">
<q-card>
<q-card-section>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Delete a paywall" class="q-pb-md">
<q-card>
<q-card-section>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
@@ -0,0 +1,107 @@
{% extends "public.html" %}
{% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h5 class="text-subtitle1 q-my-none">{{ paywall.memo }}</h5>
<strong class="text-purple">Price: <lnbits-fsat :amount="{{ paywall.amount }}"></lnbits-fsat> sat</strong>
<q-separator class="q-my-lg"></q-separator>
<div v-if="paymentReq">
<a :href="'lightning:' + paymentReq">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode :value="paymentReq" :options="{width: 800}" class="rounded-borders"></qrcode>
</q-responsive>
</a>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(paymentReq)">Copy invoice</q-btn>
</div>
</div>
<div v-if="redirectUrl">
<p>You can access the URL behind this paywall:<br>
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong></p>
<div class="row q-mt-lg">
<q-btn outline color="grey" type="a" :href="redirectUrl">Open URL</q-btn>
</div>
</div>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits paywall</h6>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode);
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null
};
},
methods: {
getInvoice: function () {
var self = this;
axios.get(
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice'
).then(function (response) {
self.paymentReq = response.data.payment_request;
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
});
paymentChecker = setInterval(function () {
axios.post(
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
{checking_id: response.data.checking_id}
).then(function (res) {
if (res.data.paid) {
clearInterval(paymentChecker);
dismissMsg();
self.redirectUrl = res.data.url;
self.$q.localStorage.set('lnbits.paywall.{{ paywall.id }}', res.data.url);
self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
});
}
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
}, 2000);
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
}
},
created: function () {
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}');
if (url) {
this.redirectUrl = url;
} else {
this.getInvoice();
};
}
});
</script>
{% endblock %}

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