Add dev dependencies and regtest test environments

This commit is contained in:
Reckless_Satoshi 2023-11-12 12:39:39 +00:00 committed by Reckless_Satoshi
parent b4fe30e733
commit ebd0a287c3
16 changed files with 485 additions and 144 deletions

View File

@ -17,61 +17,66 @@ concurrency:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
DEVELOPMENT: 1
strategy: strategy:
max-parallel: 4 max-parallel: 4
matrix: matrix:
python-version: ["3.11.6", "3.12"] python-version: ["3.11.6", "3.12"]
lnd-version: ["v0.17.0-beta","v0.17.1-beta.rc1"]
services:
db:
image: postgres:14.2
env:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps: steps:
- name: 'Checkout' - name: 'Checkout'
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 'Set up Python ${{ matrix.python-version }}' - name: 'Compose Eegtest Orchestration'
uses: actions/setup-python@v4 uses: isbang/compose-action@v1.5.1
with: with:
python-version: ${{ matrix.python-version }} compose-file: "docker-test.yml"
env: "tests/compose.env"
- name: 'Cache pip dependencies' # - name: 'Set up Python ${{ matrix.python-version }}'
uses: actions/cache@v3 # uses: actions/setup-python@v4
with: # with:
path: ~/.cache/pip # python-version: ${{ matrix.python-version }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: 'Install Python Dependencies' # - name: 'Cache pip dependencies'
run: | # uses: actions/cache@v3
python -m pip install --upgrade pip # with:
pip install -r requirements.txt # path: ~/.cache/pip
# key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
# restore-keys: |
# ${{ runner.os }}-pip-
- name: 'Install LND/CLN gRPC Dependencies' # - name: 'Install Python Dependencies'
run: bash ./scripts/generate_grpc.sh # run: |
# python -m pip install --upgrade pip
# pip install -r requirements.txt
# pip install -r requirements_dev.txt
- name: 'Create .env File' # - name: 'Install LND/CLN gRPC Dependencies'
run: | # run: bash ./scripts/generate_grpc.sh
mv .env-sample .env
sed -i "s/USE_TOR='True'/USE_TOR='False'/" .env
- name: 'Wait for PostgreSQL to become ready' # - name: 'Create .env File'
run: | # run: |
sudo apt-get install -y postgresql-client # mv .env-sample .env
until pg_isready -h localhost -p 5432 -U postgres; do sleep 2; done # sed -i "s/USE_TOR=True/USE_TOR=False/" .env
# - name: 'Wait for PostgreSQL to become ready'
# run: |
# sudo apt-get install -y postgresql-client
# until pg_isready -h localhost -p 5432 -U postgres; do sleep 2; done
- name: 'Run tests with coverage' - name: 'Run tests with coverage'
run: | run: |
pip install coverage docker exec coordinator coverage run manage.py test
coverage run manage.py test docker exec coordinator coverage report
coverage report
# jobs:
# test:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - name: Run Docker Compose
# run: |
# docker-compose up -d
# docker-compose run web python manage.py test

View File

@ -37,7 +37,7 @@ except Exception:
# Read macaroon from file or .env variable string encoded as base64 # Read macaroon from file or .env variable string encoded as base64
try: try:
with open(os.path.join(config("LND_DIR"), config("MACAROON_path")), "rb") as f: with open(os.path.join(config("LND_DIR"), config("MACAROON_PATH")), "rb") as f:
MACAROON = f.read() MACAROON = f.read()
except Exception: except Exception:
MACAROON = b64decode(config("LND_MACAROON_BASE64")) MACAROON = b64decode(config("LND_MACAROON_BASE64"))
@ -49,7 +49,7 @@ MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500_000)
# Logger function used to build tests/mocks/lnd.py # Logger function used to build tests/mocks/lnd.py
def log(name, request, response): def log(name, request, response):
if not config("LOG_LND", cast=bool, default=True): if not config("LOG_LND", cast=bool, default=False):
return return
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"######################################\nEvent: {name}\nTime: {current_time}\nRequest:\n{request}\nResponse:\n{response}\nType: {type(response)}\n" log_message = f"######################################\nEvent: {name}\nTime: {current_time}\nRequest:\n{request}\nResponse:\n{response}\nType: {type(response)}\n"

View File

@ -10,7 +10,7 @@ from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
if config("COORDINATOR_TESTING", cast=bool, default=False): if config("TESTING", cast=bool, default=False):
import random import random
import string import string

View File

