tests
This commit is contained in:
parent
1427fad9d5
commit
7fbaf437d6
11 changed files with 1335 additions and 5 deletions
|
|
@ -24,7 +24,7 @@
|
|||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.pythonPath": "/opt/python/bin/python3.9"
|
||||
// "python.pythonPath": "/opt/python/bin/python3.9"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#!/bin/bash
|
||||
CONTAINER_WORKSPACE_FOLDER=$1
|
||||
cd $CONTAINER_WORKSPACE_FOLDER
|
||||
sudo apt update -y
|
||||
sudo apt install -y python3.9-distutils curl
|
||||
sudo apt-get install -y docker.io
|
||||
|
|
@ -8,14 +9,13 @@ curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
|
|||
sudo bash /tmp/nodesource_setup.sh
|
||||
sudo apt-get install -y nodejs
|
||||
if [ ! -d lnbits ] ; then
|
||||
sudo git clone https://github.com/lnbits/lnbits.git;
|
||||
git clone https://github.com/lnbits/lnbits.git;
|
||||
fi
|
||||
sudo chown 1000:1000 -Rvf lnbits
|
||||
cd lnbits
|
||||
git checkout 0.12.8
|
||||
poetry env use python3.9
|
||||
VENV_PATH=$(poetry env info -p)
|
||||
sudo ln -s $VENV_PATH /opt/python
|
||||
# export VENV_PATH=$(poetry env info -p)
|
||||
# sudo ln -s $VENV_PATH /opt/python
|
||||
make bundle
|
||||
poetry install --no-interaction
|
||||
mkdir -p lnbits/extensions/
|
||||
|
|
|
|||
48
.github/workflows/test.yml
vendored
Normal file
48
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
|
||||
jobs:
|
||||
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: |
|
||||
cd lnbits
|
||||
poetry_env_path=$(poetry env info --path)
|
||||
source $poetry_env_path/bin/activate
|
||||
cd ..
|
||||
pytest tests/unit/*.py -s
|
||||
|
||||
- name: Setup integration tests
|
||||
run: bash tests/integration/start.sh
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd lnbits
|
||||
poetry_env_path=$(poetry env info --path)
|
||||
source $poetry_env_path/bin/activate
|
||||
cd ..
|
||||
pytest tests/unit/*.py -s
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,4 @@
|
|||
__pycache__
|
||||
lnbits
|
||||
tests/integration/strfry-data
|
||||
tests/integration/lnbits_itest_data
|
||||
252
tests/integration/.env
Normal file
252
tests/integration/.env
Normal file
|
|
@ -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 "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", 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
|
||||
1
tests/integration/.v039fk_lnbits_integration_test_folder
Normal file
1
tests/integration/.v039fk_lnbits_integration_test_folder
Normal file
|
|
@ -0,0 +1 @@
|
|||
yes v039fk_lnbits_integration_test_folder
|
||||
BIN
tests/integration/data.zip
Normal file
BIN
tests/integration/data.zip
Normal file
Binary file not shown.
44
tests/integration/start.sh
Normal file
44
tests/integration/start.sh
Normal file
|
|
@ -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
|
||||
|
||||
|
||||
6
tests/integration/stop.sh
Normal file
6
tests/integration/stop.sh
Normal file
|
|
@ -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
|
||||
138
tests/integration/strfry.conf
Normal file
138
tests/integration/strfry.conf
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
838
tests/integration/test_all.py
Normal file
838
tests/integration/test_all.py
Normal file
|
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue