Merge branch 'main' into diagon-alley

This commit is contained in:
Ben Arc 2022-02-17 20:32:48 +00:00
commit 39bec9f631
20 changed files with 368 additions and 164 deletions

View file

@ -55,11 +55,15 @@ LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009 LND_GRPC_PORT=11009
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# To use an AES-encrypted macaroon, set
# LND_GRPC_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LndRestWallet # LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/ LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# To use an AES-encrypted macaroon, set
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LNPayWallet # LNPayWallet
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/

View file

@ -30,6 +30,7 @@ sse-starlette = "*"
jinja2 = "3.0.1" jinja2 = "3.0.1"
pyngrok = "*" pyngrok = "*"
secp256k1 = "*" secp256k1 = "*"
pycryptodomex = "*"
[dev-packages] [dev-packages]
black = "==20.8b1" black = "==20.8b1"

164
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "bd78144379115a1566549f5f2d7dba7a96539fb5893b3999e61bc13c6847827e" "sha256": "3e19364434fd2db3748162ccc1f3b6bddcf7a382473069d15cee6eda5e07eef1"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -146,11 +146,11 @@
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==2.0.11" "version": "==2.0.12"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@ -201,11 +201,11 @@
}, },
"httpcore": { "httpcore": {
"hashes": [ "hashes": [
"sha256:2621ee769d0236574df51b305c5f4c69ca8f0c7b215221ad247b1ee42a9a9de1", "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade",
"sha256:435ab519628a6e2393f67812dea3ca5c6ad23b457412cd119295d9f906d96a2b" "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.14.5" "version": "==0.14.7"
}, },
"httptools": { "httptools": {
"hashes": [ "hashes": [
@ -427,6 +427,39 @@
], ],
"version": "==2.21" "version": "==2.21"
}, },
"pycryptodomex": {
"hashes": [
"sha256:1ca8e1b4c62038bb2da55451385246f51f412c5f5eabd64812c01766a5989b4a",
"sha256:298c00ea41a81a491d5b244d295d18369e5aac4b61b77b2de5b249ca61cd6659",
"sha256:2aa887683eee493e015545bd69d3d21ac8d5ad582674ec98f4af84511e353e45",
"sha256:2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2",
"sha256:3da13c2535b7aea94cc2a6d1b1b37746814c74b6e80790daddd55ca5c120a489",
"sha256:406ec8cfe0c098fadb18d597dc2ee6de4428d640c0ccafa453f3d9b2e58d29e2",
"sha256:4d0db8df9ffae36f416897ad184608d9d7a8c2b46c4612c6bc759b26c073f750",
"sha256:530756d2faa40af4c1f74123e1d889bd07feae45bac2fd32f259a35f7aa74151",
"sha256:77931df40bb5ce5e13f4de2bfc982b2ddc0198971fbd947776c8bb5050896eb2",
"sha256:797a36bd1f69df9e2798e33edb4bd04e5a30478efc08f9428c087f17f65a7045",
"sha256:8085bd0ad2034352eee4d4f3e2da985c2749cb7344b939f4d95ead38c2520859",
"sha256:8536bc08d130cae6dcba1ea689f2913dfd332d06113904d171f2f56da6228e89",
"sha256:a4d412eba5679ede84b41dbe48b1bed8f33131ab9db06c238a235334733acc5e",
"sha256:aebecde2adc4a6847094d3bd6a8a9538ef3438a5ea84ac1983fcb167db614461",
"sha256:b276cc4deb4a80f9dfd47a41ebb464b1fe91efd8b1b8620cf5ccf8b824b850d6",
"sha256:b5a185ae79f899b01ca49f365bdf15a45d78d9856f09b0de1a41b92afce1a07f",
"sha256:c4d8977ccda886d88dc3ca789de2f1adc714df912ff3934b3d0a3f3d777deafb",
"sha256:c5dd3ffa663c982d7f1be9eb494a8924f6d40e2e2f7d1d27384cfab1b2ac0662",
"sha256:ca88f2f7020002638276439a01ffbb0355634907d1aa5ca91f3dc0c2e44e8f3b",
"sha256:d2cce1c82a7845d7e2e8a0956c6b7ed3f1661c9acf18eb120fc71e098ab5c6fe",
"sha256:d709572d64825d8d59ea112e11cc7faf6007f294e9951324b7574af4251e4de8",
"sha256:da8db8374295fb532b4b0c467e66800ef17d100e4d5faa2bbbd6df35502da125",
"sha256:e36c7e3b5382cd5669cf199c4a04a0279a43b2a3bdd77627e9b89778ac9ec08c",
"sha256:e95a4a6c54d27a84a4624d2af8bb9ee178111604653194ca6880c98dcad92f48",
"sha256:ee835def05622e0c8b1435a906491760a43d0c462f065ec9143ec4b8d79f8bff",
"sha256:f75009715dcf4a3d680c2338ab19dac5498f8121173a929872950f4fb3a48fbf",
"sha256:f8524b8bc89470cec7ac51734907818d3620fb1637f8f8b542d650ebec42a126"
],
"index": "pypi",
"version": "==3.14.1"
},
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
"sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3", "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3",
@ -684,22 +717,22 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.0.1" "version": "==4.1.1"
}, },
"uvicorn": { "uvicorn": {
"extras": [ "extras": [
"standard" "standard"
], ],
"hashes": [ "hashes": [
"sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73", "sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011",
"sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41" "sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.17.1" "version": "==0.17.4"
}, },
"uvloop": { "uvloop": {
"hashes": [ "hashes": [
@ -819,53 +852,50 @@
"toml" "toml"
], ],
"hashes": [ "hashes": [
"sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69", "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c",
"sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6", "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0",
"sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749", "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554",
"sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0", "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb",
"sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc", "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2",
"sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e", "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b",
"sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab", "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8",
"sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260", "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba",
"sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5", "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734",
"sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2", "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2",
"sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01", "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f",
"sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6", "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0",
"sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d", "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1",
"sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30", "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd",
"sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7", "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687",
"sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a", "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1",
"sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c", "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c",
"sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89", "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa",
"sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2", "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8",
"sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318", "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38",
"sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53", "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8",
"sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb", "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167",
"sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f", "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27",
"sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde", "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145",
"sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d", "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa",
"sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6", "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a",
"sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099", "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed",
"sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606", "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793",
"sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f", "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4",
"sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7", "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217",
"sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5", "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e",
"sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d", "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6",
"sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196", "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d",
"sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2", "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320",
"sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76", "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f",
"sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c", "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce",
"sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48", "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975",
"sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c", "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10",
"sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d", "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525",
"sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d", "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda",
"sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92", "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"
"sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d",
"sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7",
"sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==6.3" "version": "==6.3.1"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@ -948,11 +978,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.5" "version": "==7.0.1"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@ -1051,10 +1081,10 @@
}, },
"tomli": { "tomli": {
"hashes": [ "hashes": [
"sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"version": "==2.0.0" "version": "==2.0.1"
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
@ -1088,11 +1118,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.0.1" "version": "==4.1.1"
} }
} }
} }

View file

@ -1,9 +1,6 @@
LNbits LNbits
====== ======
[![github-tests-badge]][github-tests]
[![github-mypy-badge]][github-mypy]
[![codecov-badge]][codecov]
[![license-badge]](LICENSE) [![license-badge]](LICENSE)
[![docs-badge]][docs] [![docs-badge]][docs]

View file

@ -13,9 +13,9 @@ LNbits uses [Pipenv][pipenv] to manage Python packages.
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/ cd lnbits-legend/
pipenv shell pipenv shell
# pipenv --python 3.8 shell (if you wish to use a version of Python higher than 3.7) # pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
pipenv install --dev pipenv install --dev
# pipenv --python 3.8 install --dev (if you wish to use a version of Python higher than 3.7) # pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
# If any of the modules fails to install, try checking and upgrading your setupTool module # If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools # pip install -U setuptools

View file

@ -28,7 +28,6 @@ Download this repo and install the dependencies:
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/ cd lnbits-legend/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work # ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
# for now you'll need to `git checkout FastAPI`
python3 -m venv venv python3 -m venv venv
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
cp .env.example .env cp .env.example .env

View file

@ -36,16 +36,24 @@ Using this wallet requires the installation of the `grpcio` and `protobuf` Pytho
- `LND_GRPC_ENDPOINT`: ip_address - `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port - `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert - `LND_GRPC_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon - `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
### LND (REST) ### LND (REST)
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet** - `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: ip_address - `LND_REST_ENDPOINT`: ip_address
- `LND_REST_CERT`: /file/path/tls.cert - `LND_REST_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon - `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
or
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
### LNbits ### LNbits

View file

@ -61,7 +61,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
email=user["email"], email=user["email"],
extensions=[e[0] for e in extensions], extensions=[e[0] for e in extensions],
wallets=[Wallet(**w) for w in wallets], wallets=[Wallet(**w) for w in wallets],
admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS] if LNBITS_ADMIN_USERS else False admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS]
if LNBITS_ADMIN_USERS
else False,
) )
@ -217,6 +219,8 @@ async def get_payments(
incoming: bool = False, incoming: bool = False,
since: Optional[int] = None, since: Optional[int] = None,
exclude_uncheckable: bool = False, exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Payment]: ) -> List[Payment]:
""" """
@ -261,6 +265,15 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'") clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'") clause.append("checking_id NOT LIKE 'internal_%'")
limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else ""
offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else ""
# combine limit and offset clauses
limit_offset_clause = (
f"{limit_clause} {offset_clause}"
if limit_clause and offset_clause
else limit_clause or offset_clause
)
where = "" where = ""
if clause: if clause:
where = f"WHERE {' AND '.join(clause)}" where = f"WHERE {' AND '.join(clause)}"
@ -271,10 +284,10 @@ async def get_payments(
FROM apipayments FROM apipayments
{where} {where}
ORDER BY time DESC ORDER BY time DESC
{limit_offset_clause}
""", """,
tuple(args), tuple(args),
) )
return [Payment.from_row(row) for row in rows] return [Payment.from_row(row) for row in rows]

View file

@ -62,7 +62,7 @@
</q-btn> </q-btn>
</h3> </h3>
</q-card-section> </q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md"> <div class="row q-pb-md q-px-md q-col-gutter-md gt-sm">
<div class="col"> <div class="col">
<q-btn <q-btn
unelevated unelevated
@ -662,7 +662,23 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-tabs
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max"
active-class="px-0"
indicator-color="transparent"
>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
>
</q-tab>
<q-tab icon="content_paste" label="Paste" @click="showParseDialog"> </q-tab>
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
</q-tab>
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
</q-tabs>
{% if service_fee > 0 %} {% if service_fee > 0 %}
<div ref="disclaimer"></div> <div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show"> <q-dialog v-model="disclaimerDialog.show">

View file

@ -71,7 +71,7 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_update_balance( async def api_update_balance(
amount: int, wallet: WalletTypeInfo = Depends(get_key_type) amount: int, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS: if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user"
) )

View file

@ -1,4 +1,6 @@
import time import time
import asyncio
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
from http import HTTPStatus from http import HTTPStatus
@ -11,7 +13,7 @@ from lnbits import bolt11
from lnbits.core.crud import delete_expired_invoices, get_payments from lnbits.core.crud import delete_expired_invoices, get_payments
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice
from lnbits.decorators import WalletTypeInfo from lnbits.decorators import WalletTypeInfo
from lnbits.settings import WALLET from lnbits.settings import WALLET, LNBITS_SITE_TITLE
from . import lndhub_ext from . import lndhub_ext
from .decorators import check_wallet, require_admin_key from .decorators import check_wallet, require_admin_key
@ -55,14 +57,13 @@ async def lndhub_addinvoice(
_, pr = await create_invoice( _, pr = await create_invoice(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
amount=int(data.amt), amount=int(data.amt),
memo=data.memo or "received sats", memo=data.memo or LNBITS_SITE_TITLE,
extra={"tag": "lndhub"}, extra={"tag": "lndhub"},
) )
except: except:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice" status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice"
) )
invoice = bolt11.decode(pr) invoice = bolt11.decode(pr)
return { return {
"pay_req": pr, "pay_req": pr,
@ -116,7 +117,9 @@ async def lndhub_balance(
@lndhub_ext.get("/ext/gettxs") @lndhub_ext.get("/ext/gettxs")
async def lndhub_gettxs( async def lndhub_gettxs(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200) wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
): ):
for payment in await get_payments( for payment in await get_payments(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
@ -124,11 +127,15 @@ async def lndhub_gettxs(
pending=True, pending=True,
outgoing=True, outgoing=True,
incoming=False, incoming=False,
limit=limit,
offset=offset,
exclude_uncheckable=True, exclude_uncheckable=True,
): ):
await payment.set_pending( await payment.set_pending(
(await WALLET.get_payment_status(payment.checking_id)).pending (await WALLET.get_payment_status(payment.checking_id)).pending
) )
await asyncio.sleep(0.1)
return [ return [
{ {
"payment_preimage": payment.preimage, "payment_preimage": payment.preimage,
@ -148,28 +155,34 @@ async def lndhub_gettxs(
complete=True, complete=True,
outgoing=True, outgoing=True,
incoming=False, incoming=False,
limit=limit,
offset=offset,
) )
)[:limit] )
) )
] ]
@lndhub_ext.get("/ext/getuserinvoices") @lndhub_ext.get("/ext/getuserinvoices")
async def lndhub_getuserinvoices( async def lndhub_getuserinvoices(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200) wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
): ):
await delete_expired_invoices()
for invoice in await get_payments( for invoice in await get_payments(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
complete=False, complete=False,
pending=True, pending=True,
outgoing=False, outgoing=False,
incoming=True, incoming=True,
limit=limit,
offset=offset,
exclude_uncheckable=True, exclude_uncheckable=True,
): ):
await invoice.set_pending( await invoice.set_pending(
(await WALLET.get_invoice_status(invoice.checking_id)).pending (await WALLET.get_invoice_status(invoice.checking_id)).pending
) )
await asyncio.sleep(0.1)
return [ return [
{ {
@ -192,8 +205,10 @@ async def lndhub_getuserinvoices(
complete=True, complete=True,
incoming=True, incoming=True,
outgoing=False, outgoing=False,
limit=limit,
offset=offset,
) )
)[:limit] )
) )
] ]

View file

@ -77,8 +77,8 @@
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
userAmount: {{ paywall.amount }}, userAmount: '{{ paywall.amount }}',
paywallAmount: {{ paywall.amount }}, paywallAmount: '{{ paywall.amount }}',
paymentReq: null, paymentReq: null,
redirectUrl: null, redirectUrl: null,
paymentDialog: { paymentDialog: {
@ -89,7 +89,9 @@
}, },
computed: { computed: {
amount: function () { amount: function () {
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount return this.paywallAmount > this.userAmount
? this.paywallAmount
: this.userAmount
} }
}, },
methods: { methods: {
@ -102,48 +104,55 @@
}, },
createInvoice: function () { createInvoice: function () {
var self = this var self = this
console.log(this.amount) LNbits.api
axios .request(
.post( 'POST',
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice', '/paywall/api/v1/paywalls/invoice/{{ paywall.id }}',
{amount: self.amount} 'filler',
{
amount: self.amount
}
) )
.then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request.toUpperCase() if (response.data) {
self.paymentReq = response.data.payment_request.toUpperCase()
self.paymentDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.paymentDialog.dismissMsg = self.$q.notify({ self.paymentDialog.checker = setInterval(function () {
timeout: 0, LNbits.api
message: 'Waiting for payment...' .request(
}) 'POST',
'/paywall/api/v1/paywalls/check_invoice/{{ paywall.id }}',
self.paymentDialog.checker = setInterval(function () { 'filler',
axios {payment_hash: response.data.payment_hash}
.post( )
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice', .then(function (response) {
{payment_hash: response.data.payment_hash} if (response.data) {
) if (response.data.paid) {
.then(function (res) { self.cancelPayment()
if (res.data.paid) { self.redirectUrl = response.data.url
self.cancelPayment() if (response.data.remembers) {
self.redirectUrl = res.data.url self.$q.localStorage.set(
if (res.data.remembers) { 'lnbits.paywall.{{ paywall.id }}',
self.$q.localStorage.set( response.data.url
'lnbits.paywall.{{ paywall.id }}', )
res.data.url }
) self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
})
}
} }
})
self.$q.notify({ .catch(function (error) {
type: 'positive', LNbits.utils.notifyApiError(error)
message: 'Payment received!', })
icon: null }, 2000)
}) }
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
}) })
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)

View file

@ -52,20 +52,17 @@ async def api_paywall_delete(
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/invoice") @paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
async def api_paywall_create_invoice( async def api_paywall_create_invoice(
paywall_id,
data: CreatePaywallInvoice, data: CreatePaywallInvoice,
wallet: WalletTypeInfo = Depends(get_key_type), paywall_id: str = Query(None)
): ):
paywall = await get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
if data.amount < paywall.amount: if data.amount < paywall.amount:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"Minimum amount is {paywall.amount} sat.", detail=f"Minimum amount is {paywall.amount} sat.",
) )
try: try:
amount = data.amount if data.amount > paywall.amount else paywall.amount amount = data.amount if data.amount > paywall.amount else paywall.amount
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
@ -80,15 +77,14 @@ async def api_paywall_create_invoice(
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment_hash, "payment_request": payment_request}
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/check_invoice") @paywall_ext.post("/api/v1/paywalls/check_invoice/{paywall_id}")
async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id): async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id: str = Query(None)):
paywall = await get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
payment_hash = data.payment_hash payment_hash = data.payment_hash
if not paywall: if not paywall:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
) )
try: try:
status = await check_invoice_status(paywall.wallet, payment_hash) status = await check_invoice_status(paywall.wallet, payment_hash)
is_paid = not status.pending is_paid = not status.pending
@ -101,5 +97,4 @@ async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id):
await payment.set_pending(False) await payment.set_pending(False)
return {"paid": True, "url": paywall.url, "remembers": paywall.remembers} return {"paid": True, "url": paywall.url, "remembers": paywall.remembers}
return {"paid": False} return {"paid": False}

View file

@ -151,11 +151,12 @@
</q-page> </q-page>
</q-page-container> </q-page-container>
{% endblock %} {% block footer %} {% endblock %} {% block footer %}
<q-footer <q-footer
class="bg-transparent q-px-lg q-py-md" class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}" :class="{'text-dark': !$q.dark.isActive}"
> >
<q-toolbar> <q-toolbar class="gt-sm">
<q-toolbar-title class="text-caption"> <q-toolbar-title class="text-caption">
{{ SITE_TITLE }}, {{SITE_TAGLINE}} {{ SITE_TITLE }}, {{SITE_TAGLINE}}
<br /> <br />
@ -179,6 +180,7 @@
</q-btn> </q-btn>
</q-toolbar> </q-toolbar>
</q-footer> </q-footer>
{% endblock %} {% endblock %}
</q-layout> </q-layout>

View file

@ -11,6 +11,7 @@ import base64
import hashlib import hashlib
from os import environ, error, getenv from os import environ, error, getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .macaroon import load_macaroon, AESCipher
if imports_ok: if imports_ok:
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
@ -58,12 +59,6 @@ def get_ssl_context(cert_path: str):
return context return context
def load_macaroon(macaroon_path: str):
with open(macaroon_path, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
def parse_checking_id(checking_id: str) -> bytes: def parse_checking_id(checking_id: str) -> bytes:
return base64.b64decode(checking_id.replace("_", "/")) return base64.b64decode(checking_id.replace("_", "/"))
@ -90,18 +85,19 @@ class LndWallet(Wallet):
self.port = int(getenv("LND_GRPC_PORT")) self.port = int(getenv("LND_GRPC_PORT"))
self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT") self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
macaroon_path = ( macaroon = (
getenv("LND_GRPC_MACAROON") getenv("LND_GRPC_MACAROON")
or getenv("LND_GRPC_ADMIN_MACAROON") or getenv("LND_GRPC_ADMIN_MACAROON")
or getenv("LND_ADMIN_MACAROON") or getenv("LND_ADMIN_MACAROON")
or getenv("LND_GRPC_INVOICE_MACAROON") or getenv("LND_GRPC_INVOICE_MACAROON")
or getenv("LND_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
) )
if macaroon_path.split(".")[-1] == "macaroon":
self.macaroon = load_macaroon(macaroon_path) encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
else: if encrypted_macaroon:
self.macaroon = macaroon_path macaroon = AESCipher(description="macaroon decryption").decrypt(encrypted_macaroon)
self.macaroon = load_macaroon(macaroon)
cert = open(self.cert_path, "rb").read() cert = open(self.cert_path, "rb").read()
creds = grpc.ssl_channel_credentials(cert) creds = grpc.ssl_channel_credentials(cert)

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
from pydoc import describe
import httpx import httpx
import json import json
import base64 import base64
@ -6,6 +7,7 @@ from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from lnbits import bolt11 as lnbits_bolt11 from lnbits import bolt11 as lnbits_bolt11
from .macaroon import load_macaroon, AESCipher
from .base import ( from .base import (
StatusResponse, StatusResponse,
@ -34,7 +36,13 @@ class LndRestWallet(Wallet):
or getenv("LND_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
or getenv("LND_REST_INVOICE_MACAROON") or getenv("LND_REST_INVOICE_MACAROON")
) )
self.auth = {"Grpc-Metadata-macaroon": macaroon}
encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED")
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(encrypted_macaroon)
self.macaroon = load_macaroon(macaroon)
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
self.cert = getenv("LND_REST_CERT") self.cert = getenv("LND_REST_CERT")
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:

View file

@ -0,0 +1 @@
from .macaroon import load_macaroon, AESCipher

View file

@ -0,0 +1,103 @@
from Cryptodome import Random
from Cryptodome.Cipher import AES
import base64
from hashlib import md5
import getpass
BLOCK_SIZE = 16
import getpass
def load_macaroon(macaroon: str) -> str:
"""Returns hex version of a macaroon encoded in base64 or the file path.
:param macaroon: Macaroon encoded in base64 or file path.
:type macaroon: str
:return: Hex version of macaroon.
:rtype: str
"""
# if the macaroon is a file path, load it
if macaroon.split(".")[-1] == "macaroon":
with open(macaroon, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
else:
# convert the bas64 macaroon to hex
try:
macaroon = base64.b64decode(macaroon).hex()
except:
pass
return macaroon
class AESCipher(object):
"""This class is compatible with crypto-js/aes.js
Encrypt and decrypt in Javascript using:
import AES from "crypto-js/aes.js";
import Utf8 from "crypto-js/enc-utf8.js";
AES.encrypt(decrypted, password).toString()
AES.decrypt(encrypted, password).toString(Utf8);
"""
def __init__(self, key=None, description=""):
self.key = key
self.description = description + " "
def pad(self, data):
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length) * length).encode()
def unpad(self, data):
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
@property
def passphrase(self):
passphrase = self.key if self.key is not None else None
if passphrase is None:
passphrase = getpass.getpass(f"Enter {self.description}password:")
return passphrase
def bytes_to_key(self, data, salt, output=48):
# extended from https://gist.github.com/gsakkis/4546068
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def decrypt(self, encrypted: str) -> str:
"""Decrypts a string using AES-256-CBC.
"""
passphrase = self.passphrase
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
try:
return self.unpad(aes.decrypt(encrypted[16:])).decode()
except UnicodeDecodeError:
raise ValueError("Wrong passphrase")
def encrypt(self, message: bytes) -> str:
passphrase = self.passphrase
salt = Random.new().read(8)
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return base64.b64encode(b"Salted__" + salt + aes.encrypt(self.pad(message))).decode()
# if this file is executed directly, ask for a macaroon and encrypt it
if __name__ == "__main__":
macaroon = input("Enter macaroon: ")
macaroon = load_macaroon(macaroon)
macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
print("Encrypted macaroon:")
print(macaroon)

View file

@ -25,6 +25,7 @@ markupsafe==2.0.1
marshmallow==3.13.0 marshmallow==3.13.0
outcome==1.1.0 outcome==1.1.0
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
pycryptodomex==3.14.1
pydantic==1.8.2 pydantic==1.8.2
pypng==0.0.21 pypng==0.0.21
pyqrcode==1.2.1 pyqrcode==1.2.1
@ -46,4 +47,4 @@ uvicorn==0.15.0
uvloop==0.16.0 uvloop==0.16.0
watchgod==0.7 watchgod==0.7
websockets==10.0 websockets==10.0
zipp==3.5.0 zipp==3.5.0

View file

@ -9,18 +9,21 @@ from tests.helpers import credit_wallet
from tests.extensions.bleskomat.conftest import bleskomat, lnurl from tests.extensions.bleskomat.conftest import bleskomat, lnurl
from tests.mocks import WALLET from tests.mocks import WALLET
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_missing_secret(client): async def test_bleskomat_lnurl_api_missing_secret(client):
response = await client.get("/bleskomat/u") response = await client.get("/bleskomat/u")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Missing secret"} assert response.json() == {"status": "ERROR", "reason": "Missing secret"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_invalid_secret(client): async def test_bleskomat_lnurl_api_invalid_secret(client):
response = await client.get("/bleskomat/u?k1=invalid-secret") response = await client.get("/bleskomat/u?k1=invalid-secret")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Invalid secret"} assert response.json() == {"status": "ERROR", "reason": "Invalid secret"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_unknown_api_key(client): async def test_bleskomat_lnurl_api_unknown_api_key(client):
query = { query = {
@ -33,11 +36,12 @@ async def test_bleskomat_lnurl_api_unknown_api_key(client):
"f": "EUR", "f": "EUR",
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = "xxx"# not checked, so doesn't matter signature = "xxx" # not checked, so doesn't matter
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Unknown API key"} assert response.json() == {"status": "ERROR", "reason": "Unknown API key"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat): async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
query = { query = {
@ -51,10 +55,11 @@ async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = "invalid" signature = "invalid"
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Invalid API key signature"} assert response.json() == {"status": "ERROR", "reason": "Invalid API key signature"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat): async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
query = { query = {
@ -64,41 +69,42 @@ async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
"minWithdrawable": "1", "minWithdrawable": "1",
"maxWithdrawable": "1", "maxWithdrawable": "1",
"defaultDescription": "test valid sig", "defaultDescription": "test valid sig",
"f": "EUR",# tests use the dummy exchange rate provider "f": "EUR", # tests use the dummy exchange rate provider
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = generate_bleskomat_lnurl_signature( signature = generate_bleskomat_lnurl_signature(
payload=payload, payload=payload, api_key_secret=bleskomat.api_key_secret, api_key_encoding=bleskomat.api_key_encoding
api_key_secret=bleskomat.api_key_secret,
api_key_encoding=bleskomat.api_key_encoding
) )
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["tag"] == "withdrawRequest" assert data["tag"] == "withdrawRequest"
assert data["minWithdrawable"] == 1000 assert data["minWithdrawable"] == 1000
assert data["maxWithdrawable"] == 1000 assert data["maxWithdrawable"] == 1000
assert data["defaultDescription"] == "test valid sig" assert data["defaultDescription"] == "test valid sig"
assert data["callback"] == f'http://{HOST}:{PORT}/bleskomat/u' assert data["callback"] == f"http://{HOST}:{PORT}/bleskomat/u"
k1 = data["k1"] k1 = data["k1"]
lnurl = await get_bleskomat_lnurl(secret=k1) lnurl = await get_bleskomat_lnurl(secret=k1)
assert lnurl assert lnurl
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl): async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl):
bleskomat = lnurl["bleskomat"] bleskomat = lnurl["bleskomat"]
secret = lnurl["secret"] secret = lnurl["secret"]
pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu" pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu"
WALLET.pay_invoice.reset_mock() WALLET.pay_invoice.reset_mock()
response = await client.get(f'/bleskomat/u?k1={secret}&pr={pr}') response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Failed to pay invoice: Insufficient balance."} assert response.json()["status"] == "ERROR"
assert ("Insufficient balance" in response.json()["reason"]) or ("fee" in response.json()["reason"])
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 0 assert wallet.balance_msat == 0
bleskomat_lnurl = await get_bleskomat_lnurl(secret) bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl.has_uses_remaining() == True assert bleskomat_lnurl.has_uses_remaining() == True
WALLET.pay_invoice.assert_not_called() WALLET.pay_invoice.assert_not_called()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_action_success(client, lnurl): async def test_bleskomat_lnurl_api_action_success(client, lnurl):
bleskomat = lnurl["bleskomat"] bleskomat = lnurl["bleskomat"]
@ -111,7 +117,7 @@ async def test_bleskomat_lnurl_api_action_success(client, lnurl):
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 100000 assert wallet.balance_msat == 100000
WALLET.pay_invoice.reset_mock() WALLET.pay_invoice.reset_mock()
response = await client.get(f'/bleskomat/u?k1={secret}&pr={pr}') response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.json() == {"status": "OK"} assert response.json() == {"status": "OK"}
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 50000 assert wallet.balance_msat == 50000