From c8e3fe5128448ab205ef164f46774d86abef7017 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 14 Nov 2023 23:07:28 +0000 Subject: [PATCH 1/6] Add lock escrow tests --- api/serializers.py | 5 + docker-tests.yml | 2 +- docs/assets/schemas/api-latest.yaml | 3 + tests/node_utils.py | 46 +++++-- tests/test_trade_pipeline.py | 195 +++++++++++++++++++++++----- 5 files changed, 202 insertions(+), 49 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 43419c239..9e60dd1e8 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -350,6 +350,10 @@ class OrderDetailSerializer(serializers.ModelSerializer): required=False, help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`", ) + chat_last_index = serializers.IntegerField( + required=False, + help_text="The index of the last message sent in the trade chatroom", + ) class Meta: model = Order @@ -431,6 +435,7 @@ class Meta: "network", "latitude", "longitude", + "chat_last_index", ) diff --git a/docker-tests.yml b/docker-tests.yml index 89ee98664..3b4abd349 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 --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 --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 diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index 3599b2d43..8229ea8dd 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -1671,6 +1671,9 @@ components: type: number format: double description: Longitude of the order for F2F payments + chat_last_index: + type: integer + description: The index of the last message sent in the trade chatroom required: - expires_at - id diff --git a/tests/node_utils.py b/tests/node_utils.py index 5987a069a..08a9f188d 100644 --- a/tests/node_utils.py +++ b/tests/node_utils.py @@ -63,22 +63,42 @@ def wait_for_lnd_node_sync(node_name): time.sleep(wait_step) -def wait_for_lnd_active_channels(node_name): +def LND_has_active_channels(node_name): node = get_node(node_name) + response = requests.get( + f'http://localhost:{node["port"]}/v1/getinfo', headers=node["headers"] + ) + return True if response.json()["num_active_channels"] > 0 else False + + +def CLN_has_active_channels(): + from api.lightning.cln import CLNNode + + response = CLNNode.get_info() + return True if response.num_active_channels > 0 else False + + +def wait_for_active_channels(lnvendor, node_name="coordinator"): waited = 0 while True: - response = requests.get( - f'http://localhost:{node["port"]}/v1/getinfo', headers=node["headers"] - ) - if response.json()["num_active_channels"] > 0: - return - else: - sys.stdout.write( - f"\rWaiting for {node_name} node channels to be active {round(waited,1)}s" - ) - sys.stdout.flush() - waited += wait_step - time.sleep(wait_step) + if lnvendor == "LND": + if LND_has_active_channels(node_name): + return + else: + sys.stdout.write( + f"\rWaiting for {node_name} LND node channel to be active {round(waited,1)}s" + ) + elif lnvendor == "CLN": + if CLN_has_active_channels(): + return + else: + sys.stdout.write( + f"\rWaiting for {node_name} CLN node channel to be active {round(waited,1)}s" + ) + + sys.stdout.flush() + waited += wait_step + time.sleep(wait_step) def wait_for_cln_node_sync(): diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index 9e87cc1cb..ee0e0dc34 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -10,7 +10,10 @@ from api.management.commands.follow_invoices import Command as FollowInvoices from api.models import Currency, Order from api.tasks import cache_market +from control.tasks import compute_node_balance from tests.node_utils import ( + CLN_has_active_channels, + LND_has_active_channels, connect_to_node, create_address, generate_blocks, @@ -18,9 +21,8 @@ get_lnd_node_id, open_channel, pay_invoice, - wait_for_cln_active_channels, + wait_for_active_channels, wait_for_cln_node_sync, - wait_for_lnd_active_channels, wait_for_lnd_node_sync, ) from tests.test_api import BaseAPITestCase @@ -40,7 +42,7 @@ class TradeTest(BaseAPITestCase): su_pass = "12345678" su_name = config("ESCROW_USERNAME", cast=str, default="admin") - maker_form_with_range = { + maker_form_buy_with_range = { "type": Order.Types.BUY, "currency": 1, "has_range": True, @@ -63,12 +65,17 @@ def wait_nodes_sync(): elif LNVENDOR == "CLN": wait_for_cln_node_sync() - def wait_active_channels(): - wait_for_lnd_active_channels("robot") + 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": - wait_for_lnd_active_channels("coordinator") + coordinator_channel_active = LND_has_active_channels("coordinator") elif LNVENDOR == "CLN": - wait_for_cln_active_channels() + coordinator_channel_active = CLN_has_active_channels() + return robot_channel_active and coordinator_channel_active @classmethod def setUpTestData(cls): @@ -81,6 +88,13 @@ def setUpTestData(cls): # 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": @@ -106,9 +120,12 @@ def setUpTestData(cls): # Wait a tiny bit so payments can be done in the new channel cls.wait_nodes_sync() - cls.wait_active_channels() + cls.wait_channels() time.sleep(1) + # Take the first node balances snapshot + compute_node_balance() + def test_login_superuser(self): """ Test the login functionality for the superuser. @@ -231,7 +248,7 @@ def test_make_order(self): """ Test the creation of an order. """ - maker_form = self.maker_form_with_range + maker_form = self.maker_form_buy_with_range response = self.make_order(maker_form, robot_index=1) data = json.loads(response.content.decode()) @@ -320,9 +337,21 @@ def get_order(self, order_id, robot_index=1, first_encounter=False): return response + def cancel_order(self, order_id, robot_index=1): + path = reverse("order") + params = f"?order_id={order_id}" + headers = self.get_robot_auth(robot_index) + body = {"action": "cancel"} + response = self.client.post(path + params, body, **headers) + + return response + def test_get_order_created(self): - # Make an order - maker_form = self.maker_form_with_range + """ + Tests the creation of an order and the first request to see details, + including, the creation of the maker bond invoice. + """ + maker_form = self.maker_form_buy_with_range robot_index = 1 order_made_response = self.make_order(maker_form, robot_index) @@ -359,6 +388,9 @@ def test_get_order_created(self): self.assertFalse(data["escrow_locked"]) self.assertTrue(isinstance(data["bond_satoshis"], int)) + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"]) + def check_for_locked_bonds(self): # 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" @@ -385,7 +417,10 @@ def make_and_publish_order(self, maker_form, robot_index=1): return response def test_publish_order(self): - maker_form = self.maker_form_with_range + """ + Tests a trade from order creation to published (maker bond locked). + """ + maker_form = self.maker_form_buy_with_range # Get order response = self.make_and_publish_order(maker_form) data = json.loads(response.content.decode()) @@ -409,9 +444,9 @@ def test_publish_order(self): self.assertTrue(isinstance(public_data["price_now"], float)) self.assertTrue(isinstance(data["satoshis_now"], int)) - # @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) + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"]) + def take_order(self, order_id, amount, robot_index=2): path = reverse("order") params = f"?order_id={order_id}" @@ -430,9 +465,12 @@ def make_and_take_order( return response def test_make_and_take_order(self): + """ + Tests a trade from order creation to taken. + """ maker_index = 1 taker_index = 2 - maker_form = self.maker_form_with_range + maker_form = self.maker_form_buy_with_range response = self.make_and_take_order(maker_form, 80, maker_index, taker_index) data = json.loads(response.content.decode()) @@ -440,6 +478,7 @@ def test_make_and_take_order(self): self.assertEqual(response.status_code, 200) self.assertResponse(response) + self.assertEqual(data["status_message"], Order.Status(Order.Status.TAK).label) self.assertEqual( data["ur_nick"], read_file(f"tests/robots/{taker_index}/nickname") ) @@ -449,26 +488,112 @@ def test_make_and_take_order(self): self.assertEqual( data["maker_nick"], read_file(f"tests/robots/{maker_index}/nickname") ) + self.assertEqual(data["maker_status"], "Active") + self.assertEqual(data["taker_status"], "Active") self.assertFalse(data["is_maker"]) + self.assertFalse(data["is_buyer"]) + self.assertTrue(data["is_seller"]) self.assertTrue(data["is_taker"]) self.assertTrue(data["is_participant"]) + self.assertTrue(data["maker_locked"]) + self.assertFalse(data["taker_locked"]) + self.assertFalse(data["escrow_locked"]) + + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"]) + + def make_and_lock_contract( + self, maker_form, take_amount=80, maker_index=1, taker_index=2 + ): + # Make an order + order_taken_response = self.make_and_take_order( + maker_form, take_amount, maker_index, taker_index + ) + order_taken_data = json.loads(order_taken_response.content.decode()) + + # Maker's first order fetch. Should trigger maker bond hold invoice generation. + response = self.get_order(order_taken_data["id"], taker_index) + 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) + self.check_for_locked_bonds() + + # Get order + response = self.get_order(order_taken_data["id"], taker_index) + return response + + def test_make_and_lock_contract(self): + """ + Tests a trade from order creation to taker bond locked. + """ + maker_index = 1 + taker_index = 2 + maker_form = self.maker_form_buy_with_range + + response = self.make_and_lock_contract(maker_form, 80, maker_index, taker_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.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"]) + + def trade_to_locked_escrow( + self, maker_form, take_amount=80, maker_index=1, taker_index=2 + ): + # Make an order + locked_taker_response = self.make_and_lock_contract( + maker_form, take_amount, maker_index, taker_index + ) + locked_taker_response_data = json.loads(locked_taker_response.content.decode()) + + # 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 + pay_invoice("robot", invoice) + + # Check for invoice locked (the mocked LND will return ACCEPTED) + self.check_for_locked_bonds() + + # Get order + response = self.get_order(locked_taker_response_data["id"], taker_index) + return response + + def test_trade_to_locked_escrow(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_locked_escrow(maker_form, 80, maker_index, taker_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.WFI).label) + self.assertTrue(data["maker_locked"]) + self.assertTrue(data["taker_locked"]) + self.assertTrue(data["escrow_locked"]) - # a = { - # "maker_status": "Active", - # "taker_status": "Active", - # "price_now": 38205.0, - # "premium_now": 3.34, - # "satoshis_now": 266196, - # "is_buyer": False, - # "is_seller": True, - # "taker_nick": "EquivalentWool707", - # "status_message": "Waiting for taker bond", - # "is_fiat_sent": False, - # "is_disputed": False, - # "ur_nick": "EquivalentWool707", - # "maker_locked": True, - # "taker_locked": False, - # "escrow_locked": False, - # "bond_invoice": "lntb73280n1pj5uypwpp5vklcx3s3c66ltz5v7kglppke5n3u6sa6h8m6whe278lza7rwfc7qd2j2pshjmt9de6zqun9vejhyetwvdjn5gp3vgcxgvfkv43z6e3cvyez6dpkxejj6cnxvsmj6c3exsuxxden89skzv3j9cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz2sxqzfvsp5hkz0dnvja244hc8jwmpeveaxtjd4ddzuqlpqc5zxa6tckr8py50s9qyyssqdcl6w2rhma7k3v904q4tuz68z82d6x47dgflk6m8jdtgt9dg3n9304axv8qvd66dq39sx7yu20sv5pyguv9dnjw3385y8utadxxsqtsqpf7p3w", - # "bond_satoshis": 7328, - # } + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"], 2) From 397ea78ebe8103706bc3020d6f5345c34d6b0c34 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Wed, 15 Nov 2023 19:48:04 +0000 Subject: [PATCH 2/6] 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 588ff539b..e41352047 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 266e49b7f..6bb5930be 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 ef81f7d82..956b9818a 100755 --- a/api/lightning/cln.py +++ b/api/lightning/cln.py @@ -79,6 +79,14 @@ def get_info(cls): 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 bf831fa93..2d2438db3 100644 --- a/api/lightning/lnd.py +++ b/api/lightning/lnd.py @@ -105,10 +105,11 @@ def estimate_fee(cls, amount_sats, target_conf=2, min_confs=1): 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 e5c4e7ddc..0cf427ca2 100644 --- a/api/tests/test_utils.py +++ b/api/tests/test_utils.py @@ -168,8 +168,8 @@ def test_weighted_median(self): 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 b13aaee93..92992db83 100755 --- a/control/models.py +++ b/control/models.py @@ -126,6 +126,9 @@ def get_ln_remote_unsettled(): 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 0310c5c10..ae47c5fb2 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 3b4abd349..387a9133c 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 08a9f188d..b78690181 100644 --- a/tests/node_utils.py +++ b/tests/node_utils.py @@ -3,10 +3,12 @@ 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 000000000..a269ef7e6 --- /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 ce6e527bf..697a36795 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 1b1cc1e0b..3cacd2f74 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 2f448af9f..cd9ce49d2 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 63b27f17f..d2f7a14e4 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 401274977..6be10ef61 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 ab4a40855..b9bc8e051 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 ee0e0dc34..ef5c63543 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 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 @@ def setUpTestData(cls): # 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 @@ def test_cache_market(self): 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 @@ def test_get_order_created(self): 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 @@ def test_get_order_created(self): 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 @@ def test_publish_order(self): 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 @@ def test_make_and_lock_contract(self): 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 @@ def test_make_and_lock_contract(self): 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 @@ def trade_to_locked_escrow( # 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 @@ def test_trade_to_locked_escrow(self): # 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"]) From 1a6cf66421940900efa6eaaaae19a66ad55d126d Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 16 Nov 2023 13:28:53 +0000 Subject: [PATCH 3/6] Add fiat sent/received confirmation tests. Improve API documentation --- api/logics.py | 6 +- api/nick_generator/nick_generator.py | 2 - api/serializers.py | 39 +++++++-- api/views.py | 2 +- docs/assets/schemas/api-latest.yaml | 46 ++++++++-- tests/node_utils.py | 10 +-- tests/test_trade_pipeline.py | 121 ++++++++++++++++++++++++--- 7 files changed, 193 insertions(+), 33 deletions(-) diff --git a/api/logics.py b/api/logics.py index c37a7f996..70a8a266d 100644 --- a/api/logics.py +++ b/api/logics.py @@ -1915,7 +1915,9 @@ def summarize_trade(cls, order, user): else: summary["received_sats"] = order.payout.num_satoshis summary["payment_hash"] = order.payout.payment_hash - summary["preimage"] = order.payout.preimage + summary["preimage"] = ( + order.payout.preimage if order.payout.preimage else "processing" + ) summary["trade_fee_sats"] = round( order.last_satoshis - summary["received_sats"] @@ -1959,7 +1961,7 @@ def summarize_trade(cls, order, user): order.save(update_fields=["contract_finalization_time"]) platform_summary["contract_total_time"] = ( order.contract_finalization_time - order.last_satoshis_time - ) + ).total_seconds() if not order.is_swap: platform_summary["routing_budget_sats"] = order.payout.routing_budget_sats platform_summary["trade_revenue_sats"] = int( diff --git a/api/nick_generator/nick_generator.py b/api/nick_generator/nick_generator.py index dc9ea2cf0..9105d37a0 100755 --- a/api/nick_generator/nick_generator.py +++ b/api/nick_generator/nick_generator.py @@ -160,7 +160,6 @@ def compute_pool_size_loss(self, max_length=22, max_iter=1_000_000, num_runs=500 attempts = [] for i in range(num_runs): - string = str(random.uniform(0, 1_000_000)) hash = hashlib.sha256(str.encode(string)).hexdigest() @@ -179,7 +178,6 @@ def compute_pool_size_loss(self, max_length=22, max_iter=1_000_000, num_runs=500 if __name__ == "__main__": - # Just for code timming t0 = time.time() diff --git a/api/serializers.py b/api/serializers.py index 9e60dd1e8..7fcda669e 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -84,15 +84,24 @@ class Meta: # Only used in oas_schemas class SummarySerializer(serializers.Serializer): - sent_fiat = serializers.IntegerField( + sent_fiat = serializers.FloatField( required=False, help_text="same as `amount` (only for buyer)" ) + received_fiat = serializers.FloatField( + required=False, help_text="same as `amount` (only for seller)" + ) + sent_sats = serializers.IntegerField( + required=False, help_text="The total sats you sent (only for seller)" + ) received_sats = serializers.IntegerField( required=False, help_text="same as `trade_satoshis` (only for buyer)" ) is_swap = serializers.BooleanField( required=False, help_text="True if the payout was on-chain (only for buyer)" ) + is_buyer = serializers.BooleanField( + required=False, help_text="True if the robot is the order buyer" + ) received_onchain_sats = serializers.IntegerField( required=False, help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)", @@ -109,15 +118,26 @@ class SummarySerializer(serializers.Serializer): required=False, help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`", ) - sent_sats = serializers.IntegerField( - required=False, help_text="The total sats you sent (only for seller)" + bond_size_sats = serializers.IntegerField( + required=False, help_text="The amount of Satoshis at stake" ) - received_fiat = serializers.IntegerField( - required=False, help_text="same as `amount` (only for seller)" + bond_size_percent = serializers.FloatField( + required=False, help_text="The relative size of Satoshis at stake" ) trade_fee_sats = serializers.IntegerField( required=False, - help_text="Exchange fees in sats (Does not include swap fee and miner fee)", + help_text="Exchange fees in sats (does not include swap fee and miner fee)", + ) + trade_fee_percent = serializers.FloatField( + required=False, + help_text="Exchange fees in percent (does not include swap fee and miner fee)", + ) + payment_hash = serializers.CharField( + required=False, help_text="The payment_hash of the payout invoice" + ) + preimage = serializers.CharField( + required=False, + help_text="The preimage of the payout invoice (proof of payment)", ) @@ -138,6 +158,13 @@ class PlatformSummarySerializer(serializers.Serializer): trade_revenue_sats = serializers.IntegerField( required=False, help_text="The sats the exchange earned from the trade" ) + routing_budget_sats = serializers.FloatField( + required=False, help_text="The budget allocated for routing costs in Satoshis" + ) + contract_exchange_rate = serializers.FloatField( + required=False, + help_text="The exchange rate applied to this contract. Taken from externals APIs exactly when the taker bond was locked.", + ) # Only used in oas_schemas diff --git a/api/views.py b/api/views.py index ae4bf9ace..92f509454 100644 --- a/api/views.py +++ b/api/views.py @@ -449,7 +449,7 @@ def get(self, request, format=None): Order.Status.FAI, ]: data["public_duration"] = order.public_duration - data["bond_size"] = order.bond_size + data["bond_size"] = str(order.bond_size) # Adds trade summary if order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]: diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index 8229ea8dd..89e598525 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -1788,6 +1788,15 @@ components: trade_revenue_sats: type: integer description: The sats the exchange earned from the trade + routing_budget_sats: + type: number + format: double + description: The budget allocated for routing costs in Satoshis + contract_exchange_rate: + type: number + format: double + description: The exchange rate applied to this contract. Taken from externals + APIs exactly when the taker bond was locked. PostMessage: type: object properties: @@ -1873,14 +1882,25 @@ components: type: object properties: sent_fiat: - type: integer + type: number + format: double description: same as `amount` (only for buyer) + received_fiat: + type: number + format: double + description: same as `amount` (only for seller) + sent_sats: + type: integer + description: The total sats you sent (only for seller) received_sats: type: integer description: same as `trade_satoshis` (only for buyer) is_swap: type: boolean description: True if the payout was on-chain (only for buyer) + is_buyer: + type: boolean + description: True if the robot is the order buyer received_onchain_sats: type: integer description: The on-chain sats received (only for buyer and if `is_swap` @@ -1898,16 +1918,28 @@ components: format: double description: same as `swap_fee_rate` (only for buyer and if `is_swap` is `true` - sent_sats: - type: integer - description: The total sats you sent (only for seller) - received_fiat: + bond_size_sats: type: integer - description: same as `amount` (only for seller) + description: The amount of Satoshis at stake + bond_size_percent: + type: number + format: double + description: The relative size of Satoshis at stake trade_fee_sats: type: integer - description: Exchange fees in sats (Does not include swap fee and miner + description: Exchange fees in sats (does not include swap fee and miner fee) + trade_fee_percent: + type: number + format: double + description: Exchange fees in percent (does not include swap fee and miner + fee) + payment_hash: + type: string + description: The payment_hash of the payout invoice + preimage: + type: string + description: The preimage of the payout invoice (proof of payment) Tick: type: object properties: diff --git a/tests/node_utils.py b/tests/node_utils.py index b78690181..ae69698e3 100644 --- a/tests/node_utils.py +++ b/tests/node_utils.py @@ -58,7 +58,7 @@ def wait_for_lnd_node_sync(node_name): return else: sys.stdout.write( - f"\rWaiting for {node_name} node chain sync {round(waited,1)}s" + f"\rWaiting for {node_name} node chain sync {round(waited, 1)}s" ) sys.stdout.flush() waited += WAIT_STEP @@ -88,14 +88,14 @@ def wait_for_active_channels(lnvendor, node_name="coordinator"): return else: sys.stdout.write( - f"\rWaiting for {node_name} LND node channel to be active {round(waited,1)}s" + f"\rWaiting for {node_name} LND node channel to be active {round(waited, 1)}s" ) elif lnvendor == "CLN": if CLN_has_active_channels(): return else: sys.stdout.write( - f"\rWaiting for {node_name} CLN node channel to be active {round(waited,1)}s" + f"\rWaiting for {node_name} CLN node channel to be active {round(waited, 1)}s" ) sys.stdout.flush() @@ -111,7 +111,7 @@ def wait_for_cln_node_sync(): response = CLNNode.get_info() if response.warning_bitcoind_sync or response.warning_lightningd_sync: sys.stdout.write( - f"\rWaiting for coordinator CLN node sync {round(waited,1)}s" + f"\rWaiting for coordinator CLN node sync {round(waited, 1)}s" ) sys.stdout.flush() waited += WAIT_STEP @@ -130,7 +130,7 @@ def wait_for_cln_active_channels(): return else: sys.stdout.write( - f"\rWaiting for coordinator CLN node channels to be active {round(waited,1)}s" + f"\rWaiting for coordinator CLN node channels to be active {round(waited, 1)}s" ) sys.stdout.flush() waited += WAIT_STEP diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index ef5c63543..e9439f1aa 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -1,4 +1,5 @@ import json +import random from datetime import datetime from decimal import Decimal @@ -583,8 +584,11 @@ def submit_payout_address(self, order_id, robot_index=1): 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} - + body = { + "action": "update_address", + "address": signed_payout_address, + "mining_fee_rate": 50, + } response = self.client.post(path + params, body, **headers) return response @@ -602,15 +606,18 @@ def trade_to_submitted_address( def test_trade_to_submitted_address(self): """ - Tests a trade from order creation until escrow locked, before - invoice/address is submitted by buyer. + Tests a trade from order creation until escrow locked and + address is submitted by buyer. """ maker_index = 1 taker_index = 2 maker_form = self.maker_form_buy_with_range + take_amount = round( + random.uniform(maker_form["min_amount"], maker_form["max_amount"]), 2 + ) response = self.trade_to_submitted_address( - maker_form, 80, maker_index, taker_index + maker_form, take_amount, maker_index, taker_index ) data = response.json() @@ -624,7 +631,9 @@ def test_trade_to_submitted_address(self): # 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): + def submit_payout_invoice( + self, order_id, num_satoshis, routing_budget, robot_index=1 + ): path = reverse("order") params = f"?order_id={order_id}" headers = self.get_robot_auth(robot_index) @@ -635,7 +644,11 @@ def submit_payout_invoice(self, order_id, num_satoshis, robot_index=1): 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} + body = { + "action": "update_invoice", + "invoice": signed_payout_invoice, + "routing_budget_ppm": routing_budget, + } response = self.client.post(path + params, body, **headers) @@ -653,21 +666,25 @@ def trade_to_submitted_invoice( response = self.submit_payout_invoice( response_escrow_locked.json()["id"], response_get.json()["trade_satoshis"], + 0, 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. + Tests a trade from order creation until escrow locked and + invoice is submitted by buyer. """ maker_index = 1 taker_index = 2 maker_form = self.maker_form_buy_with_range + take_amount = round( + random.uniform(maker_form["min_amount"], maker_form["max_amount"]), 2 + ) response = self.trade_to_submitted_invoice( - maker_form, 80, maker_index, taker_index + maker_form, take_amount, maker_index, taker_index ) data = response.json() @@ -679,3 +696,87 @@ def test_trade_to_submitted_invoice(self): # Cancel order to avoid leaving pending HTLCs after a successful test self.cancel_order(data["id"]) + + def confirm_fiat(self, order_id, robot_index=1): + path = reverse("order") + params = f"?order_id={order_id}" + headers = self.get_robot_auth(robot_index) + + body = {"action": "confirm"} + + response = self.client.post(path + params, body, **headers) + return response + + def trade_to_confirm_fiat_sent_LN( + self, maker_form, take_amount=80, maker_index=1, taker_index=2 + ): + response_submitted_invoice = self.trade_to_submitted_invoice( + maker_form, take_amount, maker_index, taker_index + ) + response = self.confirm_fiat( + response_submitted_invoice.json()["id"], maker_index + ) + return response + + def test_trade_to_confirm_fiat_sent_LN(self): + """ + Tests a trade from order creation until fiat sent confirmed + """ + maker_index = 1 + taker_index = 2 + maker_form = self.maker_form_buy_with_range + take_amount = round( + random.uniform(maker_form["min_amount"], maker_form["max_amount"]), 2 + ) + + response = self.trade_to_confirm_fiat_sent_LN( + maker_form, take_amount, 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.FSE).label) + self.assertTrue(data["is_fiat_sent"]) + + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"], maker_index) + self.cancel_order(data["id"], taker_index) + + def trade_to_confirm_fiat_received_LN( + self, maker_form, take_amount=80, maker_index=1, taker_index=2 + ): + response_submitted_invoice = self.trade_to_confirm_fiat_sent_LN( + maker_form, take_amount, maker_index, taker_index + ) + response = self.confirm_fiat( + response_submitted_invoice.json()["id"], taker_index + ) + return response + + def test_trade_to_confirm_fiat_received_LN(self): + """ + Tests a trade from order creation until fiat received is confirmed by seller/taker + """ + maker_index = 1 + taker_index = 2 + maker_form = self.maker_form_buy_with_range + take_amount = round( + random.uniform(maker_form["min_amount"], maker_form["max_amount"]), 2 + ) + + response = self.trade_to_confirm_fiat_received_LN( + maker_form, take_amount, 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.PAY).label) + self.assertTrue(data["is_fiat_sent"]) + self.assertFalse(data["is_disputed"]) + self.assertFalse(data["maker_locked"]) + self.assertFalse(data["taker_locked"]) + self.assertFalse(data["escrow_locked"]) From 9df1e00cba0d1e1df1e1e6168adf9552f95fb338 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 16 Nov 2023 13:37:09 +0000 Subject: [PATCH 4/6] Delete codeql workflows --- .github/workflows/codeql-client.yml | 75 ------------------------ .github/workflows/codeql-coordinator.yml | 74 ----------------------- 2 files changed, 149 deletions(-) delete mode 100644 .github/workflows/codeql-client.yml delete mode 100644 .github/workflows/codeql-coordinator.yml diff --git a/.github/workflows/codeql-client.yml b/.github/workflows/codeql-client.yml deleted file mode 100644 index 5c122cd50..000000000 --- a/.github/workflows/codeql-client.yml +++ /dev/null @@ -1,75 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL: Client" - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - paths: - - 'frontend' - - 'mobile' - schedule: - - cron: '39 10 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/codeql-coordinator.yml b/.github/workflows/codeql-coordinator.yml deleted file mode 100644 index 2090faba8..000000000 --- a/.github/workflows/codeql-coordinator.yml +++ /dev/null @@ -1,74 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL: Coordinator" - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - paths: - - 'api' - schedule: - - cron: '39 10 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 From 97cd99146e12d07c7883c43e25cc1f84125610e2 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 16 Nov 2023 13:41:20 +0000 Subject: [PATCH 5/6] Fix weird flake8 mismatch of expectations --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 61abd3225..c692e6c77 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] max-line-length = 88 extend-ignore = E501 -exclude = ./*migrations*.py \ No newline at end of file +exclude = ./*migrations*.py,./api/nick_generator/nick_generator.py \ No newline at end of file From 43c324f21a6eceef8f59972b046d873fd7230424 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 17 Nov 2023 12:57:37 +0000 Subject: [PATCH 6/6] Add pause/unpause tests --- api/logics.py | 4 ++- api/serializers.py | 2 +- docs/assets/schemas/api-latest.yaml | 3 ++- tests/node_utils.py | 4 ++- tests/test_trade_pipeline.py | 38 +++++++++++++++++++++++++++-- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/api/logics.py b/api/logics.py index 70a8a266d..306293477 100644 --- a/api/logics.py +++ b/api/logics.py @@ -739,7 +739,9 @@ def payout_amount(cls, order, user): return True, context context["swap_allowed"] = True - context["suggested_mining_fee_rate"] = order.payout_tx.suggested_mining_fee_rate + context["suggested_mining_fee_rate"] = float( + order.payout_tx.suggested_mining_fee_rate + ) context["swap_fee_rate"] = order.payout_tx.swap_fee_rate return True, context diff --git a/api/serializers.py b/api/serializers.py index 7fcda669e..f08ceb9f8 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -298,7 +298,7 @@ class OrderDetailSerializer(serializers.ModelSerializer): swap_failure_reason = serializers.CharField( required=False, help_text="Reason for why on-chain swap is not available" ) - suggested_mining_fee_rate = serializers.IntegerField( + suggested_mining_fee_rate = serializers.FloatField( required=False, help_text="fee in sats/vbyte for the on-chain swap" ) swap_fee_rate = serializers.FloatField( diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index 89e598525..ac6b680c6 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -1581,7 +1581,8 @@ components: type: string description: Reason for why on-chain swap is not available suggested_mining_fee_rate: - type: integer + type: number + format: double description: fee in sats/vbyte for the on-chain swap swap_fee_rate: type: number diff --git a/tests/node_utils.py b/tests/node_utils.py index ae69698e3..9e2238201 100644 --- a/tests/node_utils.py +++ b/tests/node_utils.py @@ -272,7 +272,9 @@ def pay_invoice(node_name, invoice): f'http://localhost:{node["port"]}/v1/channels/transactions', json=data, headers=node["headers"], - timeout=0.3, # 0.15s is enough for LND to LND hodl ACCEPT. + # 0.15s is enough for LND to LND hodl ACCEPT + # 0.4s is enough for LND to CLN hodl ACCEPT + timeout=0.2 if LNVENDOR == "LND" else 0.8, ) except ReadTimeout: # Request to pay hodl invoice has timed out: that's good! diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index e9439f1aa..bc3e8f851 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -107,7 +107,7 @@ def test_initial_balance_log(self): 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_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) @@ -306,6 +306,15 @@ def cancel_order(self, order_id, robot_index=1): return response + def pause_order(self, order_id, robot_index=1): + path = reverse("order") + params = f"?order_id={order_id}" + headers = self.get_robot_auth(robot_index) + body = {"action": "pause"} + response = self.client.post(path + params, body, **headers) + + return response + def test_get_order_created(self): """ Tests the creation of an order and the first request to see details, @@ -403,6 +412,31 @@ def test_publish_order(self): # Cancel order to avoid leaving pending HTLCs after a successful test self.cancel_order(data["id"]) + def test_pause_unpause_order(self): + """ + Tests pausing and unpausing a public order + """ + maker_form = self.maker_form_buy_with_range + # Get order + response = self.make_and_publish_order(maker_form) + + # PAUSE + response = self.pause_order(response.json()["id"]) + data = response.json() + + self.assertResponse(response) + self.assertEqual(data["status_message"], Order.Status(Order.Status.PAU).label) + + # UNPAUSE + response = self.pause_order(response.json()["id"]) + data = response.json() + + self.assertResponse(response) + self.assertEqual(data["status_message"], Order.Status(Order.Status.PUB).label) + + # Cancel order to avoid leaving pending HTLCs after a successful test + self.cancel_order(data["id"]) + def take_order(self, order_id, amount, robot_index=2): path = reverse("order") params = f"?order_id={order_id}" @@ -513,7 +547,7 @@ def test_make_and_lock_contract(self): 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["suggested_mining_fee_rate"], float) self.assertIsInstance(data["swap_fee_rate"], float) self.assertTrue(data["suggested_mining_fee_rate"] > 0) self.assertTrue(data["swap_fee_rate"] > 0)