From 3e0d451e97a1e7cec643444b1d515920e8fab944 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Wed, 15 Nov 2023 19:48:04 +0000 Subject: [PATCH] Add tests for onchain address, pgp sign verification. Improve Dockerfile --- .github/workflows/integration-tests.yml | 1 - Dockerfile | 6 + api/lightning/cln.py | 8 + api/lightning/lnd.py | 3 +- api/tests/test_utils.py | 4 +- control/models.py | 3 + docker-compose.yml | 7 +- docker-tests.yml | 10 +- tests/node_utils.py | 97 ++++++++-- tests/pgp_utils.py | 22 +++ tests/robots/1/b91_token | 2 +- tests/robots/1/enc_priv_key | 30 +-- tests/robots/1/nickname | 2 +- tests/robots/1/pub_key | 22 +-- tests/robots/1/signed_message | 10 +- tests/robots/1/token | 2 +- tests/test_trade_pipeline.py | 238 ++++++++++++++++-------- 17 files changed, 335 insertions(+), 132 deletions(-) create mode 100644 tests/pgp_utils.py diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 588ff539..e4135204 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -32,7 +32,6 @@ jobs: - name: Patch Dockerfile and .env-sample run: | sed -i "1s/FROM python:.*/FROM python:${{ matrix.python-tag }}/" Dockerfile - sed -i '/RUN pip install --no-cache-dir -r requirements.txt/a COPY requirements_dev.txt .\nRUN pip install --no-cache-dir -r requirements_dev.txt' Dockerfile sed -i "s/^LNVENDOR=.*/LNVENDOR='${{ matrix.ln-vendor }}'/" .env-sample - uses: satackey/action-docker-layer-caching@v0.0.11 diff --git a/Dockerfile b/Dockerfile index 266e49b7..6bb5930b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ FROM python:3.11.6-slim-bookworm ARG DEBIAN_FRONTEND=noninteractive +ARG DEVELOPMENT=False RUN mkdir -p /usr/src/robosats WORKDIR /usr/src/robosats @@ -17,6 +18,11 @@ RUN python -m pip install --upgrade pip COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt +COPY requirements_dev.txt ./ +RUN if [ "$DEVELOPMENT" = "true" ]; then \ + pip install --no-cache-dir -r requirements_dev.txt; \ + fi + # copy current dir's content to container's WORKDIR root i.e. all the contents of the robosats app COPY . . diff --git a/api/lightning/cln.py b/api/lightning/cln.py index ef81f7d8..956b9818 100755 --- a/api/lightning/cln.py +++ b/api/lightning/cln.py @@ -79,6 +79,14 @@ class CLNNode: except Exception as e: print(f"Cannot get CLN node id: {e}") + @classmethod + def newaddress(cls): + """Only used on tests to fund the regtest node""" + nodestub = node_pb2_grpc.NodeStub(cls.node_channel) + request = node_pb2.NewaddrRequest() + response = nodestub.NewAddr(request) + return response.bech32 + @classmethod def decode_payreq(cls, invoice): """Decodes a lightning payment request (invoice)""" diff --git a/api/lightning/lnd.py b/api/lightning/lnd.py index bf831fa9..2d2438db 100644 --- a/api/lightning/lnd.py +++ b/api/lightning/lnd.py @@ -105,10 +105,11 @@ class LNDNode: lightningstub = lightning_pb2_grpc.LightningStub(cls.channel) request = lightning_pb2.GetInfoRequest() response = lightningstub.GetInfo(request) - log("lightning_pb2_grpc.GetInfo", request, response) if response.testnet: dummy_address = "tb1qehyqhruxwl2p5pt52k6nxj4v8wwc3f3pg7377x" + elif response.chains[0].network == "regtest": + dummy_address = "bcrt1q3w8xja7knmycsglnxg2xzjq8uv9u7jdwau25nl" else: dummy_address = "bc1qgxwaqe4m9mypd7ltww53yv3lyxhcfnhzzvy5j3" # We assume segwit. Use hardcoded address as shortcut so there is no need of user inputs yet. diff --git a/api/tests/test_utils.py b/api/tests/test_utils.py index e5c4e7dd..0cf427ca 100644 --- a/api/tests/test_utils.py +++ b/api/tests/test_utils.py @@ -168,8 +168,8 @@ class TestUtils(TestCase): def test_validate_pgp_keys(self): # Example test client generated GPG keys - client_pub_key = r"-----BEGIN PGP PUBLIC KEY BLOCK-----\\xjMEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0\mUR0SKqLmdjNTFJvYm9TYXRzIElEIDU1MmRkMWE2NjFhN2FjYTRhNDFmODg5\MTBmZjM0YWMzYjFhYzgwYmI3Nzk0ZWQ5ZmQ1NWQ4Yjc2Yjk3YWFkOTfCjAQQ\FgoAPgWCZTWJ1wQLCQcICZA3N7au4gi/zgMVCAoEFgACAQIZAQKbAwIeARYh\BO5iBLnj0J/E6sntEDc3tq7iCL/OAADkVwEA/tBt9FPqrxLHOPFtyUypppr0\/t6vrl3RrLzCLqqE1nUA/0fmhir2F88KcsxmCJwADo/FglwXGFkjrV4sP6Fj\YBEBzjgEZTWJ1xIKKwYBBAGXVQEFAQEHQCyUIe3sQTaYa/IFNKGNmXz/+hrH\ukcot4TOvi2bD9p8AwEIB8J4BBgWCAAqBYJlNYnXCZA3N7au4gi/zgKbDBYh\BO5iBLnj0J/E6sntEDc3tq7iCL/OAACaFAD7BG3E7TkUoWKtJe5OPzTwX+bM\Xy7hbPSQw0zM9Re8KP0BAIeTG8d280dTK63h/seQAKeMj0zf7AYXr0CscvS7\f38D\=h03E\-----END PGP PUBLIC KEY BLOCK-----" - client_enc_priv_key = r"-----BEGIN PGP PRIVATE KEY BLOCK-----\\xYYEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0\mUR0SKqLmdj+CQMICrS3TNCA/LHgxckC+iTUMxkqQJ9GpXWCDacx1rBQCztu\PDgUHNvWdcvW1wWVxU/aJaQLqBTtRVYkJTz332jrKvsSl/LnrfwmUfKgN4nG\Oc1MUm9ib1NhdHMgSUQgNTUyZGQxYTY2MWE3YWNhNGE0MWY4ODkxMGZmMzRh\YzNiMWFjODBiYjc3OTRlZDlmZDU1ZDhiNzZiOTdhYWQ5N8KMBBAWCgA+BYJl\NYnXBAsJBwgJkDc3tq7iCL/OAxUICgQWAAIBAhkBApsDAh4BFiEE7mIEuePQ\n8Tqye0QNze2ruIIv84AAORXAQD+0G30U+qvEsc48W3JTKmmmvT+3q+uXdGs\vMIuqoTWdQD/R+aGKvYXzwpyzGYInAAOj8WCXBcYWSOtXiw/oWNgEQHHiwRl\NYnXEgorBgEEAZdVAQUBAQdALJQh7exBNphr8gU0oY2ZfP/6Gse6Ryi3hM6+\LZsP2nwDAQgH/gkDCPPoYWyzm4mT4N/TDBF11GVq0xSEEcubFqjArFKyibRy\TDnB8+o8BlkRuGClcfRyKkR5/Rp1v5B0n1BuMsc8nY4Yg4BJv4KhsPfXRp4m\31zCeAQYFggAKgWCZTWJ1wmQNze2ruIIv84CmwwWIQTuYgS549CfxOrJ7RA3\N7au4gi/zgAAmhQA+wRtxO05FKFirSXuTj808F/mzF8u4Wz0kMNMzPUXvCj9\AQCHkxvHdvNHUyut4f7HkACnjI9M3+wGF69ArHL0u39/Aw==\=1hCT\-----END PGP PRIVATE KEY BLOCK-----" + client_pub_key = r"-----BEGIN PGP PUBLIC KEY BLOCK-----\\mDMEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+LFNH+\sw2raQC0TFJvYm9TYXRzIElEIGVkN2QzYjJiMmU1ODlhYjI2NzIwNjA1ZTc0MTRh\YjRmYmNhMjFjYjRiMzFlNWI0ZTYyYTZmYTUxYzI0YTllYWKIjAQQFgoAPgWCZVO9\bwQLCQcICZAuNFtLSY2XJAMVCAoEFgACAQIZAQKbAwIeARYhBDIhViOFpzWovPuw\vC40W0tJjZckAACTeAEA+AdXmA8p6I+FFqXaFVRh5JRa5ZoO4xhGb+QY00kgZisB\AJee8XdW6FHBj2J3b4M9AYqufdpvuj+lLmaVAshN9U4MuDgEZVO9bxIKKwYBBAGX\VQEFAQEHQORkbvSesg9oJeCRKigTNdQ5tkgmVGXfdz/+vwBIl3E3AwEIB4h4BBgW\CAAqBYJlU71vCZAuNFtLSY2XJAKbDBYhBDIhViOFpzWovPuwvC40W0tJjZckAABZ\1AD/RIJM/WNb28pYqtq4XmeOaqLCrbQs2ua8mXpGBZSl8E0BALWSlbHICYTNy9L6\KV0a5pXbxcXpzejcjpJmVwzuWz8P\=32+r\-----END PGP PUBLIC KEY BLOCK-----" + client_enc_priv_key = r"-----BEGIN PGP PRIVATE KEY BLOCK-----\\xYYEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+L\FNH+sw2raQD+CQMIHkZZZnDa6d/gHioGTKf6JevirkCBWwz8tFLGFs5DFwjD\tI4ew9CJd09AUxfMq2WvTilhMNrdw2nmqtmAoaIyIo43azVT1VQoxSDnWxFv\Tc1MUm9ib1NhdHMgSUQgZWQ3ZDNiMmIyZTU4OWFiMjY3MjA2MDVlNzQxNGFi\NGZiY2EyMWNiNGIzMWU1YjRlNjJhNmZhNTFjMjRhOWVhYsKMBBAWCgA+BYJl\U71vBAsJBwgJkC40W0tJjZckAxUICgQWAAIBAhkBApsDAh4BFiEEMiFWI4Wn\Nai8+7C8LjRbS0mNlyQAAJN4AQD4B1eYDynoj4UWpdoVVGHklFrlmg7jGEZv\5BjTSSBmKwEAl57xd1boUcGPYndvgz0Biq592m+6P6UuZpUCyE31TgzHiwRl\U71vEgorBgEEAZdVAQUBAQdA5GRu9J6yD2gl4JEqKBM11Dm2SCZUZd93P/6/\AEiXcTcDAQgH/gkDCGSRul0JyboW4JZSQVlHNVlx2mrfE1gRTh2R5hJWU9Kg\aw2gET8OwWDYU4F8wKTo/s7BGn+HN4jrZeLw1k/etKUKLzuPC06KUXhj3rMF\Ti3CeAQYFggAKgWCZVO9bwmQLjRbS0mNlyQCmwwWIQQyIVYjhac1qLz7sLwu\NFtLSY2XJAAAWdQA/0SCTP1jW9vKWKrauF5njmqiwq20LNrmvJl6RgWUpfBN\AQC1kpWxyAmEzcvS+ildGuaV28XF6c3o3I6SZlcM7ls/Dw==\=YAfZ\-----END PGP PRIVATE KEY BLOCK-----" # Example valid formatted GPG keys with open("tests/robots/1/pub_key", "r") as file: diff --git a/control/models.py b/control/models.py index b13aaee9..92992db8 100755 --- a/control/models.py +++ b/control/models.py @@ -126,6 +126,9 @@ class BalanceLog(models.Model): def __str__(self): return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}" + class Meta: + get_latest_by = "time" + class Dispute(models.Model): pass diff --git a/docker-compose.yml b/docker-compose.yml index 0310c5c1..ae47c5fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,10 @@ services: network_mode: service:tor backend: - build: . + build: + context: . + args: + DEVELOPMENT: True image: backend-image container_name: django-dev restart: always @@ -30,7 +33,7 @@ services: - lnd - redis environment: - DEVELOPMENT: 1 + DEVELOPMENT: True volumes: - .:/usr/src/robosats - ./node/lnd:/lnd diff --git a/docker-tests.yml b/docker-tests.yml index 3b4abd34..387a9133 100644 --- a/docker-tests.yml +++ b/docker-tests.yml @@ -86,7 +86,7 @@ services: - cln:/root/.lightning - ./docker/cln/plugins/cln-grpc-hold:/root/.lightning/plugins/cln-grpc-hold - bitcoin:/root/.bitcoin - command: --regtest --wumbo --bitcoin-rpcuser=test --bitcoin-rpcpassword=test --rest-host=0.0.0.0 --bind-addr=127.0.0.1:9737 --max-concurrent-htlcs=483 --grpc-port=9999 --grpc-hold-port=9998 --important-plugin=/root/.lightning/plugins/cln-grpc-hold --database-upgrade=true + command: --regtest --wumbo --bitcoin-rpcuser=test --bitcoin-rpcpassword=test --rest-host=0.0.0.0 --rest-port=3010 --bind-addr=127.0.0.1:9737 --max-concurrent-htlcs=483 --grpc-port=9999 --grpc-hold-port=9998 --important-plugin=/root/.lightning/plugins/cln-grpc-hold --database-upgrade=true depends_on: - bitcoind network_mode: service:bitcoind @@ -132,7 +132,10 @@ services: network_mode: service:bitcoind coordinator: - build: . + build: + context: . + args: + DEVELOPMENT: True image: robosats-image container_name: coordinator restart: always @@ -142,6 +145,9 @@ services: USE_TOR: False MACAROON_PATH: 'data/chain/bitcoin/regtest/admin.macaroon' CLN_DIR: '/cln/regtest/' + BITCOIND_RPCURL: 'http://127.0.0.1:18443' + BITCOIND_RPCUSER: 'test' + BITCOIND_RPCPASSWORD: 'test' env_file: - ${ROBOSATS_ENVS_FILE} depends_on: diff --git a/tests/node_utils.py b/tests/node_utils.py index 08a9f188..b7869018 100644 --- a/tests/node_utils.py +++ b/tests/node_utils.py @@ -3,10 +3,12 @@ import sys import time import requests +from decouple import config from requests.auth import HTTPBasicAuth from requests.exceptions import ReadTimeout -wait_step = 0.2 +LNVENDOR = config("LNVENDOR", cast=str, default="LND") +WAIT_STEP = 0.2 def get_node(name="robot"): @@ -59,8 +61,8 @@ def wait_for_lnd_node_sync(node_name): f"\rWaiting for {node_name} node chain sync {round(waited,1)}s" ) sys.stdout.flush() - waited += wait_step - time.sleep(wait_step) + waited += WAIT_STEP + time.sleep(WAIT_STEP) def LND_has_active_channels(node_name): @@ -97,8 +99,8 @@ def wait_for_active_channels(lnvendor, node_name="coordinator"): ) sys.stdout.flush() - waited += wait_step - time.sleep(wait_step) + waited += WAIT_STEP + time.sleep(WAIT_STEP) def wait_for_cln_node_sync(): @@ -112,8 +114,8 @@ def wait_for_cln_node_sync(): f"\rWaiting for coordinator CLN node sync {round(waited,1)}s" ) sys.stdout.flush() - waited += wait_step - time.sleep(wait_step) + waited += WAIT_STEP + time.sleep(WAIT_STEP) else: return @@ -131,8 +133,66 @@ def wait_for_cln_active_channels(): f"\rWaiting for coordinator CLN node channels to be active {round(waited,1)}s" ) sys.stdout.flush() - waited += wait_step - time.sleep(wait_step) + waited += WAIT_STEP + time.sleep(WAIT_STEP) + + +def wait_nodes_sync(): + wait_for_lnd_node_sync("robot") + if LNVENDOR == "LND": + wait_for_lnd_node_sync("coordinator") + elif LNVENDOR == "CLN": + wait_for_cln_node_sync() + + +def wait_channels(): + wait_for_active_channels(LNVENDOR, "coordinator") + wait_for_active_channels("LND", "robot") + + +def set_up_regtest_network(): + if channel_is_active(): + print("Regtest network was already ready. Skipping initalization.") + return + # Fund two LN nodes in regtest and open channels + # Coordinator is either LND or CLN. Robot user is always LND. + if LNVENDOR == "LND": + coordinator_node_id = get_lnd_node_id("coordinator") + coordinator_port = 9735 + elif LNVENDOR == "CLN": + coordinator_node_id = get_cln_node_id() + coordinator_port = 9737 + + print("Coordinator Node ID: ", coordinator_node_id) + + # Fund both robot and coordinator nodes + robot_funding_address = create_address("robot") + coordinator_funding_address = create_address("coordinator") + generate_blocks(coordinator_funding_address, 1) + generate_blocks(robot_funding_address, 101) + wait_nodes_sync() + + # Open channel between Robot user and coordinator + print(f"\nOpening channel from Robot user node to coordinator {LNVENDOR} node") + connect_to_node("robot", coordinator_node_id, f"localhost:{coordinator_port}") + open_channel("robot", coordinator_node_id, 100_000_000, 50_000_000) + + # Generate 10 blocks so the channel becomes active and wait for sync + generate_blocks(robot_funding_address, 10) + + # Wait a tiny bit so payments can be done in the new channel + wait_nodes_sync() + wait_channels() + time.sleep(1) + + +def channel_is_active(): + robot_channel_active = LND_has_active_channels("robot") + if LNVENDOR == "LND": + coordinator_channel_active = LND_has_active_channels("coordinator") + elif LNVENDOR == "CLN": + coordinator_channel_active = CLN_has_active_channels() + return robot_channel_active and coordinator_channel_active def connect_to_node(node_name, node_id, ip_port): @@ -151,7 +211,7 @@ def connect_to_node(node_name, node_id, ip_port): if "already connected to peer" in response.json()["message"]: return response.json() print(f"Could not peer coordinator node: {response.json()}") - time.sleep(wait_step) + time.sleep(WAIT_STEP) def open_channel(node_name, node_id, local_funding_amount, push_sat): @@ -169,7 +229,7 @@ def open_channel(node_name, node_id, local_funding_amount, push_sat): return response.json() -def create_address(node_name): +def create_address_LND(node_name): node = get_node(node_name) response = requests.get( f'http://localhost:{node["port"]}/v1/newaddress', headers=node["headers"] @@ -177,6 +237,19 @@ def create_address(node_name): return response.json()["address"] +def create_address_CLN(): + from api.lightning.cln import CLNNode + + return CLNNode.newaddress() + + +def create_address(node_name): + if node_name == "coordinator" and LNVENDOR == "CLN": + return create_address_CLN() + else: + return create_address_LND(node_name) + + def generate_blocks(address, num_blocks): print(f"Mining {num_blocks} blocks") data = { @@ -199,7 +272,7 @@ def pay_invoice(node_name, invoice): f'http://localhost:{node["port"]}/v1/channels/transactions', json=data, headers=node["headers"], - timeout=1, + timeout=0.3, # 0.15s is enough for LND to LND hodl ACCEPT. ) except ReadTimeout: # Request to pay hodl invoice has timed out: that's good! diff --git a/tests/pgp_utils.py b/tests/pgp_utils.py new file mode 100644 index 00000000..a269ef7e --- /dev/null +++ b/tests/pgp_utils.py @@ -0,0 +1,22 @@ +import gnupg + + +def sign_message(message, private_key_path, passphrase_path): + gpg = gnupg.GPG() + + with open(private_key_path, "r") as f: + private_key = f.read() + + with open(passphrase_path, "r") as f: + passphrase = f.read() + + gpg.import_keys(private_key, passphrase=passphrase) + + # keyid=import_result.fingerprints[0] + signed_message = gpg.sign( + message, passphrase=passphrase, extra_args=["--digest-algo", "SHA512"] + ) + + # [print(name, getattr(signed_message, name)) for name in dir(signed_message) if not callable(getattr(signed_message, name))] + + return signed_message.data.decode(encoding="UTF-8", errors="strict") diff --git a/tests/robots/1/b91_token b/tests/robots/1/b91_token index ce6e527b..697a3679 100644 --- a/tests/robots/1/b91_token +++ b/tests/robots/1/b91_token @@ -1 +1 @@ -qz*fp3CzNfK0Y2MWx;]mLP@:Ka2/t_;no*:0GeGd}j2rSQ{}1qwZCED \ No newline at end of file diff --git a/tests/robots/1/enc_priv_key b/tests/robots/1/enc_priv_key index 1b1cc1e0..3cacd2f7 100644 --- a/tests/robots/1/enc_priv_key +++ b/tests/robots/1/enc_priv_key @@ -1,18 +1,18 @@ -----BEGIN PGP PRIVATE KEY BLOCK----- -xYYEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0 -mUR0SKqLmdj+CQMICrS3TNCA/LHgxckC+iTUMxkqQJ9GpXWCDacx1rBQCztu -PDgUHNvWdcvW1wWVxU/aJaQLqBTtRVYkJTz332jrKvsSl/LnrfwmUfKgN4nG -Oc1MUm9ib1NhdHMgSUQgNTUyZGQxYTY2MWE3YWNhNGE0MWY4ODkxMGZmMzRh -YzNiMWFjODBiYjc3OTRlZDlmZDU1ZDhiNzZiOTdhYWQ5N8KMBBAWCgA+BYJl -NYnXBAsJBwgJkDc3tq7iCL/OAxUICgQWAAIBAhkBApsDAh4BFiEE7mIEuePQ -n8Tqye0QNze2ruIIv84AAORXAQD+0G30U+qvEsc48W3JTKmmmvT+3q+uXdGs -vMIuqoTWdQD/R+aGKvYXzwpyzGYInAAOj8WCXBcYWSOtXiw/oWNgEQHHiwRl -NYnXEgorBgEEAZdVAQUBAQdALJQh7exBNphr8gU0oY2ZfP/6Gse6Ryi3hM6+ -LZsP2nwDAQgH/gkDCPPoYWyzm4mT4N/TDBF11GVq0xSEEcubFqjArFKyibRy -TDnB8+o8BlkRuGClcfRyKkR5/Rp1v5B0n1BuMsc8nY4Yg4BJv4KhsPfXRp4m -31zCeAQYFggAKgWCZTWJ1wmQNze2ruIIv84CmwwWIQTuYgS549CfxOrJ7RA3 -N7au4gi/zgAAmhQA+wRtxO05FKFirSXuTj808F/mzF8u4Wz0kMNMzPUXvCj9 -AQCHkxvHdvNHUyut4f7HkACnjI9M3+wGF69ArHL0u39/Aw== -=1hCT +xYYEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+L +FNH+sw2raQD+CQMIHkZZZnDa6d/gHioGTKf6JevirkCBWwz8tFLGFs5DFwjD +tI4ew9CJd09AUxfMq2WvTilhMNrdw2nmqtmAoaIyIo43azVT1VQoxSDnWxFv +Tc1MUm9ib1NhdHMgSUQgZWQ3ZDNiMmIyZTU4OWFiMjY3MjA2MDVlNzQxNGFi +NGZiY2EyMWNiNGIzMWU1YjRlNjJhNmZhNTFjMjRhOWVhYsKMBBAWCgA+BYJl +U71vBAsJBwgJkC40W0tJjZckAxUICgQWAAIBAhkBApsDAh4BFiEEMiFWI4Wn +Nai8+7C8LjRbS0mNlyQAAJN4AQD4B1eYDynoj4UWpdoVVGHklFrlmg7jGEZv +5BjTSSBmKwEAl57xd1boUcGPYndvgz0Biq592m+6P6UuZpUCyE31TgzHiwRl +U71vEgorBgEEAZdVAQUBAQdA5GRu9J6yD2gl4JEqKBM11Dm2SCZUZd93P/6/ +AEiXcTcDAQgH/gkDCGSRul0JyboW4JZSQVlHNVlx2mrfE1gRTh2R5hJWU9Kg +aw2gET8OwWDYU4F8wKTo/s7BGn+HN4jrZeLw1k/etKUKLzuPC06KUXhj3rMF +Ti3CeAQYFggAKgWCZVO9bwmQLjRbS0mNlyQCmwwWIQQyIVYjhac1qLz7sLwu +NFtLSY2XJAAAWdQA/0SCTP1jW9vKWKrauF5njmqiwq20LNrmvJl6RgWUpfBN +AQC1kpWxyAmEzcvS+ildGuaV28XF6c3o3I6SZlcM7ls/Dw== +=YAfZ -----END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/tests/robots/1/nickname b/tests/robots/1/nickname index 2f448af9..cd9ce49d 100644 --- a/tests/robots/1/nickname +++ b/tests/robots/1/nickname @@ -1 +1 @@ -MyopicRacket333 \ No newline at end of file +UptightPub730 \ No newline at end of file diff --git a/tests/robots/1/pub_key b/tests/robots/1/pub_key index 63b27f17..d2f7a14e 100644 --- a/tests/robots/1/pub_key +++ b/tests/robots/1/pub_key @@ -1,14 +1,14 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -mDMEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0mUR0 -SKqLmdi0TFJvYm9TYXRzIElEIDU1MmRkMWE2NjFhN2FjYTRhNDFmODg5MTBmZjM0 -YWMzYjFhYzgwYmI3Nzk0ZWQ5ZmQ1NWQ4Yjc2Yjk3YWFkOTeIjAQQFgoAPgWCZTWJ -1wQLCQcICZA3N7au4gi/zgMVCAoEFgACAQIZAQKbAwIeARYhBO5iBLnj0J/E6snt -EDc3tq7iCL/OAADkVwEA/tBt9FPqrxLHOPFtyUypppr0/t6vrl3RrLzCLqqE1nUA -/0fmhir2F88KcsxmCJwADo/FglwXGFkjrV4sP6FjYBEBuDgEZTWJ1xIKKwYBBAGX -VQEFAQEHQCyUIe3sQTaYa/IFNKGNmXz/+hrHukcot4TOvi2bD9p8AwEIB4h4BBgW -CAAqBYJlNYnXCZA3N7au4gi/zgKbDBYhBO5iBLnj0J/E6sntEDc3tq7iCL/OAACa -FAD7BG3E7TkUoWKtJe5OPzTwX+bMXy7hbPSQw0zM9Re8KP0BAIeTG8d280dTK63h -/seQAKeMj0zf7AYXr0CscvS7f38D -=+xY8 +mDMEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+LFNH+ +sw2raQC0TFJvYm9TYXRzIElEIGVkN2QzYjJiMmU1ODlhYjI2NzIwNjA1ZTc0MTRh +YjRmYmNhMjFjYjRiMzFlNWI0ZTYyYTZmYTUxYzI0YTllYWKIjAQQFgoAPgWCZVO9 +bwQLCQcICZAuNFtLSY2XJAMVCAoEFgACAQIZAQKbAwIeARYhBDIhViOFpzWovPuw +vC40W0tJjZckAACTeAEA+AdXmA8p6I+FFqXaFVRh5JRa5ZoO4xhGb+QY00kgZisB +AJee8XdW6FHBj2J3b4M9AYqufdpvuj+lLmaVAshN9U4MuDgEZVO9bxIKKwYBBAGX +VQEFAQEHQORkbvSesg9oJeCRKigTNdQ5tkgmVGXfdz/+vwBIl3E3AwEIB4h4BBgW +CAAqBYJlU71vCZAuNFtLSY2XJAKbDBYhBDIhViOFpzWovPuwvC40W0tJjZckAABZ +1AD/RIJM/WNb28pYqtq4XmeOaqLCrbQs2ua8mXpGBZSl8E0BALWSlbHICYTNy9L6 +KV0a5pXbxcXpzejcjpJmVwzuWz8P +=32+r -----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/robots/1/signed_message b/tests/robots/1/signed_message index 40127497..6be10ef6 100644 --- a/tests/robots/1/signed_message +++ b/tests/robots/1/signed_message @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 -test +bcrt1qrrvml8tr4lkwlqpg9g394tye6s5950qf9tj9e9 -----BEGIN PGP SIGNATURE----- -wnUEARYKACcFgmU22/EJkDc3tq7iCL/OFiEE7mIEuePQn8Tqye0QNze2ruII -v84AAJDMAP9JXQJNRYUiPaSroIfmfJccPQeaVuHTnl0fJqLToL6GbAD/Rt7c -Y67Co6RJi70vytMorPKWmiX6C/mrnKL0auQC8gQ= -=1ouc +iHUEARYIAB0WIQQyIVYjhac1qLz7sLwuNFtLSY2XJAUCZVUUTQAKCRAuNFtLSY2X +JA4zAP9PW71ZvQglGnexa9LYryVbnI0w3WnWXYaOmowy/aMM5wD/a2xZNk95DiDq +s8PnKT41yS+QIBrn7+iZ2DqlCjKdNgc= +=NOcM -----END PGP SIGNATURE----- diff --git a/tests/robots/1/token b/tests/robots/1/token index ab4a4085..b9bc8e05 100644 --- a/tests/robots/1/token +++ b/tests/robots/1/token @@ -1 +1 @@ -gUNa4xT98AA2AQWj4hsdCWFixOmvReu5If3R \ No newline at end of file +C2etfi7nPeUD7rCcwAOy4XoLvEAxbTRGSK6H \ No newline at end of file diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index ee0e0dc3..ef5c6354 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -1,5 +1,4 @@ import json -import time from datetime import datetime from decimal import Decimal @@ -10,25 +9,17 @@ from django.urls import reverse from api.management.commands.follow_invoices import Command as FollowInvoices from api.models import Currency, Order from api.tasks import cache_market +from control.models import BalanceLog from control.tasks import compute_node_balance from tests.node_utils import ( - CLN_has_active_channels, - LND_has_active_channels, - connect_to_node, + add_invoice, create_address, - generate_blocks, - get_cln_node_id, - get_lnd_node_id, - open_channel, pay_invoice, - wait_for_active_channels, - wait_for_cln_node_sync, - wait_for_lnd_node_sync, + set_up_regtest_network, ) +from tests.pgp_utils import sign_message from tests.test_api import BaseAPITestCase -LNVENDOR = config("LNVENDOR", cast=str, default="LND") - def read_file(file_path): """ @@ -58,25 +49,6 @@ class TradeTest(BaseAPITestCase): "longitude": 135.503, } - def wait_nodes_sync(): - wait_for_lnd_node_sync("robot") - if LNVENDOR == "LND": - wait_for_lnd_node_sync("coordinator") - elif LNVENDOR == "CLN": - wait_for_cln_node_sync() - - def wait_channels(): - wait_for_active_channels("LND", "robot") - wait_for_active_channels(LNVENDOR, "coordinator") - - def channel_is_active(): - robot_channel_active = LND_has_active_channels("robot") - if LNVENDOR == "LND": - coordinator_channel_active = LND_has_active_channels("coordinator") - elif LNVENDOR == "CLN": - coordinator_channel_active = CLN_has_active_channels() - return robot_channel_active and coordinator_channel_active - @classmethod def setUpTestData(cls): """ @@ -88,40 +60,8 @@ class TradeTest(BaseAPITestCase): # Fetch currency prices from external APIs cache_market() - # Skip node setup and channel creation if both nodes have an active channel already - if cls.channel_is_active(): - print("Regtest network was already ready. Skipping initalization.") - # Take the first node balances snapshot - compute_node_balance() - return - - # Fund two LN nodes in regtest and open channels - # Coordinator is either LND or CLN. Robot user is always LND. - if LNVENDOR == "LND": - coordinator_node_id = get_lnd_node_id("coordinator") - coordinator_port = 9735 - elif LNVENDOR == "CLN": - coordinator_node_id = get_cln_node_id() - coordinator_port = 9737 - - print("Coordinator Node ID: ", coordinator_node_id) - - funding_address = create_address("robot") - generate_blocks(funding_address, 101) - cls.wait_nodes_sync() - - # Open channel between Robot user and coordinator - print(f"\nOpening channel from Robot user node to coordinator {LNVENDOR} node") - connect_to_node("robot", coordinator_node_id, f"localhost:{coordinator_port}") - open_channel("robot", coordinator_node_id, 100_000_000, 50_000_000) - - # Generate 10 blocks so the channel becomes active and wait for sync - generate_blocks(funding_address, 10) - - # Wait a tiny bit so payments can be done in the new channel - cls.wait_nodes_sync() - cls.wait_channels() - time.sleep(1) + # Initialize bitcoin core, mine some blocks, connect nodes, open channel + set_up_regtest_network() # Take the first node balances snapshot compute_node_balance() @@ -155,6 +95,25 @@ class TradeTest(BaseAPITestCase): usd.timestamp, datetime, "External price timestamp is not a datetime" ) + def test_initial_balance_log(self): + """ + Test if the initial node BalanceLog is correct. + One channel should exist with 0.5BTC in local. + No onchain balance should exist. + """ + balance_log = BalanceLog.objects.latest() + + self.assertIsInstance(balance_log.time, datetime) + self.assertTrue(balance_log.total > 0) + self.assertTrue(balance_log.ln_local > 0) + self.assertEqual(balance_log.ln_local_unsettled, 0) + self.assertTrue(balance_log.ln_remote > 0) + self.assertEqual(balance_log.ln_remote_unsettled, 0) + self.assertTrue(balance_log.onchain_total > 0) + self.assertTrue(balance_log.onchain_confirmed > 0) + self.assertEqual(balance_log.onchain_unconfirmed, 0) + self.assertTrue(balance_log.onchain_fraction > 0) + def get_robot_auth(self, robot_index, first_encounter=False): """ Create an AUTH header that embeds token, pub_key, and enc_priv_key into a single string @@ -365,12 +324,8 @@ class TradeTest(BaseAPITestCase): self.assertResponse(response) self.assertEqual(data["id"], order_made_data["id"]) - self.assertTrue( - isinstance(datetime.fromisoformat(data["created_at"]), datetime) - ) - self.assertTrue( - isinstance(datetime.fromisoformat(data["expires_at"]), datetime) - ) + self.assertIsInstance(datetime.fromisoformat(data["created_at"]), datetime) + self.assertIsInstance(datetime.fromisoformat(data["expires_at"]), datetime) self.assertTrue(data["is_maker"]) self.assertTrue(data["is_participant"]) self.assertTrue(data["is_buyer"]) @@ -382,11 +337,11 @@ class TradeTest(BaseAPITestCase): self.assertEqual( data["ur_nick"], read_file(f"tests/robots/{robot_index}/nickname") ) - self.assertTrue(isinstance(data["satoshis_now"], int)) + self.assertIsInstance(data["satoshis_now"], int) self.assertFalse(data["maker_locked"]) self.assertFalse(data["taker_locked"]) self.assertFalse(data["escrow_locked"]) - self.assertTrue(isinstance(data["bond_satoshis"], int)) + self.assertIsInstance(data["bond_satoshis"], int) # Cancel order to avoid leaving pending HTLCs after a successful test self.cancel_order(data["id"]) @@ -441,8 +396,8 @@ class TradeTest(BaseAPITestCase): public_data = json.loads(public_response.content.decode()) self.assertFalse(public_data["is_participant"]) - self.assertTrue(isinstance(public_data["price_now"], float)) - self.assertTrue(isinstance(data["satoshis_now"], int)) + self.assertIsInstance(public_data["price_now"], float) + self.assertIsInstance(data["satoshis_now"], int) # Cancel order to avoid leaving pending HTLCs after a successful test self.cancel_order(data["id"]) @@ -533,6 +488,7 @@ class TradeTest(BaseAPITestCase): taker_index = 2 maker_form = self.maker_form_buy_with_range + # Taker GET response = self.make_and_lock_contract(maker_form, 80, maker_index, taker_index) data = json.loads(response.content.decode()) @@ -547,6 +503,26 @@ class TradeTest(BaseAPITestCase): self.assertTrue(data["taker_locked"]) self.assertFalse(data["escrow_locked"]) + # Maker GET + response = self.get_order(data["id"], maker_index) + data = json.loads(response.content.decode()) + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual(data["status_message"], Order.Status(Order.Status.WF2).label) + self.assertTrue(data["swap_allowed"]) + self.assertIsInstance(data["suggested_mining_fee_rate"], int) + self.assertIsInstance(data["swap_fee_rate"], float) + self.assertTrue(data["suggested_mining_fee_rate"] > 0) + self.assertTrue(data["swap_fee_rate"] > 0) + self.assertEqual(data["maker_status"], "Active") + self.assertEqual(data["taker_status"], "Active") + self.assertTrue(data["is_participant"]) + self.assertTrue(data["maker_locked"]) + self.assertTrue(data["taker_locked"]) + self.assertFalse(data["escrow_locked"]) + # Cancel order to avoid leaving pending HTLCs after a successful test self.cancel_order(data["id"]) @@ -561,8 +537,6 @@ class TradeTest(BaseAPITestCase): # Maker's first order fetch. Should trigger maker bond hold invoice generation. response = self.get_order(locked_taker_response_data["id"], taker_index) - print("HEREEEEEEEEEEEEEEEEEEEEEEREEEEEEEEEEEEEEEE") - print(response.json()) invoice = response.json()["escrow_invoice"] # Lock the invoice from the robot's node @@ -597,3 +571,111 @@ class TradeTest(BaseAPITestCase): # Cancel order to avoid leaving pending HTLCs after a successful test self.cancel_order(data["id"], 2) + + def submit_payout_address(self, order_id, robot_index=1): + path = reverse("order") + params = f"?order_id={order_id}" + headers = self.get_robot_auth(robot_index) + + payout_address = create_address("robot") + signed_payout_address = sign_message( + payout_address, + passphrase_path=f"tests/robots/{robot_index}/token", + private_key_path=f"tests/robots/{robot_index}/enc_priv_key", + ) + body = {"action": "update_address", "address": signed_payout_address} + + response = self.client.post(path + params, body, **headers) + + return response + + def trade_to_submitted_address( + self, maker_form, take_amount=80, maker_index=1, taker_index=2 + ): + response_escrow_locked = self.trade_to_locked_escrow( + maker_form, take_amount, maker_index, taker_index + ) + response = self.submit_payout_address( + response_escrow_locked.json()["id"], maker_index + ) + return response + + def test_trade_to_submitted_address(self): + """ + Tests a trade from order creation until escrow locked, before + invoice/address is submitted by buyer. + """ + maker_index = 1 + taker_index = 2 + maker_form = self.maker_form_buy_with_range + + response = self.trade_to_submitted_address( + maker_form, 80, maker_index, taker_index + ) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label) + + self.assertFalse(data["is_fiat_sent"]) + + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"]) + + def submit_payout_invoice(self, order_id, num_satoshis, robot_index=1): + path = reverse("order") + params = f"?order_id={order_id}" + headers = self.get_robot_auth(robot_index) + + payout_invoice = add_invoice("robot", num_satoshis) + signed_payout_invoice = sign_message( + payout_invoice, + passphrase_path=f"tests/robots/{robot_index}/token", + private_key_path=f"tests/robots/{robot_index}/enc_priv_key", + ) + body = {"action": "update_invoice", "invoice": signed_payout_invoice} + + response = self.client.post(path + params, body, **headers) + + return response + + def trade_to_submitted_invoice( + self, maker_form, take_amount=80, maker_index=1, taker_index=2 + ): + response_escrow_locked = self.trade_to_locked_escrow( + maker_form, take_amount, maker_index, taker_index + ) + + response_get = self.get_order(response_escrow_locked.json()["id"], maker_index) + + response = self.submit_payout_invoice( + response_escrow_locked.json()["id"], + response_get.json()["trade_satoshis"], + maker_index, + ) + return response + + def test_trade_to_submitted_invoice(self): + """ + Tests a trade from order creation until escrow locked, before + invoice/address is submitted by buyer. + """ + maker_index = 1 + taker_index = 2 + maker_form = self.maker_form_buy_with_range + + response = self.trade_to_submitted_invoice( + maker_form, 80, maker_index, taker_index + ) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label) + self.assertFalse(data["is_fiat_sent"]) + + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"])