@ -28,8 +28,8 @@ class InfoSerializer(serializers.Serializer):
lifetime_volume = serializers.FloatField( lifetime_volume = serializers.FloatField(
help_text="Total volume in BTC since exchange's inception" help_text="Total volume in BTC since exchange's inception"
) )
lnd_version = serializers.CharField() lnd_version = serializers.CharField(required=False)
cln_version = serializers.CharField() cln_version = serializers.CharField(required=False)
robosats_running_commit_hash = serializers.CharField() robosats_running_commit_hash = serializers.CharField()
alternative_site = serializers.CharField() alternative_site = serializers.CharField()
alternative_name = serializers.CharField() alternative_name = serializers.CharField()
@ -170,11 +170,15 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"- **'Inactive'** (seen more than 10 min ago)\n\n" "- **'Inactive'** (seen more than 10 min ago)\n\n"
"Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty", "Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty",
) )
taker_status = serializers.BooleanField( taker_status = serializers.CharField(
required=False, required=False,
help_text="True if you are either a taker or maker, False otherwise", help_text="Status of the maker:\n"
"- **'Active'** (seen within last 2 min)\n"
"- **'Seen Recently'** (seen within last 10 min)\n"
"- **'Inactive'** (seen more than 10 min ago)\n\n"
"Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty",
) )
price_now = serializers.IntegerField( price_now = serializers.FloatField(
required=False, required=False,
help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)", help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)",
) )
@ -274,11 +278,11 @@ class OrderDetailSerializer(serializers.ModelSerializer):
required=False, required=False,
help_text="in percentage, the swap fee rate the platform charges", help_text="in percentage, the swap fee rate the platform charges",
) )
latitude = serializers.CharField( latitude = serializers.FloatField(
required=False, required=False,
help_text="Latitude of the order for F2F payments", help_text="Latitude of the order for F2F payments",
) )
longitude = serializers.CharField( longitude = serializers.FloatField(
required=False, required=False,
help_text="Longitude of the order for F2F payments", help_text="Longitude of the order for F2F payments",
) )

206
docker-tests.yml Normal file
View File

