diff --git a/api/logics.py b/api/logics.py index c37a7f99..70a8a266 100644 --- a/api/logics.py +++ b/api/logics.py @@ -1915,7 +1915,9 @@ class Logics: 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 @@ class Logics: 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 dc9ea2cf..9105d37a 100755 --- a/api/nick_generator/nick_generator.py +++ b/api/nick_generator/nick_generator.py @@ -160,7 +160,6 @@ class NickGenerator: 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 @@ class NickGenerator: if __name__ == "__main__": - # Just for code timming t0 = time.time() diff --git a/api/serializers.py b/api/serializers.py index 9e60dd1e..7fcda669 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -84,15 +84,24 @@ class ListOrderSerializer(serializers.ModelSerializer): # 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 ae4bf9ac..92f50945 100644 --- a/api/views.py +++ b/api/views.py @@ -449,7 +449,7 @@ class OrderView(viewsets.ViewSet): 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 8229ea8d..89e59852 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: + bond_size_sats: type: integer - description: The total sats you sent (only for seller) - received_fiat: - 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 b7869018..ae69698e 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 ef5c6354..e9439f1a 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 @@ class TradeTest(BaseAPITestCase): 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 @@ class TradeTest(BaseAPITestCase): 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 @@ class TradeTest(BaseAPITestCase): # 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 @@ class TradeTest(BaseAPITestCase): 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 @@ class TradeTest(BaseAPITestCase): 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 @@ class TradeTest(BaseAPITestCase): # 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"])