From 0175e73ca186ff9aea5ed1051e0fc194a0ee41b1 Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Fri, 28 Jun 2024 15:56:58 +0200 Subject: [PATCH 1/7] New notifications --- api/admin.py | 2 ++ api/models/order.py | 8 +++--- api/notifications.py | 44 +++++++++++++++++++++++++++++ api/serializers.py | 2 +- api/tasks.py | 6 ++++ api/views.py | 1 + tests/test_trade_pipeline.py | 54 ++++++++++++++++++++++++++++++++++++ tests/utils/trade.py | 7 +++++ 8 files changed, 119 insertions(+), 5 deletions(-) diff --git a/api/admin.py b/api/admin.py index 01d4f2d5c..0ca8e536f 100644 --- a/api/admin.py +++ b/api/admin.py @@ -210,6 +210,7 @@ def maker_wins(self, request, queryset): f"Dispute of order {order.id} solved successfully on favor of the maker", messages.SUCCESS, ) + send_notification.delay(order_id=order.id, message="dispute_closed") else: self.message_user( @@ -248,6 +249,7 @@ def taker_wins(self, request, queryset): f"Dispute of order {order.id} solved successfully on favor of the taker", messages.SUCCESS, ) + send_notification.delay(order_id=order.id, message="dispute_closed") else: self.message_user( diff --git a/api/models/order.py b/api/models/order.py index 161f6d3e5..f9a72ecf6 100644 --- a/api/models/order.py +++ b/api/models/order.py @@ -10,6 +10,7 @@ from django.db.models.signals import pre_delete from django.dispatch import receiver from django.utils import timezone +from api.tasks import send_notification if config("TESTING", cast=bool, default=False): import random @@ -91,10 +92,7 @@ class ExpiryReasons(models.IntegerChoices): decimal_places=2, default=0, null=True, - validators=[ - MinValueValidator(Decimal(-100)), - MaxValueValidator(Decimal(999)) - ], + validators=[MinValueValidator(Decimal(-100)), MaxValueValidator(Decimal(999))], blank=True, ) # explicit @@ -352,6 +350,8 @@ def update_status(self, new_status): self.log( f"Order state went from {old_status}: {Order.Status(old_status).label} to {new_status}: {Order.Status(new_status).label}" ) + if new_status == Order.Status.FAI: + send_notification.delay(order_id=self.id, message="lightning_failed") @receiver(pre_delete, sender=Order) diff --git a/api/notifications.py b/api/notifications.py index 480c6d477..7adc9f609 100644 --- a/api/notifications.py +++ b/api/notifications.py @@ -219,3 +219,47 @@ def coordinator_cancelled(self, order): title = f"๐Ÿ› ๏ธ Your order with ID {order.id} has been cancelled by the coordinator {config('COORDINATOR_ALIAS', cast=str, default='NoAlias')} for the upcoming maintenance stop." self.send_message(order, order.maker.robot, title) return + + def dispute_closed(self, order): + lang = order.maker.robot.telegram_lang_code + if order.status == Order.Status.MLD: + # Maker lost dispute + looser = order.maker + winner = order.taker + elif order.status == Order.Status.TLD: + # Taker lost dispute + looser = order.taker + winner = order.maker + + lang = looser.robot.telegram_lang_code + if lang == "es": + title = f"โš–๏ธ Hey {looser.username}, has perdido la disputa en la orden con ID {str(order.id)}." + else: + title = f"โš–๏ธ Hey {looser.username}, you lost the dispute on your order with ID {str(order.id)}." + self.send_message(order, looser.robot, title) + + lang = winner.robot.telegram_lang_code + if lang == "es": + title = f"โš–๏ธ Hey {winner.username}, has ganado la disputa en la orden con ID {str(order.id)}." + else: + title = f"โš–๏ธ Hey {winner.username}, you won the dispute on your order with ID {str(order.id)}." + self.send_message(order, winner.robot, title) + + return + + def lightning_failed(self, order): + lang = order.maker.robot.telegram_lang_code + if order.type == Order.Types.BUY: + buyer = order.maker + else: + buyer = order.taker + + if lang == "es": + title = f"โšกโŒ Hey {buyer.username}, el pago lightning en la order con ID {str(order.id)} ha fallado." + description = "Intentalo de nuevo con una nueva factura o con otra wallet." + else: + title = f"โšกโŒ Hey {buyer.username}, the lightning payment on your order with ID {str(order.id)} failed." + description = "Try again with a new invoice or from another wallet." + + self.send_message(order, buyer.robot, title, description) + return diff --git a/api/serializers.py b/api/serializers.py index 2f7b2310f..fb8a0cdd0 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -493,7 +493,7 @@ class Meta: class ListNotificationSerializer(serializers.ModelSerializer): class Meta: model = Notification - fields = ("title", "description", "order_id") + fields = ("title", "description", "order_id", "order_status") class OrderPublicSerializer(serializers.ModelSerializer): diff --git a/api/tasks.py b/api/tasks.py index 88e924a86..656feb894 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -303,4 +303,10 @@ def send_notification(order_id=None, chat_message_id=None, message=None): elif message == "coordinator_cancelled": notifications.coordinator_cancelled(order) + elif message == "dispute_closed": + notifications.dispute_closed(order) + + elif message == "lightning_failed": + notifications.lightning_failed(order) + return diff --git a/api/views.py b/api/views.py index 0caa7407a..171dfe24e 100644 --- a/api/views.py +++ b/api/views.py @@ -764,6 +764,7 @@ def get(self, request, format=None): data["title"] = str(notification.title) data["description"] = str(notification.description) data["order_id"] = notification.order.id + data["order_status"] = notification.order.status notification_data.append(data) diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index bf438a482..91e6d75a4 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -1163,6 +1163,60 @@ def test_order_expires_after_only_maker_messaged(self): f"โš–๏ธ Hey {data['taker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.", ) + # def test_dispute_closed_maker_wins(self): + # trade = Trade(self.client) + # trade.publish_order() + # trade.take_order() + # trade.lock_taker_bond() + # trade.lock_escrow(trade.taker_index) + # trade.submit_payout_invoice(trade.maker_index) + + # trade.change_order_status(Order.Status.TLD) + + # trade.clean_orders() + + # maker_headers = trade.get_robot_auth(trade.maker_index) + # response = self.client.get(reverse("notifications"), **maker_headers) + # self.assertResponse(response) + # notifications_data = list(response.json()) + # self.assertEqual(notifications_data[0]["order_id"], trade.order_id) + # self.assertEqual( + # notifications_data[0]["title"], + # f"โš–๏ธ Hey {data['maker_nick']}, you won the dispute on your order with ID {str(trade.order_id)}." + # ) + # taker_headers = trade.get_robot_auth(trade.taker_index) + # response = self.client.get(reverse("notifications"), **taker_headers) + # self.assertResponse(response) + # notifications_data = list(response.json()) + # self.assertEqual(notifications_data[0]["order_id"], trade.order_id) + # self.assertEqual( + # notifications_data[0]["title"], + # f"โš–๏ธ Hey {data['taker_nick']}, you lost the dispute on your order with ID {str(trade.order_id)}." + # ) + + def test_lightning_payment_failed(self): + trade = Trade(self.client) + trade.publish_order() + trade.take_order() + trade.lock_taker_bond() + trade.lock_escrow(trade.taker_index) + trade.submit_payout_invoice(trade.maker_index) + + trade.change_order_status(Order.Status.FAI) + + trade.clean_orders() + + maker_headers = trade.get_robot_auth(trade.maker_index) + maker_nick = read_file(f"tests/robots/{trade.maker_index}/nickname") + response = self.client.get(reverse("notifications"), **maker_headers) + self.assertResponse(response) + notifications_data = list(response.json()) + self.assertEqual(notifications_data[0]["order_id"], trade.order_id) + self.assertEqual( + notifications_data[0]["title"], + f"โšกโŒ Hey {maker_nick}, the lightning payment on your order with ID {str(trade.order_id)} failed.", + ) + def test_withdraw_reward_after_unilateral_cancel(self): """ Tests withdraw rewards as taker after maker cancels order unilaterally diff --git a/tests/utils/trade.py b/tests/utils/trade.py index c00b87576..eaf5f0236 100644 --- a/tests/utils/trade.py +++ b/tests/utils/trade.py @@ -271,3 +271,10 @@ def expire_order(self): order = Order.objects.get(id=self.order_id) order.expires_at = datetime.now() order.save() + + @patch("api.tasks.send_notification.delay", send_notification) + def change_order_status(self, status): + # Change order expiry to now + order = Order.objects.get(id=self.order_id) + order.status = status + order.save() From 3de8b7e3cced2dde48bf0dffa9cc03b00d7fd58c Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Fri, 28 Jun 2024 16:02:15 +0200 Subject: [PATCH 2/7] Test trigger --- tests/utils/trade.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/utils/trade.py b/tests/utils/trade.py index eaf5f0236..39bcda0a1 100644 --- a/tests/utils/trade.py +++ b/tests/utils/trade.py @@ -276,5 +276,4 @@ def expire_order(self): def change_order_status(self, status): # Change order expiry to now order = Order.objects.get(id=self.order_id) - order.status = status - order.save() + order.update_status(status) From 9855e5d1f21ade8978900671f27b0530f6904943 Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Fri, 28 Jun 2024 17:16:31 +0200 Subject: [PATCH 3/7] Test fix --- api/serializers.py | 2 +- api/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index fb8a0cdd0..e5d777546 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -493,7 +493,7 @@ class Meta: class ListNotificationSerializer(serializers.ModelSerializer): class Meta: model = Notification - fields = ("title", "description", "order_id", "order_status") + fields = ("title", "description", "order_id", "status") class OrderPublicSerializer(serializers.ModelSerializer): diff --git a/api/views.py b/api/views.py index 171dfe24e..bd7ddd89d 100644 --- a/api/views.py +++ b/api/views.py @@ -764,7 +764,7 @@ def get(self, request, format=None): data["title"] = str(notification.title) data["description"] = str(notification.description) data["order_id"] = notification.order.id - data["order_status"] = notification.order.status + data["status"] = notification.order.status notification_data.append(data) From 53bdb301664ac56e266f735809998cd618b380d2 Mon Sep 17 00:00:00 2001 From: koalasat Date: Sat, 29 Jun 2024 17:09:04 +0200 Subject: [PATCH 4/7] Fix endpoint fields --- api/migrations/0048_alter_order_reference.py | 19 +++++++++++++++++++ api/serializers.py | 7 +++++++ api/views.py | 4 ---- docker-tests.yml | 14 +++++++------- docs/assets/schemas/api-latest.yaml | 5 +++++ 5 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 api/migrations/0048_alter_order_reference.py diff --git a/api/migrations/0048_alter_order_reference.py b/api/migrations/0048_alter_order_reference.py new file mode 100644 index 000000000..cf8fa0769 --- /dev/null +++ b/api/migrations/0048_alter_order_reference.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.6 on 2024-06-29 14:07 + +import api.models.order +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0047_notification'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='reference', + field=models.UUIDField(default=api.models.order.custom_uuid, editable=False), + ), + ] diff --git a/api/serializers.py b/api/serializers.py index e5d777546..74cf125f7 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -491,10 +491,17 @@ class Meta: class ListNotificationSerializer(serializers.ModelSerializer): + status = serializers.SerializerMethodField( + help_text="The `status` of the order when the notification was trigered", + ) class Meta: model = Notification fields = ("title", "description", "order_id", "status") + def get_status(self, notification) -> int: + return notification.order.status + + class OrderPublicSerializer(serializers.ModelSerializer): maker_nick = serializers.CharField(required=False) diff --git a/api/views.py b/api/views.py index bd7ddd89d..a03dbd33d 100644 --- a/api/views.py +++ b/api/views.py @@ -761,11 +761,7 @@ def get(self, request, format=None): notification_data = [] for notification in queryset: data = self.serializer_class(notification).data - data["title"] = str(notification.title) - data["description"] = str(notification.description) data["order_id"] = notification.order.id - data["status"] = notification.order.status - notification_data.append(data) return Response(notification_data, status=status.HTTP_200_OK) diff --git a/docker-tests.yml b/docker-tests.yml index 0a4af45e2..975747e88 100644 --- a/docker-tests.yml +++ b/docker-tests.yml @@ -15,7 +15,7 @@ version: '3.9' services: bitcoind: image: ruimarinho/bitcoin-core:${BITCOIND_VERSION:-24.0.1}-alpine - container_name: btc + container_name: test-btc restart: always ports: - "8000:8000" @@ -50,7 +50,7 @@ services: coordinator-LND: image: lightninglabs/lnd:${LND_VERSION:-v0.17.0-beta} - container_name: coordinator-LND + container_name: test-coordinator-LND restart: always volumes: - bitcoin:/root/.bitcoin/ @@ -83,7 +83,7 @@ services: coordinator-CLN: image: elementsproject/lightningd:${CLN_VERSION:-v24.05} restart: always - container_name: coordinator-CLN + container_name: test-coordinator-CLN environment: LIGHTNINGD_NETWORK: 'regtest' volumes: @@ -97,7 +97,7 @@ services: robot-LND: image: lightninglabs/lnd:${LND_VERSION:-v0.17.0-beta} - container_name: robot-LND + container_name: test-robot-LND restart: always volumes: - bitcoin:/root/.bitcoin/ @@ -129,7 +129,7 @@ services: redis: image: redis:${REDIS_VERSION:-7.2.1}-alpine - container_name: redis + container_name: test-redis restart: always volumes: - redisdata:/data @@ -141,7 +141,7 @@ services: args: DEVELOPMENT: True image: backend-image - container_name: coordinator + container_name: test-coordinator restart: always environment: DEVELOPMENT: True @@ -171,7 +171,7 @@ services: postgres: image: postgres:${POSTGRES_VERSION:-14.2}-alpine - container_name: sql + container_name: test-sql restart: always environment: POSTGRES_PASSWORD: 'example' diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index 60c5c933e..8aa93469b 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -1106,8 +1106,13 @@ components: order_id: type: integer readOnly: true + status: + type: integer + readOnly: true + description: The `status` of the order when the notification was trigered required: - order_id + - status ListOrder: type: object properties: From adbaa60da2815f449ac04f805454e47c96cf8f5b Mon Sep 17 00:00:00 2001 From: koalasat Date: Sat, 29 Jun 2024 22:50:20 +0200 Subject: [PATCH 5/7] Update test comment --- tests/test_trade_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index 91e6d75a4..c226b5c01 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -1171,7 +1171,7 @@ def test_order_expires_after_only_maker_messaged(self): # trade.lock_escrow(trade.taker_index) # trade.submit_payout_invoice(trade.maker_index) - # trade.change_order_status(Order.Status.TLD) + # # Admin resolves dispute # trade.clean_orders() From 6885a24f59a072a5a272fb22ef23ec21f1b75d38 Mon Sep 17 00:00:00 2001 From: koalasat Date: Tue, 9 Jul 2024 21:50:48 +0200 Subject: [PATCH 6/7] Include created_at --- api/serializers.py | 4 ++-- docs/assets/schemas/api-latest.yaml | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 74cf125f7..46fb1c582 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -494,15 +494,15 @@ class ListNotificationSerializer(serializers.ModelSerializer): status = serializers.SerializerMethodField( help_text="The `status` of the order when the notification was trigered", ) + class Meta: model = Notification - fields = ("title", "description", "order_id", "status") + fields = ("title", "description", "order_id", "status", "created_at") def get_status(self, notification) -> int: return notification.order.status - class OrderPublicSerializer(serializers.ModelSerializer): maker_nick = serializers.CharField(required=False) maker_hash_id = serializers.CharField(required=False) diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index 8aa93469b..490c35ae4 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -1110,6 +1110,9 @@ components: type: integer readOnly: true description: The `status` of the order when the notification was trigered + created_at: + type: string + format: date-time required: - order_id - status From c960d196374fb4fd8d5f5a0e5ff6b7da886e16bd Mon Sep 17 00:00:00 2001 From: koalasat Date: Tue, 9 Jul 2024 21:55:45 +0200 Subject: [PATCH 7/7] fix ruff --- api/models/robot.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/api/models/robot.py b/api/models/robot.py index 9efed1c15..c838d3547 100644 --- a/api/models/robot.py +++ b/api/models/robot.py @@ -1,12 +1,8 @@ -from pathlib import Path - -from django.conf import settings from django.contrib.auth.models import User from django.core.validators import validate_comma_separated_integer_list from django.db import models -from django.db.models.signals import post_save, pre_delete +from django.db.models.signals import post_save from django.dispatch import receiver -from django.utils.html import mark_safe class Robot(models.Model):