@ -0,0 +1,206 @@
# Spin up a regtest lightning network to run integration tests
# docker-compose -f docker-tests.yml --env-file tests/compose.env up -d
# Some useful handy commands that hopefully are never needed
# docker exec -it btc bitcoin-cli -chain=regtest -rpcpassword=test -rpcuser=test createwallet default
# docker exec -it btc bitcoin-cli -chain=regtest -rpcpassword=test -rpcuser=test -generate 101
# docker exec -it coordinator-lnd lncli --network=regtest getinfo
# docker exec -it robot-lnd lncli --network=regtest --rpcserver localhost:10010 getinfo
version: '3.9'
services:
bitcoind:
image: ruimarinho/bitcoin-core:${BITCOIND_TAG}
container_name: btc
restart: always
ports:
- "8000:8000"
volumes:
- bitcoin:/bitcoin/.bitcoin/
command:
--txindex=1
--printtoconsole
--regtest=1
--server=1
--rest=1
--rpcuser=test
--rpcpassword=test
--logips=1
--debug=1
--rpcport=18443
--rpcallowip=172.0.0.0/8
--rpcallowip=192.168.0.0/16
--zmqpubrawblock=tcp://0.0.0.0:28332
--zmqpubrawtx=tcp://0.0.0.0:28333
--listenonion=0
coordinator-lnd:
image: lightninglabs/lnd:${LND_TAG}
container_name: coordinator-lnd
restart: always
volumes:
- bitcoin:/root/.bitcoin/
- lnd:/home/lnd/.lnd
- lnd:/root/.lnd
command:
--noseedbackup
--nobootstrap
--restlisten=localhost:8081
--no-rest-tls
--debuglevel=debug
--maxpendingchannels=10
--rpclisten=0.0.0.0:10009
--listen=0.0.0.0:9735
--color=#4126a7
--alias=RoboSats
--bitcoin.active
--bitcoin.regtest
--bitcoin.node=bitcoind
--bitcoind.rpchost=127.0.0.1
--bitcoind.rpcuser=test
--bitcoind.rpcpass=test
--bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332
--bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333
--protocol.wumbo-channels
depends_on:
- bitcoind
network_mode: service:bitcoind
robot-lnd:
image: lightninglabs/lnd:${LND_TAG}
container_name: robot-lnd
restart: always
volumes:
- bitcoin:/root/.bitcoin/
- lndrobot:/home/lnd/.lnd
- lndrobot:/root/.lnd
command:
--noseedbackup
--nobootstrap
--no-rest-tls
--debuglevel=debug
--maxpendingchannels=10
--rpclisten=0.0.0.0:10010
--listen=0.0.0.0:9736
--color=#4126a7
--alias=Robot
--bitcoin.active
--bitcoin.regtest
--bitcoin.node=bitcoind
--bitcoind.rpchost=127.0.0.1
--bitcoind.rpcuser=test
--bitcoind.rpcpass=test
--bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332
--bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333
--protocol.wumbo-channels
depends_on:
- bitcoind
network_mode: service:bitcoind
redis:
image: redis:${REDIS_TAG}
container_name: redis
restart: always
volumes:
- redisdata:/data
network_mode: service:bitcoind
coordinator:
build: .
image: robosats-image
container_name: coordinator
restart: always
environment:
DEVELOPMENT: True
TESTING: True
USE_TOR: False
MACAROON_PATH: 'data/chain/bitcoin/regtest/admin.macaroon'
env_file:
- ${ROBOSATS_ENVS_FILE}
depends_on:
- redis
- coordinator-lnd
- postgres
network_mode: service:bitcoind
volumes:
- .:/usr/src/robosats
- lnd:/lnd
- lndrobot:/lndrobot
- cln:/cln
postgres:
image: postgres:${POSTGRES_TAG:-14.2-alpine}
container_name: sql
restart: always
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_DB: ${POSTGRES_DB}
network_mode: service:bitcoind
# clean-orders:
# image: robosats-image
# restart: always
# container_name: clord
# command: python3 manage.py clean_orders
# environment:
# SKIP_COLLECT_STATIC: "true"
# POSTGRES_HOST: 'postgres'
# env_file:
# - ${ROBOSATS_ENVS_FILE}
# follow-invoices:
# image: robosats-image
# container_name: invo
# restart: always
# env_file:
# - ${ROBOSATS_ENVS_FILE}
# environment:
# SKIP_COLLECT_STATIC: "true"
# POSTGRES_HOST: 'postgres'
# command: python3 manage.py follow_invoices
# telegram-watcher:
# image: robosats-image
# container_name: tg
# restart: always
# environment:
# SKIP_COLLECT_STATIC: "true"
# POSTGRES_HOST: 'postgres'
# env_file:
# - ${ROBOSATS_ENVS_FILE}
# command: python3 manage.py telegram_watcher
# celery:
# image: robosats-image
# container_name: cele
# restart: always
# env_file:
# - ${ROBOSATS_ENVS_FILE}
# environment:
# SKIP_COLLECT_STATIC: "true"
# POSTGRES_HOST: 'postgres'
# command: celery -A robosats worker --loglevel=WARNING
# depends_on:
# - redis
# celery-beat:
# image: robosats-image
# container_name: beat
# restart: always
# env_file:
# - ${ROBOSATS_ENVS_FILE}
# environment:
# SKIP_COLLECT_STATIC: "true"
# POSTGRES_HOST: 'postgres'
# command: celery -A robosats beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
# depends_on:
# - redis
volumes:
redisdata:
bitcoin:
lnd:
cln:
lndrobot:

View File

@ -26,9 +26,5 @@ python-gnupg==0.5.1
daphne==4.0.0 daphne==4.0.0
drf-spectacular==0.26.2 drf-spectacular==0.26.2
drf-spectacular-sidecar==2023.5.1 drf-spectacular-sidecar==2023.5.1
black==23.3.0
isort==5.12.0
flake8==6.1.0
pyflakes==3.1.0
django-cors-headers==4.3.0 django-cors-headers==4.3.0
base91==1.0.1 base91==1.0.1

7
requirements_dev.txt Normal file
View File

@ -0,0 +1,7 @@
black==23.3.0
isort==5.12.0
flake8==6.1.0
pyflakes==3.1.0
coverage==7.3.2
drf-openapi-tester==2.3.3
pre-commit==3.5.0

View File

@ -161,6 +161,8 @@ class RobotTokenSHA256AuthenticationMiddleWare:
resized_img.save(f, format="WEBP", quality=80) resized_img.save(f, format="WEBP", quality=80)
user.robot.avatar = "static/assets/avatars/" + nickname + ".webp" user.robot.avatar = "static/assets/avatars/" + nickname + ".webp"
update_last_login(None, user)
user.save() user.save()
response = self.get_response(request) response = self.get_response(request)

View File

@ -10,6 +10,12 @@ else
python manage.py collectstatic --noinput python manage.py collectstatic --noinput
fi fi
# Collect static files
if [ $DEVELOPMENT ]; then
echo "Installing python development dependencies"
pip install -r requirements_dev.txt
fi
# Print first start up message when pb2/grpc files if they do exist # Print first start up message when pb2/grpc files if they do exist
if [ ! -f "/usr/src/robosats/api/lightning/lightning_pb2.py" ]; then if [ ! -f "/usr/src/robosats/api/lightning/lightning_pb2.py" ]; then
echo "Looks like the first run of this container. pb2 and gRPC files were not detected on the attached volume, copying them into the attached volume /robosats/api/lightning ." echo "Looks like the first run of this container. pb2 and gRPC files were not detected on the attached volume, copying them into the attached volume /robosats/api/lightning ."

