From c3013f92b7c725d449ee84e7a4c1266b08252d0c Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Thu, 25 Jul 2024 20:13:34 +0000 Subject: [PATCH] tests --- .github/workflows/test.yml | 36 + .gitignore | 3 + tests/integration/.env | 252 ++++++ .../.v039fk_lnbits_integration_test_folder | 1 + tests/integration/data.zip | Bin 0 -> 69158 bytes tests/integration/start.sh | 44 + tests/integration/stop.sh | 6 + tests/integration/strfry.conf | 138 +++ tests/integration/test_all.py | 838 ++++++++++++++++++ 9 files changed, 1318 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 tests/integration/.env create mode 100644 tests/integration/.v039fk_lnbits_integration_test_folder create mode 100644 tests/integration/data.zip create mode 100644 tests/integration/start.sh create mode 100644 tests/integration/stop.sh create mode 100644 tests/integration/strfry.conf create mode 100644 tests/integration/test_all.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..72a38ff --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + pull_request: + + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + + - name: Run setup script + run: /bin/bash .devcontainer/setup.sh ${{ github.workspace }} + + - name: Run unit tests + run: pytest tests/unit/*.py -s + + - name: Setup integration tests + run: bash tests/integration/start.sh + + - name: Run integration tests + run: pytest tests/integration/test_all.py -s \ No newline at end of file diff --git a/.gitignore b/.gitignore index bee8a64..3f09e69 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ __pycache__ +lnbits +tests/integration/strfry-data +tests/integration/lnbits_itest_data \ No newline at end of file diff --git a/tests/integration/.env b/tests/integration/.env new file mode 100644 index 0000000..0f1cedc --- /dev/null +++ b/tests/integration/.env @@ -0,0 +1,252 @@ +#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/ + +###################################### +########### Admin Settings ########### +###################################### + +# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available. +# Warning: Enabling this will make LNbits ignore most configurations in file. Only the +# configurations defined in `ReadOnlySettings` will still be read from the environment variables. +# The rest of the settings will be stored in your database and you will be able to change them +# only through the Admin UI. +# Disable this to make LNbits use this config file again. +LNBITS_ADMIN_UI=true + +# Change theme +LNBITS_SITE_TITLE="LNBits_NWC_SP" +LNBITS_SITE_TAGLINE="free and open-source lightning wallet" +LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack." +# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber +LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber" +# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" + +HOST=0.0.0.0 +PORT=5000 + +###################################### +########## Funding Source ############ +###################################### + +# which fundingsources are allowed in the admin ui +LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet" + +LNBITS_BACKEND_WALLET_CLASS=FakeWallet +# VoidWallet is just a fallback that works without any actual Lightning capabilities, +# just so you can see the UI before dealing with this file. + +# How many times to retry connectiong to the Funding Source before defaulting to the VoidWallet +# FUNDING_SOURCE_MAX_RETRIES=4 + +# Invoice expiry for LND, CLN, Eclair, LNbits funding sources +LIGHTNING_INVOICE_EXPIRY=3600 + +# Set one of these blocks depending on the wallet kind you chose above: + +# ClicheWallet +CLICHE_ENDPOINT=ws://127.0.0.1:12000 + +# SparkWallet +SPARK_URL=http://localhost:9737/rpc +SPARK_TOKEN=myaccesstoken + +# CoreLightningWallet +CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" + +# CoreLightningRestWallet +CORELIGHTNING_REST_URL=http://127.0.0.1:8185/ +CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" # or BASE64/HEXSTRING +CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert" + +# LnbitsWallet +LNBITS_ENDPOINT=https://demo.lnbits.com +LNBITS_KEY=LNBITS_ADMIN_KEY + +# LndWallet +LND_GRPC_ENDPOINT=127.0.0.1 +LND_GRPC_PORT=10009 +LND_GRPC_CERT="/home/bob/.lnd/tls.cert" +LND_GRPC_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING +# To use an AES-encrypted macaroon, set +# LND_GRPC_MACAROON="eNcRyPtEdMaCaRoOn" + +# LndRestWallet +LND_REST_ENDPOINT=https://127.0.0.1:8080/ +LND_REST_CERT="/home/bob/.lnd/tls.cert" +LND_REST_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING +# To use an AES-encrypted macaroon, set +# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn" + +# LNPayWallet +LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ +# Secret API Key under developers tab +LNPAY_API_KEY=LNPAY_API_KEY +# Wallet Admin in Wallet Access Keys +LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY + +# AlbyWallet +ALBY_API_ENDPOINT=https://api.getalby.com/ +ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN + +# ZBDWallet +ZBD_API_ENDPOINT=https://api.zebedee.io/v0/ +ZBD_API_KEY=ZBD_ACCESS_TOKEN + +# PhoenixdWallet +PHOENIXD_API_ENDPOINT=http://localhost:9740/ +PHOENIXD_API_PASSWORD=PHOENIXD_KEY + +# OpenNodeWallet +OPENNODE_API_ENDPOINT=https://api.opennode.com/ +OPENNODE_KEY=OPENNODE_ADMIN_KEY + +# FakeWallet +FAKE_WALLET_SECRET="ToTheMoon1" +LNBITS_DENOMINATION=sats + +# EclairWallet +ECLAIR_URL=http://127.0.0.1:8283 +ECLAIR_PASS=eclairpw + +# LnTipsWallet +# Enter /api in LightningTipBot to get your key +LNTIPS_API_KEY=LNTIPS_ADMIN_KEY +LNTIPS_API_ENDPOINT=https://ln.tips + +###################################### +####### Auth Configurations ########## +###################################### +# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value. +AUTH_SECRET_KEY="secret" +AUTH_TOKEN_EXPIRE_MINUTES=525600 +# Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth +AUTH_ALLOWED_METHODS="user-id-only, username-password" +# Set this flag if HTTP is used for OAuth +# OAUTHLIB_INSECURE_TRANSPORT="1" + +# Google OAuth Config +# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" + +# GitHub OAuth Config +# Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" + +# Keycloak OAuth Config +# Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token +KEYCLOAK_CLIENT_ID="" +KEYCLOAK_CLIENT_SECRET="" +KEYCLOAK_DISCOVERY_URL="" + + +###################################### + +# uvicorn variable, uncomment to allow https behind a proxy +# IMPORTANT: this also needs the webserver to be configured to forward the headers +# http://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https +# FORWARDED_ALLOW_IPS="*" + +# Server security, rate limiting ips, blocked ips, allowed ips +LNBITS_RATE_LIMIT_NO="200" +LNBITS_RATE_LIMIT_UNIT="minute" +LNBITS_ALLOWED_IPS="" +LNBITS_BLOCKED_IPS="" + +# Allow users and admins by user IDs (comma separated list) +# if set new users will not be able to create accounts +LNBITS_ALLOWED_USERS="" +LNBITS_ADMIN_USERS="" +# ID of the super user. The user ID must exist. +# SUPER_USER="" + +# Extensions only admin can access +LNBITS_ADMIN_EXTENSIONS="ngrok, admin" + +# Start LNbits core only. The extensions are not loaded. +# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true + +# Disable account creation for new users +# LNBITS_ALLOW_NEW_ACCOUNTS=false + +# Enable Node Management without activating the LNBITS Admin GUI +# by setting the following variables to true. +LNBITS_NODE_UI=false +LNBITS_PUBLIC_NODE_UI=false +# Enabling the transactions tab can cause crashes on large Core Lightning nodes. +LNBITS_NODE_UI_TRANSACTIONS=false + +LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" + +# Ad space description +# LNBITS_AD_SPACE_TITLE="Supported by" +# csv ad space, format ";;, ;;", extensions can choose to honor +# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png" +# LNBITS_SHOW_HOME_PAGE_ELEMENTS=true # if set to true, the ad space will be displayed on the home page +# LNBITS_CUSTOM_BADGE="USE WITH CAUTION - LNbits wallet is still in BETA" +# LNBITS_CUSTOM_BADGE_COLOR="warning" + +# Hides wallet api, extensions can choose to honor +LNBITS_HIDE_API=false + +# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json" +# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN +# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx + +# Path where extensions will be installed (defaults to `./lnbits/`). +# Inside this directory the `extensions` and `upgrades` sub-directories will be created. +# LNBITS_EXTENSIONS_PATH="/path/to/some/dir" + +# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart. +# The extension must be removed from this list in order to not be re-installed. +LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos" + +# Database: to use SQLite, specify LNBITS_DATA_FOLDER +# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... +# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... +# for both PostgreSQL and CockroachDB, you'll need to install +# psycopg2 as an additional dependency +LNBITS_DATA_FOLDER="./data" +# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" + +# the service fee (in percent) +LNBITS_SERVICE_FEE=0.0 +# the wallet where fees go to +# LNBITS_SERVICE_FEE_WALLET= +# the maximum fee per transaction (in satoshis) +# LNBITS_SERVICE_FEE_MAX=1000 +# disable fees for internal transactions +# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true + +# value in millisats +LNBITS_RESERVE_FEE_MIN=2000 +# value in percent +LNBITS_RESERVE_FEE_PERCENT=1.0 + +# limit the maximum balance for each wallet +# throw an error if the wallet attempts to create a new invoice + +# LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000 +# LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000 +# LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60 + +# Limit fiat currencies allowed to see in UI +# LNBITS_ALLOWED_CURRENCIES="EUR, USD" + +###################################### +###### Logging and Development ####### +###################################### + +DEBUG=true +DEBUG_DATABASE=false +BUNDLE_ASSETS=true + +# logging into LNBITS_DATA_FOLDER/logs/ +ENABLE_LOG_TO_FILE=true + +# https://loguru.readthedocs.io/en/stable/api/logger.html#file +LOG_ROTATION="100 MB" +LOG_RETENTION="3 months" + +# for database cleanup commands +# CLEANUP_WALLETS_DAYS=90 \ No newline at end of file diff --git a/tests/integration/.v039fk_lnbits_integration_test_folder b/tests/integration/.v039fk_lnbits_integration_test_folder new file mode 100644 index 0000000..3e4aa3c --- /dev/null +++ b/tests/integration/.v039fk_lnbits_integration_test_folder @@ -0,0 +1 @@ +yes v039fk_lnbits_integration_test_folder \ No newline at end of file diff --git a/tests/integration/data.zip b/tests/integration/data.zip new file mode 100644 index 0000000000000000000000000000000000000000..e89b53ec073079d9ab15fdf7a1bef52f9bd25948 GIT binary patch literal 69158 zcmZ^~1z4QD(mxE8A}#L4Dekto6^ayhcXxMhao6JRy108=+>5)z;=VY`N1r3lIq!G= zxvov_%zfvVNivyCGFb)bH?UYxf5LL)y~bY;|BNu8NTF=(j4WN8^(|dYon7=z3|$PF zRaKCo;H&NP&HnXs^>_~j4ZHsa3hJ*&;V(P{Cf`@pdmTg?6!KiN|GW~r~dB>n&SImwc? zp^()2dg8WxI^g2{!HwmYMeZDX`zqs-TNlZj_Q+M;)sqiGZOf;gCn#bjq9&$lrlw}Q z1$E`(&gVC5Ul5oOYNw<~8@{T2{e(c~_(_M`d2{e{&)W-b2OS`+h^UF`+=%J?xYvTA z*GsjoZW_A3_!GLn=qr#(-)4a>R~{u_7(NZ{kaJF2Mo!LStCFRion4l%o>5MYL5}Cm zJ)wRvDG@pFU*xs&nv_3YLH-G=zqO~ zf;vCgn)>cl%L}0Say7}{SpV;m zP(BYWGdVCC8Xt0?TWUJaf0_zH_vE}xRO@7n;QmMbJ*aO6HlLC@TetgWs~Pn>!orNu z6Azor5F^MW;V^8psS{)uQp$4GDUVbYy;N0DNM>%%-v0Pz5j?hP(B9ELTa-p;`}ji_ zym~@ZA9(s9w0}Dpw_|!~k&2DcIt-xm^31XHsY>1Q_h6&0)SMpu;$Y;#$yEA@xTDgA zVj!D@5SV{&FAr0$H~KTGE0xY?Txv_e;Uvj5dkV)y^9qIDV-G~_!1Km4N$4d<|2~xG z2LA6R_LRh_S!r1uSyJo@h?&i!K&{q@1u7wNkzf4rC1Iq}cXQ;+Nq>AqX>9|t}? zfxFuv!PecQDF5#3Z0Hz$osPQOskODWm7+*oN&+%vEM&16S22}1O1d^0L(w=}!_^x0 z9Z!Iw88Fw(TvS>0^QZJ;1!yv>()7ILVo9_)bXFG|ye47SA2nDbYRKB?qLqViN^y2! z#m~TbObCzP#g+n>W_-L9mWdS}eQF;$d7mgot!xx*u$92-j%J%-p#o7xf(52@o<^OE ztO;U+N=BF{PFOUukw(541v2eBs*p-;{CHSwNjhgznd-0puN>nY_Sc2K&C{;4ij0V6 z#lS``n`uzdt*N!Rc1bilC`OYV2q?rWW~?uDeWO_UVNeZe)M?Zxja+HeCC)Ye@qKct zd4b7bNLlS&njX^&VYi22DS>oNF=<8-Vp@bPKHTpxrnK_BwDd0≫t}MFe~**zpJ& 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%0A&_o~6yfSs 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

wYI~3YZk|X_{*`qN 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 PW;Xm3!R_MCBEZ}KRm%5k literal 0 HcmV?d00001 diff --git a/tests/integration/start.sh b/tests/integration/start.sh new file mode 100644 index 0000000..dd68b60 --- /dev/null +++ b/tests/integration/start.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# cd in the script folder +cd "$(dirname "$0")" + +# Check if we are in the right folder +if [ ! -f ".v039fk_lnbits_integration_test_folder" ]; then + echo "Please run this script from the tests/integration folder" + exit 1 +fi + +# Double check if we are in the right folder +if [ "`cat .v039fk_lnbits_integration_test_folder`" != "yes v039fk_lnbits_integration_test_folder" ]; then + echo "Please run this script from the tests/integration folder!" + exit 1 +fi + +# Start nostr Relay +docker run --name=lnbits_nwcprovider_ext_nostr_test \ +-d \ +--rm \ +-v $PWD/strfry.conf:/etc/strfry.conf \ +-v $PWD/strfry-data:/app/strfry-db \ +-p 7777:7777 \ +ghcr.io/hoytech/strfry:latest + +# Start lnbits with the nwcprovider extension +rm -Rf lnbits_itest_data +unzip data.zip +docker run --name=lnbits_nwcprovider_ext_lnbits_test \ +-d \ +--rm \ +-p 5002:5000 \ +-v ${PWD}/.env:/app/.env \ +-v ${PWD}/lnbits_itest_data/:/app/data \ +-v ${PWD}/../../:/app/lnbits/extensions/nwcprovider:ro \ +lnbits/lnbits + + +docker network create lnbits_nwcprovider_ext_test_network || true +docker network connect lnbits_nwcprovider_ext_test_network lnbits_nwcprovider_ext_nostr_test --alias nostr|| true +docker network connect lnbits_nwcprovider_ext_test_network lnbits_nwcprovider_ext_lnbits_test --alias lnbits|| true + + diff --git a/tests/integration/stop.sh b/tests/integration/stop.sh new file mode 100644 index 0000000..6da1e19 --- /dev/null +++ b/tests/integration/stop.sh @@ -0,0 +1,6 @@ +#!/bin/bash +docker stop lnbits_nwcprovider_ext_nostr_test || true +docker stop lnbits_nwcprovider_ext_lnbits_test || true + +# Remove lnbits_nwcprovider_ext_test_network network +docker network rm lnbits_nwcprovider_ext_test_network || true \ No newline at end of file diff --git a/tests/integration/strfry.conf b/tests/integration/strfry.conf new file mode 100644 index 0000000..150bcc3 --- /dev/null +++ b/tests/integration/strfry.conf @@ -0,0 +1,138 @@ +## +## Default strfry config +## + +# Directory that contains the strfry LMDB database (restart required) +db = "./strfry-db/" + +dbParams { + # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required) + maxreaders = 256 + + # Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required) + mapsize = 10995116277760 + + # Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required) + noReadAhead = false +} + +events { + # Maximum size of normalised JSON, in bytes + maxEventSize = 65536 + + # Events newer than this will be rejected + rejectEventsNewerThanSeconds = 900 + + # Events older than this will be rejected + rejectEventsOlderThanSeconds = 94608000 + + # Ephemeral events older than this will be rejected + rejectEphemeralEventsOlderThanSeconds = 60 + + # Ephemeral events will be deleted from the DB when older than this + ephemeralEventsLifetimeSeconds = 300 + + # Maximum number of tags allowed + maxNumTags = 2000 + + # Maximum size for tag values, in bytes + maxTagValSize = 1024 +} + +relay { + # Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required) + bind = "0.0.0.0" + + # Port to open for the nostr websocket protocol (restart required) + port = 7777 + + # Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required) + nofiles = 1000000 + + # HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case) + realIpHeader = "" + + info { + # NIP-11: Name of this server. Short/descriptive (< 30 characters) + name = "strfry default" + + # NIP-11: Detailed information about relay, free-form + description = "This is a strfry instance." + + # NIP-11: Administrative nostr pubkey, for contact purposes + pubkey = "" + + # NIP-11: Alternative administrative contact (email, website, etc) + contact = "" + } + + # Maximum accepted incoming websocket frame size (should be larger than max event) (restart required) + maxWebsocketPayloadSize = 131072 + + # Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required) + autoPingSeconds = 55 + + # If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy) + enableTcpKeepalive = false + + # How much uninterrupted CPU time a REQ query should get during its DB scan + queryTimesliceBudgetMicroseconds = 10000 + + # Maximum records that can be returned per filter + maxFilterLimit = 500 + + # Maximum number of subscriptions (concurrent REQs) a connection can have open at any time + maxSubsPerConnection = 20 + + writePolicy { + # If non-empty, path to an executable script that implements the writePolicy plugin logic + plugin = "" + } + + compression { + # Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required) + enabled = true + + # Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required) + slidingWindow = true + } + + logging { + # Dump all incoming messages + dumpInAll = false + + # Dump all incoming EVENT messages + dumpInEvents = false + + # Dump all incoming REQ/CLOSE messages + dumpInReqs = false + + # Log performance metrics for initial REQ database scans + dbScanPerf = false + + # Log reason for invalid event rejection? Can be disabled to silence excessive logging + invalidEvents = true + } + + numThreads { + # Ingester threads: route incoming requests, validate events/sigs (restart required) + ingester = 3 + + # reqWorker threads: Handle initial DB scan for events (restart required) + reqWorker = 3 + + # reqMonitor threads: Handle filtering of new events (restart required) + reqMonitor = 3 + + # negentropy threads: Handle negentropy protocol messages (restart required) + negentropy = 2 + } + + negentropy { + # Support negentropy protocol messages + enabled = true + + # Maximum records that sync will process before returning an error + maxSyncEvents = 1000000 + } +} \ No newline at end of file diff --git a/tests/integration/test_all.py b/tests/integration/test_all.py new file mode 100644 index 0000000..d07d88a --- /dev/null +++ b/tests/integration/test_all.py @@ -0,0 +1,838 @@ +import pytest +import asyncio +from loguru import logger +import httpx +import asyncio +import bolt11 +import secp256k1 +import time +from typing import List, Dict +import websockets +import random +import json +from typing import List, Dict, Optional +from Cryptodome.Util.Padding import pad, unpad +import hashlib +from Cryptodome import Random +from Cryptodome.Cipher import AES +import base64 +from typing import Union, Dict +wallets = { + "wallet1": { + "name": "wallet1", + "id": "ca464af8b1a94f988d6d729586961d2a", + "admin_key": "7d2541d0c4154a498e43e5e287c64640", + "balance_msats": 1000000, + }, + "wallet2": { + "name": "wallet2", + "id": "147adb7b35f14fcca146a5e9b570fc18", + "admin_key": "4d67c02489f34cd78aec68af48c7c8b4", + "balance_msats": 1000000, + }, + "wallet3": { + "name": "wallet3", + "id": "343a5d4a96cc4cf793a49a2df9ca04e6", + "admin_key": "ca4e7c921fdb4ec2b761cadb5fd1d30d", + "balance_msats": 1000000, + }, + "wallet4": { + "name": "wallet4", + "id": "2faa91184177414ab14712cadafbc78f", + "admin_key": "0ffd65580a664e0aae85687f99dac7ad", + "balance_msats": 1000000, + } +} + +async def check_services(): + # wait for http server in localhost:7777 + while True: + try: + async with httpx.AsyncClient() as client: + resp = await client.get('http://localhost:7777') + assert resp.status_code == 200 + break + except: + logger.info("Waiting for nostr relay @ http://localhost:7777") + logger.info("Please start the required services by running `bash start.sh` if you haven't already") + await asyncio.sleep(1) + + # wait lnbits @ localhost:5000 + while True: + try: + async with httpx.AsyncClient() as client: + resp = await client.get('http://localhost:5002') + assert resp.status_code == 200 + break + except: + logger.info("Waiting for lnbits @ http://localhost:5002") + logger.info("Please start the required services by running `bash start.sh` if you haven't already") + await asyncio.sleep(1) + + +async def get_wallet_balance(w:str): + api_key = wallets[w]["admin_key"] + async with httpx.AsyncClient() as client: + resp = await client.get(f'http://localhost:5002/api/v1/wallet?api-key={api_key}') + assert resp.status_code == 200 + v = resp.json() + balance = v["balance"] + return balance + +async def refresh_wallet_balances(): + for w in wallets: + wallets[w]["balance_msats"] = await get_wallet_balance(w) + logger.info(f"{w} balance: {wallets[w]['balance_msats']}") + + +def gen_keypair(): + private_key_hex = bytes.hex(secp256k1._gen_private_key()) + private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex)) + public_key = private_key.pubkey + public_key_hex = public_key.serialize().hex()[2:] + return { + "priv": private_key_hex, + "pub": public_key_hex + } + +async def create_nwc(w:str, desc:str, permissions:List[str], budgets:List[Dict[str, int]], expiration: 0): + keypair = gen_keypair() + api_key = wallets[w]["admin_key"] + async with httpx.AsyncClient() as client: + resp = await client.put(f'http://localhost:5002/nwcprovider/api/v1/nwc/{keypair["pub"]}?api-key={api_key}', json={ + "permissions": permissions, + "description": desc, + "expires_at": time.time()+expiration if expiration > 0 else 0, + "budgets": budgets + }) + assert resp.status_code == 201 + nwc = resp.json() + + async with httpx.AsyncClient() as client: + resp = await client.get(f'http://localhost:5002/nwcprovider/api/v1/pairing/{keypair["priv"]}') + assert resp.status_code == 200 + pairing = resp.json() + return { + "pubkey": keypair["pub"], + "privkey": keypair["priv"], + "pairing": pairing, + "nwc": nwc + } + + + +async def delete_nwc(w:str, pubkey:str): + + api_key = wallets[w]["admin_key"] + async with httpx.AsyncClient() as client: + resp = await client.delete(f'http://localhost:5002/nwcprovider/api/v1/nwc/{pubkey}?api-key={api_key}') + assert resp.status_code == 200 + return resp.json() + + +class NWCWallet : + def __init__(self, pairing_url): + # Extract from Pairing url nostr+walletconnect://provider_pub?relay=relay&secret=secret + self.pairing_url = pairing_url + self.provider_pub_hex = pairing_url.split("://")[1].split("?")[0] + self.relay = pairing_url.split("relay=")[1].split("&")[0] + self.secret = pairing_url.split("secret=")[1] + self.ws = None + self.connected = False + self.shutdown = False + self.event_queue = [] + self.subscriptions_count = 0 + self.sub_id="" + self.private_key = secp256k1.PrivateKey(bytes.fromhex(self.secret)) + self.private_key_hex = self.secret + self.public_key = self.private_key.pubkey + self.public_key_hex = self.public_key.serialize().hex()[2:] + + + async def close(self): + self.shutdown = True + await self.ws.close() + self.connected = False + + async def _wait_for_connection(self): + while not self.connected: + await asyncio.sleep(0.2) + + async def start(self): + asyncio.create_task(self._run()) + await self._wait_for_connection() + + + + def _is_shutting_down(self): + return self.shutdown + + def _get_new_subid(self) -> str: + subid = "lnbitsnwcstest"+str(self.subscriptions_count) + self.subscriptions_count += 1 + maxLength = 64 + chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + n = maxLength - len(subid) + if n > 0: + for i in range(n): + subid += chars[random.randint(0, len(chars) - 1)] + return subid + + async def _run(self): + while True: + try: + async with websockets.connect(self.relay) as ws: + self.ws = ws + self.connected = True + self.sub_id = self._get_new_subid() + res_filter = { + "kinds": [23195], + "authors": [self.provider_pub_hex], + "since": int(time.time()) + } + await self.ws.send(self._json_dumps(["REQ", self.sub_id, res_filter])) + while not self._is_shutting_down() and not ws.closed: + try: + reply = await ws.recv() + try: + await self._on_message(ws, reply) + except Exception as e: + pass + except Exception as e: + logger.debug("Error receiving message: " + str(e)) + break + except Exception as e: + logger.debug("Error connecting to relay: " + str(e)) + pass + self.connected = False + if not self._is_shutting_down(): + await asyncio.sleep(0.2) + else: + break + + def _encrypt_content(self, content: str, pubkey_hex: str, iv_seed: Optional[int] = None) -> str: + pubkey = secp256k1.PublicKey( + bytes.fromhex("02" + pubkey_hex), True) + shared = pubkey.tweak_mul(bytes.fromhex( + self.private_key_hex)).serialize()[1:] + if not iv_seed: + iv = Random.new().read(AES.block_size) + else: + iv = hashlib.sha256(iv_seed.to_bytes(32, byteorder='big')).digest() + iv = iv[:AES.block_size] + aes = AES.new(shared, AES.MODE_CBC, iv) + content_bytes = content.encode("utf-8") + content_bytes = pad(content_bytes, AES.block_size) + encrypted_b64 = base64.b64encode( + aes.encrypt(content_bytes)).decode("ascii") + ivB64 = base64.b64encode(iv).decode("ascii") + encrypted_content = encrypted_b64 + "?iv=" + ivB64 + return encrypted_content + + def _decrypt_content(self, content: str, pubkey_hex: str) -> str: + pubkey = secp256k1.PublicKey( + bytes.fromhex("02" + pubkey_hex), True) + shared = pubkey.tweak_mul(bytes.fromhex( + self.private_key_hex)).serialize()[1:] + (encrypted_content_b64, iv_b64) = content.split("?iv=") + encrypted_content = base64.b64decode( + encrypted_content_b64.encode("ascii")) + iv = base64.b64decode(iv_b64.encode("ascii")) + aes = AES.new(shared, AES.MODE_CBC, iv) + decrypted_bytes = aes.decrypt(encrypted_content) + decrypted_bytes = unpad(decrypted_bytes, AES.block_size) + decrypted = decrypted_bytes.decode("utf-8") + return decrypted + + async def _on_message(self, ws, message: str): + logger.debug("Received message: " + message) + msg = json.loads(message) + if msg[0] == "EVENT": # Event message + sub_id = msg[1] + event = msg[2] + nwc_pubkey = event["pubkey"] + content = self._decrypt_content(event["content"], nwc_pubkey) + content = json.loads(content) + self.event_queue.append({ + "created_at": event["created_at"], + "content": content, + "result": content["result"] if "result" in content else None, + "error": content["error"] if "error" in content else None, + "method": content["result_type"], + "tags": event["tags"] + }) + + def _json_dumps(self, data: Union[Dict, list]) -> str: + if isinstance(data, Dict): + data = {k: v for k, v in data.items() if v is not None} + return json.dumps(data, separators=(',', ':'), ensure_ascii=False) + + def _sign_event(self, event: Dict) -> Dict: + signature_data = self._json_dumps([ + 0, + self.public_key_hex, + event["created_at"], + event["kind"], + event["tags"], + event["content"] + ]) + + event_id = hashlib.sha256(signature_data.encode()).hexdigest() + event["id"] = event_id + event["pubkey"] = self.public_key_hex + signature = (self.private_key.schnorr_sign( + bytes.fromhex(event_id), None, raw=True)).hex() + event["sig"] = signature + return event + + + async def sendEvent(self, method,params): + await self._wait_for_connection() + event = { + "created_at": int(time.time()), + "kind": 23194, + "tags":[ + ["p", self.provider_pub_hex], + ], + "content": json.dumps({ + "method": method, + "params": params + + }) + } + logger.debug("Sending event: " + str(event)) + event["content"] = self._encrypt_content(event["content"], self.provider_pub_hex) + self._sign_event(event) + logger.debug("Sending event (encrypted): " + str(event)) + await self.ws.send(self._json_dumps(["EVENT", event])) + + async def waitFor(self, result_type, callback=None, on_error_callback=None, timeout=10): + now = time.time() + while True: + for i in range(len(self.event_queue)): + e = self.event_queue[i] + event_time = e["created_at"] + if e["method"] == result_type: + if event_time > now - timeout: + if not callback or callback(e["result"], e["tags"]): + self.event_queue.pop(i) + if e["error"]: + if on_error_callback: + on_error_callback(e["error"], e["tags"]) + + return e["result"], e["tags"], e["error"] + else: + return e["result"], e["tags"], None + await asyncio.sleep(1) + if timeout > 0 and time.time() > now + timeout: + raise Exception("Timeout") + + + +@pytest.mark.asyncio +async def test_create(): + await check_services() + nwc = await create_nwc("wallet1", "test_create", ["pay"], [], 0) + logger.info(nwc) + assert nwc["nwc"]["data"]["expires_at"] == 0 + assert nwc["nwc"]["data"]["permissions"] == "pay" + assert nwc["nwc"]["data"]["description"] == "test_create" + assert nwc["nwc"]["data"]["last_used"] > time.time() - 10 + assert nwc["nwc"]["data"]["last_used"] < time.time() + 10 + assert nwc["nwc"]["data"]["created_at"] > time.time() - 10 + assert nwc["nwc"]["data"]["created_at"] < time.time() + 10 + assert len(nwc["nwc"]["budgets"]) == 0 + + +@pytest.mark.asyncio +async def test_make_invoice(): + await check_services() + nwc = await create_nwc("wallet1", "test_make_invoice", ["invoice"], [], 0) + wallet1 = NWCWallet(nwc["pairing"]) + await wallet1.start() + await wallet1.sendEvent("make_invoice", {"amount": 1, "description": "test 123", "expiry": 1000}) + result, tags, error = await wallet1.waitFor("make_invoice") + logger.info(error) + assert error , "Expected internal error, because amount is too low" + + await wallet1.sendEvent("make_invoice", {"amount": 123000, "description":"test 123", "expiry": 1000}) + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + assert result["type"] == "incoming" + assert result["description"] == "test 123" + assert result["amount"] == 123000 + assert result["preimage"] + assert result["created_at"] < time.time() + 10 + assert result["created_at"] > time.time() - 10 + assert result["expires_at"] < time.time() + 1000 + 10 + assert result["expires_at"] > time.time() + assert result["invoice"] + + invoice = result["invoice"] + decoded_invoice = bolt11.decode(invoice) + assert decoded_invoice.amount_msat == 123000 + + await wallet1.close() + + +@pytest.mark.asyncio +async def test_lookup_invoice(): + await check_services() + nwc = await create_nwc("wallet1", "test_lookup_invoice_make", ["invoice"], [], 0) + nwc2 = await create_nwc("wallet1", "test_lookup_invoice_lookup", ["lookup"], [], 0) + + wallet1 = NWCWallet(nwc["pairing"]) + await wallet1.start() + + + await wallet1.sendEvent("make_invoice", {"amount": 123000, "description": "test 123", "expiry": 1000}) + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + assert result["type"] == "incoming" + assert result["description"] == "test 123" + assert result["amount"] == 123000 + assert result["preimage"] + assert result["created_at"] < time.time() + 10 + assert result["created_at"] > time.time() - 10 + assert result["expires_at"] < time.time() + 1000 + 10 + assert result["expires_at"] > time.time() + assert result["invoice"] + + wallet2 = NWCWallet(nwc2["pairing"]) + await wallet2.start() + + await wallet2.sendEvent("lookup_invoice", {"invoice": result["invoice"]}) + result, tags, error = await wallet2.waitFor("lookup_invoice") + assert not error + assert result["type"] == "incoming" + assert result["description"] == "test 123" + assert result["amount"] == 123000 + assert result["preimage"] + assert result["created_at"] < time.time() + 10 + assert result["created_at"] > time.time() - 10 + assert result["expires_at"] < time.time() + 1000 + 10 + assert result["expires_at"] > time.time() + assert result["invoice"] + + await wallet1.close() + await wallet2.close() + + +@pytest.mark.asyncio +async def test_get_info(): + await check_services() + nwc = await create_nwc("wallet1", "test_get_info", ["info"], [], 0) + + + wallet1 = NWCWallet(nwc["pairing"]) + await wallet1.start() + + + await wallet1.sendEvent("get_info", {}) + result, tags, error = await wallet1.waitFor("get_info") + assert not error + assert result["alias"] == "LNBits_NWC_SP" + assert result["color"] == "" + assert result["network"] == "mainnet" + assert result["block_height"] == 0 + assert result["block_hash"] == "" + assert result["methods"] == ["get_info"] + + await wallet1.close() + + +@pytest.mark.asyncio +async def test_permisions(): + await check_services() + nwc = await create_nwc("wallet1", "test_permisions1", ["info"], [], 0) + nwc2 = await create_nwc("wallet1", "test_permisions2", [ "pay", "invoice"], [], 0) + nwc3 = await create_nwc("wallet1", "test_permisions3", ["info" , "pay", "invoice"], [], 0) + + + wallet1 = NWCWallet(nwc["pairing"]) + wallet2 = NWCWallet(nwc2["pairing"]) + wallet3 = NWCWallet(nwc3["pairing"]) + await wallet1.start() + + + await wallet1.sendEvent("get_info", {}) + result, tags, error = await wallet1.waitFor("get_info") + assert not error + + await wallet1.sendEvent("make_invoice", { + "amount": 123000, + "description": "test 123", + "expiry": 1000 + }) + result, tags, error = await wallet1.waitFor("make_invoice") + assert error + + await wallet1.close() + await wallet2.start() + + await wallet2.sendEvent("get_info", {}) + result, tags, error = await wallet2.waitFor("get_info") + assert error + + + await wallet2.sendEvent("make_invoice", { + "amount": 123000, + "description": "test 123", + "expiry": 1000 + }) + result, tags, error = await wallet2.waitFor("make_invoice") + assert not error + + await wallet2.close() + await wallet3.start() + + await wallet3.sendEvent("get_info", {}) + result, tags, error = await wallet3.waitFor("get_info") + assert not error + assert "make_invoice" in result["methods"] + assert "pay_invoice" in result["methods"] + assert "get_info" in result["methods"] + + await wallet3.close() + + +@pytest.mark.asyncio +async def test_pay_invoice_and_balance(): + await check_services() + nwc = await create_nwc("wallet1", "test_pay_invoice_and_balance", ["invoice", "balance"], [], 0) + nwc2 = await create_nwc("wallet2", "test_pay_invoice_and_balance", ["pay", "balance"], [], 0) + + + wallet1 = NWCWallet(nwc["pairing"]) + await wallet1.start() + + + await refresh_wallet_balances() + wallet1_balance = wallets["wallet1"]["balance_msats"] + wallet2_balance = wallets["wallet2"]["balance_msats"] + + + await wallet1.sendEvent("make_invoice", { + "amount": 123000, + "description": "test 123" + }) + + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + assert result["invoice"] + + invoice = result["invoice"] + wallet2 = NWCWallet(nwc2["pairing"]) + await wallet2.start() + + await wallet2.sendEvent("pay_invoice", { + "invoice": invoice + }) + result, tags, error = await wallet2.waitFor("pay_invoice") + assert not error + assert result["preimage"] + + await refresh_wallet_balances() + wallet1_balance_new = wallets["wallet1"]["balance_msats"] + wallet2_balance_new = wallets["wallet2"]["balance_msats"] + + assert wallet1_balance_new == wallet1_balance + 123000 + assert wallet2_balance_new == wallet2_balance - 123000 + + await wallet1.sendEvent("get_balance", {}) + result, tags, error = await wallet1.waitFor("get_balance") + assert not error + assert result["balance"] == wallet1_balance_new + + await wallet2.sendEvent("get_balance", {}) + result, tags, error = await wallet2.waitFor("get_balance") + assert not error + assert result["balance"] == wallet2_balance_new + + await wallet1.close() + await wallet2.close() + + +@pytest.mark.asyncio +async def test_multi_pay_invoices(): + nwc1 = await create_nwc("wallet1", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0) + nwc2 = await create_nwc("wallet2", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0) + nwc3 = await create_nwc("wallet3", "test_multi_pay_invoices", ["invoice", "pay", "balance"], [], 0) + + wallet1 = NWCWallet(nwc1["pairing"]) + wallet2 = NWCWallet(nwc2["pairing"]) + wallet3 = NWCWallet(nwc3["pairing"]) + + await wallet1.start() + await wallet2.start() + await wallet3.start() + + await refresh_wallet_balances() + wallet1_balance = wallets["wallet1"]["balance_msats"] + wallet2_balance = wallets["wallet2"]["balance_msats"] + wallet3_balance = wallets["wallet3"]["balance_msats"] + + await wallet1.sendEvent("make_invoice", { + "amount": 123000, + "description": "test 123" + }) + + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + assert result["invoice"] + invoice1 = result["invoice"] + + await wallet1.sendEvent("make_invoice", { + "amount": 123000, + "description": "test 123" + }) + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + assert result["invoice"] + invoice2 = result["invoice"] + + await wallet2.sendEvent("make_invoice", { + "amount": 123000, + "description": "test 123" + }) + result, tags, error = await wallet2.waitFor("make_invoice") + assert not error + assert result["invoice"] + invoice3 = result["invoice"] + + await wallet3.sendEvent("multi_pay_invoice", { + "invoices":[ + {"id":"invoice1", "invoice": invoice1, "amount": 123000}, + {"id":"invoice2", "invoice": invoice2, "amount": 123000}, + { "invoice": invoice3} + ] + }) + result, tags, error = await wallet3.waitFor("multi_pay_invoice") + assert not error + d_tag = [t[1] for t in tags if t[0] == "d"][0] + if d_tag=="invoice1": + assert result["preimage"] + elif d_tag=="invoice2": + assert result["preimage"] + elif d_tag==invoice3: + assert result["preimage"] + else: + assert False + + + await refresh_wallet_balances() + wallet1_balance_new = wallets["wallet1"]["balance_msats"] + wallet2_balance_new = wallets["wallet2"]["balance_msats"] + wallet3_balance_new = wallets["wallet3"]["balance_msats"] + + assert wallet1_balance_new == wallet1_balance + 123000 + 123000 + assert wallet2_balance_new == wallet2_balance + 123000 + assert wallet3_balance_new == wallet3_balance - 123000 - 123000 - 123000 + + + + await wallet1.sendEvent("get_balance", {}) + result, tags, error = await wallet1.waitFor("get_balance") + assert not error + assert result["balance"] == wallet1_balance_new + + await wallet2.sendEvent("get_balance", {}) + result, tags, error = await wallet2.waitFor("get_balance") + assert not error + assert result["balance"] == wallet2_balance_new + + await wallet3.sendEvent("get_balance", {}) + result, tags, error = await wallet3.waitFor("get_balance") + assert not error + assert result["balance"] == wallet3_balance_new + + await wallet1.close() + await wallet2.close() + await wallet3.close() + + + + + + +@pytest.mark.asyncio +async def test_insufficient_balance(): + nwc1 = await create_nwc("wallet1", "test_insufficient_balance", ["invoice", "pay", "balance"], [], 0) + nwc2 = await create_nwc("wallet2", "test_insufficient_balance", ["invoice", "pay", "balance"], [], 0) + await refresh_wallet_balances() + wallet1_balance = wallets["wallet1"]["balance_msats"] + amount_to_spend = wallet1_balance + 1000 + wallet1 = NWCWallet(nwc1["pairing"]) + wallet2 = NWCWallet(nwc2["pairing"]) + await wallet1.start() + await wallet2.start() + + await wallet2.sendEvent("make_invoice", { + "amount": amount_to_spend, + "description": "test 123" + }) + result, tags, error = await wallet2.waitFor("make_invoice") + assert not error + assert result["invoice"] + invoice = result["invoice"] + + await wallet1.sendEvent("pay_invoice", { + "invoice": invoice + }) + result, tags, error = await wallet1.waitFor("pay_invoice") + logger.info(error) + logger.info(result) + logger.info(amount_to_spend) + + assert error + # The proper error code should be INSUFFICIENT_BALANCE + # but we use the more generic PAYMENT_FAILED in our implementation for simplicity + #assert error["code"] == "INSUFFICIENT_BALANCE" + assert error["code"] == "PAYMENT_FAILED" + + await wallet1.close() + await wallet2.close() + + + +@pytest.mark.asyncio +async def test_expiry(): + nwc = await create_nwc("wallet3", "test_expiry", ["invoice", "pay", "balance"], [], 1) + await asyncio.sleep(2) + wallet3 = NWCWallet(nwc["pairing"]) + await wallet3.start() + await wallet3.sendEvent("make_invoice", { + "amount": 123000, + "description": "test 123" + }) + result, tags, error = await wallet3.waitFor("make_invoice") + assert error + assert error["code"] == "UNAUTHORIZED" , "Expected UNAUTHORIZED error, because the NWC expired" + await wallet3.close() + + + + +@pytest.mark.asyncio +async def test_budget(): + nwc1 = await create_nwc("wallet1", "test_expiry", ["invoice", "pay", "balance"], [], 0) + nwc3 = await create_nwc("wallet3", "test_expiry", ["invoice", "pay", "balance"], [ + { + "budget_msats": 100000, + "refresh_window": 3600, + "created_at": time.time() + } + ], 0) + wallet1 = NWCWallet(nwc1["pairing"]) + wallet3 = NWCWallet(nwc3["pairing"]) + await wallet3.start() + await wallet1.start() + await wallet1.sendEvent("make_invoice", { + "amount": 101000, + "description": "Invalid" + }) + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + + await wallet3.sendEvent("pay_invoice", { + "invoice": result["invoice"] + }) + result, tags, error = await wallet3.waitFor("pay_invoice") + assert error + assert error["code"] == "QUOTA_EXCEEDED" , "Expected QUOTA_EXCEEDED error, because the budget was exceeded" + + await wallet1.sendEvent("make_invoice", { + "amount": 99000, + "description": "Valid" + }) + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + + await wallet3.sendEvent("pay_invoice", { + "invoice": result["invoice"] + }) + result, tags, error = await wallet3.waitFor("pay_invoice") + assert not error, "Expected successful payment, because the budget was not exceeded" + assert result["preimage"] + + await wallet1.sendEvent("make_invoice", { + "amount": 100000-99000+1000, + "description": "Invalid" + }) + + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + + await wallet3.sendEvent("pay_invoice", { + "invoice": result["invoice"] + }) + result, tags, error = await wallet3.waitFor("pay_invoice") + assert error + assert error["code"] == "QUOTA_EXCEEDED" , "Expected QUOTA_EXCEEDED error, because the budget was exceeded" + + await wallet3.close() + await wallet1.close() + + +@pytest.mark.asyncio +async def test_budget_refresh(): + nwc1 = await create_nwc("wallet1", "test_expiry", ["invoice", "pay", "balance"], [], 0) + nwc3 = await create_nwc("wallet3", "test_expiry", ["invoice", "pay", "balance"], [ { + "budget_msats": 100000, + "refresh_window": 5, + "created_at": time.time() + }], 0) + wallet1 = NWCWallet(nwc1["pairing"]) + wallet3 = NWCWallet(nwc3["pairing"]) + await wallet3.start() + await wallet1.start() + await wallet1.sendEvent("make_invoice", { + "amount": 100000, + "description": "Invalid" + }) + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + + await wallet1.sendEvent("make_invoice", { + "amount": 100000, + "description": "Invalid" + }) + result2, tags, error = await wallet1.waitFor("make_invoice") + assert not error + + await wallet3.sendEvent("pay_invoice", { + "invoice": result["invoice"] + }) + result, tags, error = await wallet3.waitFor("pay_invoice") + assert not error, "Expected successful payment, because the budget was not exceeded" + + + await wallet3.sendEvent("pay_invoice", { + "invoice": result2["invoice"] + }) + result, tags, error = await wallet3.waitFor("pay_invoice") + assert error + assert error["code"] == "QUOTA_EXCEEDED", "Expected QUOTA_EXCEEDED error, because the budget was exceeded" + + await asyncio.sleep(5) + await wallet1.sendEvent("make_invoice", { + "amount": 100000, + "description": "Valid" + }) + result, tags, error = await wallet1.waitFor("make_invoice") + assert not error + + + await wallet3.sendEvent("pay_invoice", { + "invoice": result["invoice"] + }) + result, tags, error = await wallet3.waitFor("pay_invoice") + assert not error, "Expected successful payment, because the budget was refreshed" + + await wallet3.close() + await wallet1.close() + + + + +