View File

@ -1161,12 +1161,10 @@ components:
- alternative_site - alternative_site
- bond_size - bond_size
- book_liquidity - book_liquidity
- cln_version
- current_swap_fee_rate - current_swap_fee_rate
- last_day_nonkyc_btc_premium - last_day_nonkyc_btc_premium
- last_day_volume - last_day_volume
- lifetime_volume - lifetime_volume
- lnd_version
- maker_fee - maker_fee
- network - network
- node_alias - node_alias
@ -1486,10 +1484,17 @@ components:
Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty
taker_status: taker_status:
type: boolean type: string
description: True if you are either a taker or maker, False otherwise description: |-
Status of the maker:
- **'Active'** (seen within last 2 min)
- **'Seen Recently'** (seen within last 10 min)
- **'Inactive'** (seen more than 10 min ago)
Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty
price_now: price_now:
type: integer type: number
format: double
description: Price of the order in the order's currency at the time of request description: Price of the order in the order's currency at the time of request
(upto 5 significant digits) (upto 5 significant digits)
premium_percentile: premium_percentile:
@ -1657,10 +1662,12 @@ components:
description: The network eg. 'testnet', 'mainnet'. Only if status = `14` description: The network eg. 'testnet', 'mainnet'. Only if status = `14`
(Successful Trade) and is_buyer = `true` (Successful Trade) and is_buyer = `true`
latitude: latitude:
type: string type: number
format: double
description: Latitude of the order for F2F payments description: Latitude of the order for F2F payments
longitude: longitude:
type: string type: number
format: double
description: Longitude of the order for F2F payments description: Longitude of the order for F2F payments
required: required:
- expires_at - expires_at

10
tests/compose.env Normal file
View File

@ -0,0 +1,10 @@
ROBOSATS_ENVS_FILE=".env-sample"
BITCOIND_TAG='24.0.1-alpine'
LND_TAG='v0.17.0-beta'
REDIS_TAG='7.2.1-alpine@sha256:7f5a0dfbf379db69dc78434091dce3220e251022e71dcdf36207928cbf9010de'
POSTGRES_TAG='14.2-alpine'
POSTGRES_DB='postgres'
POSTGRES_USER='postgres'
POSTGRES_PASSWORD='example'

View File

@ -1,6 +1,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
# Mock up of CLN gRPC responses # Mock up of CLN gRPC responses
# Unfinished, during integration tests we SHOULD spin up a regtest CLN instance
class MockNodeStub: class MockNodeStub:
@ -31,28 +32,3 @@ class MockNodeStub:
response.binding.address = "127.0.0.1" response.binding.address = "127.0.0.1"
response.binding.port = 9736 response.binding.port = 9736
return response return response
class MockHoldStub:
def __init__(self, channel):
pass
def HoldInvoiceLookup(self, request):
response = MagicMock()
return response
def HoldInvoice(self, request):
response = MagicMock()
return response
def HoldInvoiceSettle(self, request):
response = MagicMock()
return response
def HoldInvoiceCancel(self, request):
response = MagicMock()
return response
def DecodeBolt11(self, request):
response = MagicMock()
return response

View File

@ -1,6 +1,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
# Mock up of LND gRPC responses # Mock up of LND gRPC responses
# Unfinished, during integration tests we spin up a regtest LND instance
class MockLightningStub: class MockLightningStub:
@ -22,7 +23,7 @@ class MockLightningStub:
response = MagicMock() response = MagicMock()
if ( if (
request.pay_req request.pay_req
== "lntb17310n1pj552mdpp50p2utzh7mpsf3uq7u7cws4a96tj3kyq54hchdkpw8zecamx9klrqd2j2pshjmt9de6zqun9vejhyetwvdjn5gphxs6nsvfe893z6wphvfsj6dryvymj6wp5xvuz6wp5xcukvdec8yukgcf49cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x" == "lntb17310n1pj552mdpp50p2utgzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x"
): ):
response.destination = ( response.destination = (
"033b58d7681fe5dd2fb21fd741996cda5449616f77317dd1156b80128d6a71b807" "033b58d7681fe5dd2fb21fd741996cda5449616f77317dd1156b80128d6a71b807"
@ -35,7 +36,9 @@ class MockLightningStub:
response.expiry = 450 response.expiry = 450
response.description = "Payment reference: 7458199b-87ba-4da7-8438-8469f7899da5. This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) unless you cheat or cancel unilaterally." response.description = "Payment reference: 7458199b-87ba-4da7-8438-8469f7899da5. This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) unless you cheat or cancel unilaterally."
response.cltv_expiry = 650 response.cltv_expiry = 650
response.payment_addr = '\275\205\224\016\363\325\262\201\306"8\022e\343\215\355\277\304\021\r\037l\202\023\314\353\334\265\002\036h\322' response.payment_addr = (
"\275\205\224\016\363\325\262\201\353\334\265\002\036h\322"
)
response.num_msat = 1731000 response.num_msat = 1731000
return response return response
@ -86,7 +89,7 @@ class MockInvoicesStub:
def AddHoldInvoice(self, request): def AddHoldInvoice(self, request):
response = MagicMock() response = MagicMock()
# if request.value == 1731: print(request)
response.payment_request = "lntb17310n1pj552mdpp50p2utzh7mpsf3uq7u7cws4a96tj3kyq54hchdkpw8zecamx9klrqd2j2pshjmt9de6zqun9vejhyetwvdjn5gphxs6nsvfe893z6wphvfsj6dryvymj6wp5xvuz6wp5xcukvdec8yukgcf49cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x" response.payment_request = "lntb17310n1pj552mdpp50p2utzh7mpsf3uq7u7cws4a96tj3kyq54hchdkpw8zecamx9klrqd2j2pshjmt9de6zqun9vejhyetwvdjn5gphxs6nsvfe893z6wphvfsj6dryvymj6wp5xvuz6wp5xcukvdec8yukgcf49cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x"
response.add_index = 1 response.add_index = 1
response.payment_addr = b'\275\205\224\016\363\325\262\201\306"8\022e\343\215\355\277\304\021\r\037l\202\023\314\353\334\265\002\036h\322' response.payment_addr = b'\275\205\224\016\363\325\262\201\306"8\022e\343\215\355\277\304\021\r\037l\202\023\314\353\334\265\002\036h\322'

98
tests/node_utils.py Normal file
View File

@ -0,0 +1,98 @@
import requests
from requests.auth import HTTPBasicAuth
from requests.exceptions import ReadTimeout
def get_node(name="robot"):
"""
We have two regtest LND nodes: "coordinator" (the robosats backend) and "robot" (the robosats user)
"""
if name == "robot":
with open("/lndrobot/data/chain/bitcoin/regtest/admin.macaroon", "rb") as f:
macaroon = f.read()
return {"port": 8080, "headers": {"Grpc-Metadata-macaroon": macaroon.hex()}}
elif name == "coordinator":
with open("/lnd/data/chain/bitcoin/regtest/admin.macaroon", "rb") as f:
macaroon = f.read()
return {"port": 8081, "headers": {"Grpc-Metadata-macaroon": macaroon.hex()}}
def get_node_id(node_name):
node = get_node(node_name)
response = requests.get(
f'http://localhost:{node["port"]}/v1/getinfo', headers=node["headers"]
)
data = response.json()
return data["identity_pubkey"]
def connect_to_node(node_name, node_id, ip_port):
node = get_node(node_name)
data = {"addr": {"pubkey": node_id, "host": ip_port}}
response = requests.post(
f'http://localhost:{node["port"]}/v1/peers', json=data, headers=node["headers"]
)
return response.json()
def open_channel(node_name, node_id, local_funding_amount, push_sat):
node = get_node(node_name)
data = {
"node_pubkey_string": node_id,
"local_funding_amount": local_funding_amount,
"push_sat": push_sat,
}
response = requests.post(
f'http://localhost:{node["port"]}/v1/channels',
json=data,
headers=node["headers"],
)
return response.json()
def create_address(node_name):
node = get_node(node_name)
response = requests.get(
f'http://localhost:{node["port"]}/v1/newaddress', headers=node["headers"]
)
return response.json()["address"]
def generate_blocks(address, num_blocks):
data = {
"jsonrpc": "1.0",
"id": "curltest",
"method": "generatetoaddress",
"params": [num_blocks, address],
}
response = requests.post(
"http://localhost:18443", json=data, auth=HTTPBasicAuth("test", "test")
)
return response.json()
def pay_invoice(node_name, invoice):
node = get_node(node_name)
data = {"payment_request": invoice}
try:
requests.post(
f'http://localhost:{node["port"]}/v1/channels/transactions',
json=data,
headers=node["headers"],
timeout=1,
)
except ReadTimeout:
# Request to pay hodl invoice has timed out: that's good!
return
def add_invoice(node_name, amount):
node = get_node(node_name)
data = {"value": amount}
response = requests.post(
f'http://localhost:{node["port"]}/v1/invoices',
json=data,
headers=node["headers"],
)
return response.json()["payment_request"]

View File

@ -1,5 +1,4 @@
import json import json
from unittest.mock import patch
from decouple import config from decouple import config
from django.conf import settings from django.conf import settings
@ -7,8 +6,6 @@ from django.contrib.auth.models import User
from django.test import Client from django.test import Client
from django.urls import reverse from django.urls import reverse
from tests.mocks.cln import MockNodeStub
from tests.mocks.lnd import MockVersionerStub
from tests.test_api import BaseAPITestCase from tests.test_api import BaseAPITestCase
FEE = config("FEE", cast=float, default=0.2) FEE = config("FEE", cast=float, default=0.2)
@ -31,8 +28,6 @@ class CoordinatorInfoTest(BaseAPITestCase):
self.client = Client() self.client = Client()
User.objects.create_superuser(self.su_name, "super@user.com", self.su_pass) User.objects.create_superuser(self.su_name, "super@user.com", self.su_pass)
@patch("api.lightning.cln.node_pb2_grpc.NodeStub", MockNodeStub)
@patch("api.lightning.lnd.verrpc_pb2_grpc.VersionerStub", MockVersionerStub)
def test_info(self): def test_info(self):
path = reverse("info") path = reverse("info")
@ -56,7 +51,9 @@ class CoordinatorInfoTest(BaseAPITestCase):
) )
self.assertEqual(data["version"], settings.VERSION) self.assertEqual(data["version"], settings.VERSION)
self.assertEqual(data["node_id"], NODE_ID) self.assertEqual(data["node_id"], NODE_ID)
self.assertEqual(data["network"], "testnet") self.assertEqual(
data["network"], "testnet"
) # tests take place in regtest, but this attribute is read from .env
self.assertAlmostEqual(data["maker_fee"], MAKER_FEE) self.assertAlmostEqual(data["maker_fee"], MAKER_FEE)
self.assertAlmostEqual(data["taker_fee"], TAKER_FEE) self.assertAlmostEqual(data["taker_fee"], TAKER_FEE)
self.assertAlmostEqual(data["bond_size"], BOND_SIZE) self.assertAlmostEqual(data["bond_size"], BOND_SIZE)

View File

@ -1,7 +1,7 @@
import json import json
import time
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
from unittest.mock import patch
from decouple import config from decouple import config
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -10,10 +10,13 @@ from django.urls import reverse
from api.management.commands.follow_invoices import Command as FollowInvoices from api.management.commands.follow_invoices import Command as FollowInvoices
from api.models import Currency, Order from api.models import Currency, Order
from api.tasks import cache_market from api.tasks import cache_market
from tests.mocks.cln import MockHoldStub # , MockNodeStub from tests.node_utils import (
from tests.mocks.lnd import ( # MockRouterStub,; MockSignerStub,; MockVersionerStub, connect_to_node,
MockInvoicesStub, create_address,
MockLightningStub, generate_blocks,
get_node_id,
open_channel,
pay_invoice,
) )
from tests.test_api import BaseAPITestCase from tests.test_api import BaseAPITestCase
@ -57,6 +60,21 @@ class TradeTest(BaseAPITestCase):
# Fetch currency prices from external APIs # Fetch currency prices from external APIs
cache_market() cache_market()
# Fund two LN nodes in regtest and open channels
coordinator_node_id = get_node_id("coordinator")
connect_to_node("robot", coordinator_node_id, "localhost:9735")
funding_address = create_address("robot")
generate_blocks(funding_address, 101)
time.sleep(
2
) # channels cannot be created until the node is fully sync. We just created 101 blocks.
open_channel("robot", coordinator_node_id, 100_000_000, 50_000_000)
# Generate 6 blocks so the channel becomes active
generate_blocks(funding_address, 6)
def test_login_superuser(self): def test_login_superuser(self):
""" """
Test the login functionality for the superuser. Test the login functionality for the superuser.
@ -65,7 +83,9 @@ class TradeTest(BaseAPITestCase):
data = {"username": self.su_name, "password": self.su_pass} data = {"username": self.su_name, "password": self.su_pass}
response = self.client.post(path, data) response = self.client.post(path, data)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertResponse(response) self.assertResponse(
response
) # should skip given that /coordinator/login is not documented
def test_cache_market(self): def test_cache_market(self):
""" """
@ -102,13 +122,15 @@ class TradeTest(BaseAPITestCase):
else: else:
headers = {"HTTP_AUTHORIZATION": f"Token {b91_token}"} headers = {"HTTP_AUTHORIZATION": f"Token {b91_token}"}
return headers, pub_key, enc_priv_key return headers
def assert_robot(self, response, pub_key, enc_priv_key, robot_index): def assert_robot(self, response, robot_index):
""" """
Assert that the robot is created correctly. Assert that the robot is created correctly.
""" """
nickname = read_file(f"tests/robots/{robot_index}/nickname") nickname = read_file(f"tests/robots/{robot_index}/nickname")
pub_key = read_file(f"tests/robots/{robot_index}/pub_key")
enc_priv_key = read_file(f"tests/robots/{robot_index}/enc_priv_key")
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
@ -148,18 +170,17 @@ class TradeTest(BaseAPITestCase):
Creates the robots in /tests/robots/{robot_index} Creates the robots in /tests/robots/{robot_index}
""" """
path = reverse("robot") path = reverse("robot")
headers, pub_key, enc_priv_key = self.get_robot_auth(robot_index, True) headers = self.get_robot_auth(robot_index, True)
response = self.client.get(path, **headers) return self.client.get(path, **headers)
self.assert_robot(response, pub_key, enc_priv_key, robot_index)
def test_create_robots(self): def test_create_robots(self):
""" """
Test the creation of two robots to be used in the trade tests Test the creation of two robots to be used in the trade tests
""" """
self.create_robot(robot_index=1) for robot_index in [1, 2]:
self.create_robot(robot_index=2) response = self.create_robot(robot_index)
self.assert_robot(response, robot_index)
def make_order(self, maker_form, robot_index=1): def make_order(self, maker_form, robot_index=1):
""" """
@ -167,7 +188,7 @@ class TradeTest(BaseAPITestCase):
""" """
path = reverse("make") path = reverse("make")
# Get valid robot auth headers # Get valid robot auth headers
headers, _, _ = self.get_robot_auth(robot_index, True) headers = self.get_robot_auth(robot_index, True)
response = self.client.post(path, maker_form, **headers) response = self.client.post(path, maker_form, **headers)
return response return response
@ -181,6 +202,8 @@ class TradeTest(BaseAPITestCase):
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
# Checks # Checks
self.assertResponse(response)
self.assertIsInstance(data["id"], int, "Order ID is not an integer") self.assertIsInstance(data["id"], int, "Order ID is not an integer")
self.assertEqual( self.assertEqual(
data["status"], data["status"],
@ -255,13 +278,10 @@ class TradeTest(BaseAPITestCase):
return data return data
@patch("api.lightning.cln.hold_pb2_grpc.HoldStub", MockHoldStub)
@patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub)
@patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub)
def get_order(self, order_id, robot_index=1, first_encounter=False): def get_order(self, order_id, robot_index=1, first_encounter=False):
path = reverse("order") path = reverse("order")
params = f"?order_id={order_id}" params = f"?order_id={order_id}"
headers, _, _ = self.get_robot_auth(robot_index, first_encounter) headers = self.get_robot_auth(robot_index, first_encounter)
response = self.client.get(path + params, **headers) response = self.client.get(path + params, **headers)
return response return response
@ -279,6 +299,8 @@ class TradeTest(BaseAPITestCase):
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertEqual(data["id"], order_made_data["id"]) self.assertEqual(data["id"], order_made_data["id"])
self.assertTrue( self.assertTrue(
isinstance(datetime.fromisoformat(data["created_at"]), datetime) isinstance(datetime.fromisoformat(data["created_at"]), datetime)
@ -301,13 +323,8 @@ class TradeTest(BaseAPITestCase):
self.assertFalse(data["maker_locked"]) self.assertFalse(data["maker_locked"])
self.assertFalse(data["taker_locked"]) self.assertFalse(data["taker_locked"])
self.assertFalse(data["escrow_locked"]) self.assertFalse(data["escrow_locked"])
self.assertEqual(
data["bond_invoice"],
"lntb17310n1pj552mdpp50p2utzh7mpsf3uq7u7cws4a96tj3kyq54hchdkpw8zecamx9klrqd2j2pshjmt9de6zqun9vejhyetwvdjn5gphxs6nsvfe893z6wphvfsj6dryvymj6wp5xvuz6wp5xcukvdec8yukgcf49cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x",
)
self.assertTrue(isinstance(data["bond_satoshis"], int)) self.assertTrue(isinstance(data["bond_satoshis"], int))
@patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub)
def check_for_locked_bonds(self): def check_for_locked_bonds(self):
# A background thread checks every 5 second the status of invoices. We invoke directly during test. # A background thread checks every 5 second the status of invoices. We invoke directly during test.
# It will ask LND via gRPC. In our test, the request/response from LND is mocked, and it will return fake invoice status "ACCEPTED" # It will ask LND via gRPC. In our test, the request/response from LND is mocked, and it will return fake invoice status "ACCEPTED"
@ -320,7 +337,11 @@ class TradeTest(BaseAPITestCase):
order_made_data = json.loads(order_made_response.content.decode()) order_made_data = json.loads(order_made_response.content.decode())
# Maker's first order fetch. Should trigger maker bond hold invoice generation. # Maker's first order fetch. Should trigger maker bond hold invoice generation.
self.get_order(order_made_data["id"]) response = self.get_order(order_made_data["id"])
invoice = response.json()["bond_invoice"]
# Lock the invoice from the robot's node
pay_invoice("robot", invoice)
# Check for invoice locked (the mocked LND will return ACCEPTED) # Check for invoice locked (the mocked LND will return ACCEPTED)
self.check_for_locked_bonds() self.check_for_locked_bonds()
@ -336,6 +357,8 @@ class TradeTest(BaseAPITestCase):
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertEqual(data["id"], data["id"]) self.assertEqual(data["id"], data["id"])
self.assertEqual(data["status_message"], Order.Status(Order.Status.PUB).label) self.assertEqual(data["status_message"], Order.Status(Order.Status.PUB).label)
self.assertTrue(data["maker_locked"]) self.assertTrue(data["maker_locked"])
@ -352,13 +375,13 @@ class TradeTest(BaseAPITestCase):
self.assertTrue(isinstance(public_data["price_now"], float)) self.assertTrue(isinstance(public_data["price_now"], float))
self.assertTrue(isinstance(data["satoshis_now"], int)) self.assertTrue(isinstance(data["satoshis_now"], int))
@patch("api.lightning.cln.hold_pb2_grpc.HoldStub", MockHoldStub) # @patch("api.lightning.cln.hold_pb2_grpc.HoldStub", MockHoldStub)
@patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub) # @patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub)
@patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub) # @patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub)
def take_order(self, order_id, amount, robot_index=2): def take_order(self, order_id, amount, robot_index=2):
path = reverse("order") path = reverse("order")
params = f"?order_id={order_id}" params = f"?order_id={order_id}"
headers, _, _ = self.get_robot_auth(robot_index, first_encounter=True) headers = self.get_robot_auth(robot_index, first_encounter=True)
body = {"action": "take", "amount": amount} body = {"action": "take", "amount": amount}
response = self.client.post(path + params, body, **headers) response = self.client.post(path + params, body, **headers)
@ -372,28 +395,29 @@ class TradeTest(BaseAPITestCase):
response = self.take_order(data_publised["id"], take_amount, taker_index) response = self.take_order(data_publised["id"], take_amount, taker_index)
return response return response
# def test_make_and_take_order(self): def test_make_and_take_order(self):
# maker_index = 1 maker_index = 1
# taker_index = 2 taker_index = 2
# maker_form = self.maker_form_with_range maker_form = self.maker_form_with_range
# self.create_robot(taker_index) #### WEEEE SHOULD NOT BE NEEDED >??? WHY ROBOT HAS NO LOGIN TIME??
# response = self.make_and_take_order(maker_form, 80, maker_index, taker_index)
# data = json.loads(response.content.decode())
# print(data) response = self.make_and_take_order(maker_form, 80, maker_index, taker_index)
data = json.loads(response.content.decode())
# self.assertEqual( self.assertEqual(response.status_code, 200)
# data["ur_nick"], read_file(f"tests/robots/{taker_index}/nickname") self.assertResponse(response)
# )
# self.assertEqual( self.assertEqual(
# data["taker_nick"], read_file(f"tests/robots/{taker_index}/nickname") data["ur_nick"], read_file(f"tests/robots/{taker_index}/nickname")
# ) )
# self.assertEqual( self.assertEqual(
# data["maker_nick"], read_file(f"tests/robots/{maker_index}/nickname") data["taker_nick"], read_file(f"tests/robots/{taker_index}/nickname")
# ) )
# self.assertFalse(data["is_maker"]) self.assertEqual(
# self.assertTrue(data["is_taker"]) data["maker_nick"], read_file(f"tests/robots/{maker_index}/nickname")
# self.assertTrue(data["is_participant"]) )
self.assertFalse(data["is_maker"])
self.assertTrue(data["is_taker"])
self.assertTrue(data["is_participant"])
# a = { # a = {
# "maker_status": "Active", # "maker_status": "Active",