diff --git a/.github/workflows/coordinator-image.yml b/.github/workflows/coordinator-image.yml
index ccbeeef7..52b46f89 100644
--- a/.github/workflows/coordinator-image.yml
+++ b/.github/workflows/coordinator-image.yml
@@ -75,7 +75,7 @@ jobs:
echo ${{ steps.commit.outputs.long }}>"commit_sha"
- name: 'Build and push Docker image'
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: .
push: true
diff --git a/.github/workflows/selfhosted-client-image.yml b/.github/workflows/selfhosted-client-image.yml
index aaf053e5..2640903a 100644
--- a/.github/workflows/selfhosted-client-image.yml
+++ b/.github/workflows/selfhosted-client-image.yml
@@ -85,7 +85,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: 'Build and push Docker image'
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: ./nodeapp
platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/web-client-image.yml b/.github/workflows/web-client-image.yml
index d522077a..473eb44d 100644
--- a/.github/workflows/web-client-image.yml
+++ b/.github/workflows/web-client-image.yml
@@ -85,7 +85,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: 'Build and push Docker image'
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: ./web
platforms: linux/amd64,linux/arm64
diff --git a/.gitignore b/.gitignore
index f5d79e09..40a4ba98 100755
--- a/.gitignore
+++ b/.gitignore
@@ -650,3 +650,6 @@ node
mobile/html/Web.bundle/js*
mobile/html/Web.bundle/css*
mobile/html/Web.bundle/assets*
+
+# Protocol Buffers
+api/lightning/*.proto
diff --git a/api/admin.py b/api/admin.py
index 01d4f2d5..0ca8e536 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -210,6 +210,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
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 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
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/management/commands/telegram_watcher.py b/api/management/commands/telegram_watcher.py
index 40e3cb4c..d626b756 100644
--- a/api/management/commands/telegram_watcher.py
+++ b/api/management/commands/telegram_watcher.py
@@ -6,7 +6,7 @@ from django.core.management.base import BaseCommand
from django.db import transaction
from api.models import Robot
-from api.notifications import Telegram
+from api.notifications import Notifications
from api.utils import get_session
@@ -17,7 +17,7 @@ class Command(BaseCommand):
bot_token = config("TELEGRAM_TOKEN")
updates_url = f"https://api.telegram.org/bot{bot_token}/getUpdates"
session = get_session()
- telegram = Telegram()
+ notifications = Notifications()
def handle(self, *args, **options):
offset = 0
@@ -49,17 +49,17 @@ class Command(BaseCommand):
continue
parts = message.split(" ")
if len(parts) < 2:
- self.telegram.send_message(
- chat_id=result["message"]["from"]["id"],
- text='You must enable the notifications bot using the RoboSats client. Click on your "Robot robot" -> "Enable Telegram" and follow the link or scan the QR code.',
+ self.notifications.send_telegram_message(
+ result["message"]["from"]["id"],
+ 'You must enable the notifications bot using the RoboSats client. Click on your "Robot robot" -> "Enable Telegram" and follow the link or scan the QR code.',
)
continue
token = parts[-1]
robot = Robot.objects.filter(telegram_token=token).first()
if not robot:
- self.telegram.send_message(
- chat_id=result["message"]["from"]["id"],
- text=f'Wops, invalid token! There is no Robot with telegram chat token "{token}"',
+ self.notifications.send_telegram_message(
+ result["message"]["from"]["id"],
+ f'Wops, invalid token! There is no Robot with telegram chat token "{token}"',
)
continue
@@ -71,7 +71,7 @@ class Command(BaseCommand):
robot.telegram_lang_code = result["message"]["from"][
"language_code"
]
- self.telegram.welcome(robot.user)
+ self.notifications.welcome(robot.user)
robot.telegram_enabled = True
robot.save(
update_fields=[
diff --git a/api/migrations/0047_notification.py b/api/migrations/0047_notification.py
new file mode 100644
index 00000000..24ce5c92
--- /dev/null
+++ b/api/migrations/0047_notification.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.0.6 on 2024-06-14 18:31
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0046_alter_currency_currency'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
+ ('title', models.CharField(default=None, max_length=240)),
+ ('description', models.CharField(blank=True, default=None, max_length=240)),
+ ('order', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='api.order')),
+ ('robot', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='api.robot')),
+ ],
+ ),
+ ]
diff --git a/api/migrations/0048_alter_order_reference.py b/api/migrations/0048_alter_order_reference.py
new file mode 100644
index 00000000..cf8fa076
--- /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/migrations/0049_alter_currency_currency.py b/api/migrations/0049_alter_currency_currency.py
new file mode 100644
index 00000000..b4a7d5e1
--- /dev/null
+++ b/api/migrations/0049_alter_currency_currency.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.8 on 2024-08-15 18:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0048_alter_order_reference'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='currency',
+ name='currency',
+ field=models.PositiveSmallIntegerField(choices=[(1, 'USD'), (2, 'EUR'), (3, 'JPY'), (4, 'GBP'), (5, 'AUD'), (6, 'CAD'), (7, 'CHF'), (8, 'CNY'), (9, 'HKD'), (10, 'NZD'), (11, 'SEK'), (12, 'KRW'), (13, 'SGD'), (14, 'NOK'), (15, 'MXN'), (16, 'BYN'), (17, 'RUB'), (18, 'ZAR'), (19, 'TRY'), (20, 'BRL'), (21, 'CLP'), (22, 'CZK'), (23, 'DKK'), (24, 'HRK'), (25, 'HUF'), (26, 'INR'), (27, 'ISK'), (28, 'PLN'), (29, 'RON'), (30, 'ARS'), (31, 'VES'), (32, 'COP'), (33, 'PEN'), (34, 'UYU'), (35, 'PYG'), (36, 'BOB'), (37, 'IDR'), (38, 'ANG'), (39, 'CRC'), (40, 'CUP'), (41, 'DOP'), (42, 'GHS'), (43, 'GTQ'), (44, 'ILS'), (45, 'JMD'), (46, 'KES'), (47, 'KZT'), (48, 'MYR'), (49, 'NAD'), (50, 'NGN'), (51, 'AZN'), (52, 'PAB'), (53, 'PHP'), (54, 'PKR'), (55, 'QAR'), (56, 'SAR'), (57, 'THB'), (58, 'TTD'), (59, 'VND'), (60, 'XOF'), (61, 'TWD'), (62, 'TZS'), (63, 'XAF'), (64, 'UAH'), (65, 'EGP'), (66, 'LKR'), (67, 'MAD'), (68, 'AED'), (69, 'TND'), (70, 'ETB'), (71, 'GEL'), (72, 'UGX'), (73, 'RSD'), (74, 'IRT'), (75, 'BDT'), (76, 'ALL'), (77, 'DZD'), (300, 'XAU'), (1000, 'BTC')], unique=True),
+ ),
+ ]
diff --git a/api/migrations/0050_alter_order_status.py b/api/migrations/0050_alter_order_status.py
new file mode 100644
index 00000000..731f07b0
--- /dev/null
+++ b/api/migrations/0050_alter_order_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.8 on 2024-08-22 08:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0049_alter_currency_currency'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='order',
+ name='status',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting for maker bond'), (1, 'Public'), (2, 'Paused'), (3, 'Waiting for taker bond'), (4, 'Cancelled'), (5, 'Expired'), (6, 'Waiting for trade collateral and buyer invoice'), (7, 'Waiting only for seller trade collateral'), (8, 'Waiting only for buyer invoice'), (9, 'Sending fiat - In chatroom'), (10, 'Fiat sent - In chatroom'), (11, 'In dispute'), (12, 'Collaboratively cancelled'), (13, 'Sending satoshis to buyer'), (14, 'Successful trade'), (15, 'Failed lightning network routing'), (16, 'Wait for dispute resolution'), (17, 'Maker lost dispute'), (18, 'Taker lost dispute')], default=0),
+ ),
+ ]
diff --git a/api/models/__init__.py b/api/models/__init__.py
index d29ac0e4..645a7fac 100644
--- a/api/models/__init__.py
+++ b/api/models/__init__.py
@@ -4,5 +4,14 @@ from .market_tick import MarketTick
from .onchain_payment import OnchainPayment
from .order import Order
from .robot import Robot
+from .notification import Notification
-__all__ = ["Currency", "LNPayment", "MarketTick", "OnchainPayment", "Order", "Robot"]
+__all__ = [
+ "Currency",
+ "LNPayment",
+ "MarketTick",
+ "OnchainPayment",
+ "Order",
+ "Robot",
+ "Notification",
+]
diff --git a/api/models/notification.py b/api/models/notification.py
new file mode 100644
index 00000000..790b98dc
--- /dev/null
+++ b/api/models/notification.py
@@ -0,0 +1,35 @@
+# We use custom seeded UUID generation during testing
+import uuid
+
+from decouple import config
+from api.models import Order, Robot
+from django.db import models
+from django.utils import timezone
+
+if config("TESTING", cast=bool, default=False):
+ import random
+ import string
+
+ random.seed(1)
+ chars = string.ascii_lowercase + string.digits
+
+ def custom_uuid():
+ return uuid.uuid5(uuid.NAMESPACE_DNS, "".join(random.choices(chars, k=20)))
+
+else:
+ custom_uuid = uuid.uuid4
+
+
+class Notification(models.Model):
+ # notification info
+ created_at = models.DateTimeField(default=timezone.now)
+
+ robot = models.ForeignKey(Robot, on_delete=models.CASCADE, default=None)
+ order = models.ForeignKey(Order, on_delete=models.CASCADE, default=None)
+
+ # notification details
+ title = models.CharField(max_length=240, null=False, default=None)
+ description = models.CharField(max_length=240, default=None, blank=True)
+
+ def __str__(self):
+ return f"{self.title} {self.description}"
diff --git a/api/models/order.py b/api/models/order.py
index 161f6d3e..483e6fa4 100644
--- a/api/models/order.py
+++ b/api/models/order.py
@@ -10,6 +10,7 @@ from django.db import models
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
@@ -45,7 +46,7 @@ class Order(models.Model):
DIS = 11, "In dispute"
CCA = 12, "Collaboratively cancelled"
PAY = 13, "Sending satoshis to buyer"
- SUC = 14, "Sucessful trade"
+ SUC = 14, "Successful trade"
FAI = 15, "Failed lightning network routing"
WFR = 16, "Wait for dispute resolution"
MLD = 17, "Maker lost dispute"
@@ -91,10 +92,7 @@ class Order(models.Model):
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 @@ class Order(models.Model):
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/models/robot.py b/api/models/robot.py
index b6a99f8d..c838d354 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):
@@ -88,25 +84,5 @@ class Robot(models.Model):
def save_user_robot(sender, instance, **kwargs):
instance.robot.save()
- @receiver(pre_delete, sender=User)
- def del_avatar_from_disk(sender, instance, **kwargs):
- try:
- avatar_file = Path(
- settings.AVATAR_ROOT + instance.robot.avatar.url.split("/")[-1]
- )
- avatar_file.unlink()
- except Exception:
- pass
-
def __str__(self):
return self.user.username
-
- # to display avatars in admin panel
- def get_avatar(self):
- if not self.avatar:
- return settings.STATIC_ROOT + "unknown_avatar.png"
- return self.avatar.url
-
- # method to create a fake table field in read only mode
- def avatar_tag(self):
- return mark_safe('' % self.get_avatar())
diff --git a/api/notifications.py b/api/notifications.py
index 02319197..57ec4a42 100644
--- a/api/notifications.py
+++ b/api/notifications.py
@@ -1,12 +1,14 @@
from secrets import token_urlsafe
from decouple import config
-
-from api.models import Order
+from api.models import (
+ Order,
+ Notification,
+)
from api.utils import get_session
-class Telegram:
+class Notifications:
"""Simple telegram messages using TG's API"""
session = get_session()
@@ -29,13 +31,24 @@ class Telegram:
return context
- def send_message(self, chat_id, text):
+ def send_message(self, order, robot, title, description=""):
+ """Save a message for a user and sends it to Telegram"""
+ self.save_message(order, robot, title, description)
+ if robot.telegram_enabled:
+ self.send_telegram_message(robot.telegram_chat_id, title, description)
+
+ def save_message(self, order, robot, title, description=""):
+ """Save a message for a user"""
+ Notification.objects.create(
+ title=title, description=description, robot=robot, order=order
+ )
+
+ def send_telegram_message(self, chat_id, title, description=""):
"""sends a message to a user with telegram notifications enabled"""
bot_token = config("TELEGRAM_TOKEN")
-
+ text = f"{title} {description}"
message_url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}"
-
# if it fails, it should keep trying
while True:
try:
@@ -49,119 +62,127 @@ class Telegram:
lang = user.robot.telegram_lang_code
if lang == "es":
- text = f"🔔 Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats."
+ title = f"🔔 Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats."
else:
- text = f"🔔 Hey {user.username}, I will send you notifications about your RoboSats orders."
- self.send_message(user.robot.telegram_chat_id, text)
+ title = f"🔔 Hey {user.username}, I will send you notifications about your RoboSats orders."
+ self.send_telegram_message(user.robot.telegram_chat_id, title)
user.robot.telegram_welcomed = True
user.robot.save(update_fields=["telegram_welcomed"])
return
def order_taken_confirmed(self, order):
- if order.maker.robot.telegram_enabled:
- lang = order.maker.robot.telegram_lang_code
- if lang == "es":
- text = f"✅ Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar."
- else:
- text = f"✅ Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade."
- self.send_message(order.maker.robot.telegram_chat_id, text)
+ lang = order.maker.robot.telegram_lang_code
+ if lang == "es":
+ title = f"✅ Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳"
+ description = f"Visita http://{self.site}/order/{order.id} para continuar."
+ else:
+ title = f"✅ Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳"
+ description = (
+ f"Visit http://{self.site}/order/{order.id} to proceed with the trade."
+ )
+ self.send_message(order, order.maker.robot, title, description)
- if order.taker.robot.telegram_enabled:
- lang = order.taker.robot.telegram_lang_code
- if lang == "es":
- text = f"✅ Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
- else:
- text = f"✅ Hey {order.taker.username}, you just took the order with ID {order.id}."
- self.send_message(order.taker.robot.telegram_chat_id, text)
+ lang = order.taker.robot.telegram_lang_code
+ if lang == "es":
+ title = f"✅ Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
+ else:
+ title = f"✅ Hey {order.taker.username}, you just took the order with ID {order.id}."
+ self.send_message(order, order.taker.robot, title)
return
def fiat_exchange_starts(self, order):
for user in [order.maker, order.taker]:
- if user.robot.telegram_enabled:
- lang = user.robot.telegram_lang_code
- if lang == "es":
- text = f"✅ Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte."
- else:
- text = f"✅ Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart."
- self.send_message(user.robot.telegram_chat_id, text)
+ lang = user.robot.telegram_lang_code
+ if lang == "es":
+ title = f"✅ Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat."
+ description = f"Visita http://{self.site}/order/{order.id} para hablar con tu contraparte."
+ else:
+ title = f"✅ Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat."
+ description = f"Visit http://{self.site}/order/{order.id} to talk with your counterpart."
+ self.send_message(order, user.robot, title, description)
return
def order_expired_untaken(self, order):
- if order.maker.robot.telegram_enabled:
- lang = order.maker.robot.telegram_lang_code
- if lang == "es":
- text = f"😪 Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla."
- else:
- text = f"😪 Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it."
- self.send_message(order.maker.robot.telegram_chat_id, text)
+ lang = order.maker.robot.telegram_lang_code
+ if lang == "es":
+ title = f"😪 Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot."
+ description = f"Visita http://{self.site}/order/{order.id} para renovarla."
+ else:
+ title = f"😪 Hey {order.maker.username}, your order with ID {order.id} has expired without a taker."
+ description = f"Visit http://{self.site}/order/{order.id} to renew it."
+ self.send_message(order, order.maker.robot, title, description)
return
def trade_successful(self, order):
for user in [order.maker, order.taker]:
- if user.robot.telegram_enabled:
- lang = user.robot.telegram_lang_code
- if lang == "es":
- text = f"🥳 ¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar."
- else:
- text = f"🥳 Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve."
- self.send_message(user.robot.telegram_chat_id, text)
+ lang = user.robot.telegram_lang_code
+ if lang == "es":
+ title = f"🥳 ¡Tu orden con ID {order.id} ha finalizado exitosamente!"
+ description = (
+ "⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar."
+ )
+ else:
+ title = f"🥳 Your order with ID {order.id} has finished successfully!"
+ description = "⚡ Join us @robosats and help us improve."
+ self.send_message(order, user.robot, title, description)
return
def public_order_cancelled(self, order):
- if order.maker.robot.telegram_enabled:
- lang = order.maker.robot.telegram_lang_code
- if lang == "es":
- text = f"❌ Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
- else:
- text = f"❌ Hey {order.maker.username}, you have cancelled your public order with ID {order.id}."
- self.send_message(order.maker.robot.telegram_chat_id, text)
+ lang = order.maker.robot.telegram_lang_code
+ if lang == "es":
+ title = f"❌ Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
+ else:
+ title = f"❌ Hey {order.maker.username}, you have cancelled your public order with ID {order.id}."
+ self.send_message(order, order.maker.robot, title)
return
def collaborative_cancelled(self, order):
for user in [order.maker, order.taker]:
- if user.robot.telegram_enabled:
- lang = user.robot.telegram_lang_code
- if lang == "es":
- text = f"❌ Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
- else:
- text = f"❌ Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled."
- self.send_message(user.robot.telegram_chat_id, text)
+ lang = user.robot.telegram_lang_code
+ if lang == "es":
+ title = f"❌ Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
+ else:
+ title = f"❌ Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled."
+ self.send_message(order, user.robot, title)
return
def dispute_opened(self, order):
for user in [order.maker, order.taker]:
- if user.robot.telegram_enabled:
- lang = user.robot.telegram_lang_code
- if lang == "es":
- text = f"⚖️ Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
- else:
- text = f"⚖️ Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}."
- self.send_message(user.robot.telegram_chat_id, text)
+ lang = user.robot.telegram_lang_code
+ if lang == "es":
+ title = f"⚖️ Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
+ else:
+ title = f"⚖️ Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}."
+ self.send_message(order, user.robot, title)
admin_chat_id = config("TELEGRAM_COORDINATOR_CHAT_ID")
if len(admin_chat_id) == 0:
return
- coordinator_text = f"There is a new dispute opened for the order with ID {str(order.id)}. Visit http://{self.site}/coordinator/api/order/{str(order.id)}/change to proceed."
- self.send_message(admin_chat_id, coordinator_text)
+ coordinator_text = (
+ f"There is a new dispute opened for the order with ID {str(order.id)}."
+ )
+ coordinator_description = f"Visit http://{self.site}/coordinator/api/order/{str(order.id)}/change to proceed."
+ self.send_telegram_message(
+ admin_chat_id, coordinator_text, coordinator_description
+ )
return
def order_published(self, order):
- if order.maker.robot.telegram_enabled:
- lang = order.maker.robot.telegram_lang_code
- # In weird cases the order cannot be found (e.g. it is cancelled)
- queryset = Order.objects.filter(maker=order.maker)
- if len(queryset) == 0:
- return
- order = queryset.last()
- if lang == "es":
- text = f"✅ Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
- else:
- text = f"✅ Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book."
- self.send_message(order.maker.robot.telegram_chat_id, text)
+ lang = order.maker.robot.telegram_lang_code
+ # In weird cases the order cannot be found (e.g. it is cancelled)
+ queryset = Order.objects.filter(maker=order.maker)
+ if len(queryset) == 0:
+ return
+ order = queryset.last()
+ if lang == "es":
+ title = f"✅ Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
+ else:
+ title = f"✅ Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book."
+ self.send_message(order, order.maker.robot, title)
return
def new_chat_message(self, order, chat_message):
@@ -189,14 +210,56 @@ class Telegram:
notification_reason = f"(You receive this notification because this was the first in-chat message. You will only be notified again if there is a gap bigger than {TIMEGAP} minutes between messages)"
user = chat_message.receiver
- if user.robot.telegram_enabled:
- text = f"💬 Hey {user.username}, a new chat message in-app was sent to you by {chat_message.sender.username} for order ID {str(order.id)}. {notification_reason}"
- self.send_message(user.robot.telegram_chat_id, text)
+ title = f"💬 Hey {user.username}, a new chat message in-app was sent to you by {chat_message.sender.username} for order ID {str(order.id)}."
+ self.send_message(order, user.robot, title, notification_reason)
return
def coordinator_cancelled(self, order):
- if order.maker.robot.telegram_enabled:
- text = 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.maker.robot.telegram_chat_id, text)
+ 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/oas_schemas.py b/api/oas_schemas.py
index 8b6412e6..8d214f32 100644
--- a/api/oas_schemas.py
+++ b/api/oas_schemas.py
@@ -112,7 +112,7 @@ class OrderViewSchema:
- `11` "In dispute"
- `12` "Collaboratively cancelled"
- `13` "Sending satoshis to buyer"
- - `14` "Sucessful trade"
+ - `14` "Successful trade"
- `15` "Failed lightning network routing"
- `16` "Wait for dispute resolution"
- `17` "Maker lost dispute"
@@ -378,6 +378,21 @@ class BookViewSchema:
}
+class NotificationSchema:
+ get = {
+ "summary": "Get robot notifications",
+ "description": "Get a list of notifications sent to the robot.",
+ "parameters": [
+ OpenApiParameter(
+ name="created_at",
+ location=OpenApiParameter.QUERY,
+ description=("Shows notifications created AFTER this date."),
+ type=str,
+ ),
+ ],
+ }
+
+
class RobotViewSchema:
get = {
"summary": "Get robot info",
diff --git a/api/serializers.py b/api/serializers.py
index 4bd41fd2..46fb1c58 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -2,7 +2,7 @@ from decouple import config
from decimal import Decimal
from rest_framework import serializers
-from .models import MarketTick, Order
+from .models import MarketTick, Order, Notification
RETRY_TIME = int(config("RETRY_TIME"))
@@ -490,6 +490,19 @@ class OrderDetailSerializer(serializers.ModelSerializer):
)
+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", "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/api/tasks.py b/api/tasks.py
index dee5709a..656feb89 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -263,48 +263,50 @@ def send_notification(order_id=None, chat_message_id=None, message=None):
chat_message = Message.objects.get(id=chat_message_id)
order = chat_message.order
- taker_enabled = False if order.taker is None else order.taker.robot.telegram_enabled
- if not (order.maker.robot.telegram_enabled or taker_enabled):
- return
+ from api.notifications import Notifications
- from api.notifications import Telegram
-
- telegram = Telegram()
+ notifications = Notifications()
if message == "welcome":
- telegram.welcome(order)
+ notifications.welcome(order)
elif message == "order_expired_untaken":
- telegram.order_expired_untaken(order)
+ notifications.order_expired_untaken(order)
elif message == "trade_successful":
- telegram.trade_successful(order)
+ notifications.trade_successful(order)
elif message == "public_order_cancelled":
- telegram.public_order_cancelled(order)
+ notifications.public_order_cancelled(order)
elif message == "taker_expired_b4bond":
- telegram.taker_expired_b4bond(order)
+ notifications.taker_expired_b4bond(order)
elif message == "order_published":
- telegram.order_published(order)
+ notifications.order_published(order)
elif message == "order_taken_confirmed":
- telegram.order_taken_confirmed(order)
+ notifications.order_taken_confirmed(order)
elif message == "fiat_exchange_starts":
- telegram.fiat_exchange_starts(order)
+ notifications.fiat_exchange_starts(order)
elif message == "dispute_opened":
- telegram.dispute_opened(order)
+ notifications.dispute_opened(order)
elif message == "collaborative_cancelled":
- telegram.collaborative_cancelled(order)
+ notifications.collaborative_cancelled(order)
elif message == "new_chat_message":
- telegram.new_chat_message(order, chat_message)
+ notifications.new_chat_message(order, chat_message)
elif message == "coordinator_cancelled":
- telegram.coordinator_cancelled(order)
+ 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/urls.py b/api/urls.py
index 7d8b19c6..7264c1e6 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -15,6 +15,7 @@ from .views import (
RobotView,
StealthView,
TickView,
+ NotificationsView,
)
urlpatterns = [
@@ -36,4 +37,5 @@ urlpatterns = [
path("ticks/", TickView.as_view(), name="ticks"),
path("stealth/", StealthView.as_view(), name="stealth"),
path("chat/", ChatView.as_view({"get": "get", "post": "post"}), name="chat"),
+ path("notifications/", NotificationsView.as_view(), name="notifications"),
]
diff --git a/api/utils.py b/api/utils.py
index 13fad5b0..10f938c1 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -141,7 +141,7 @@ def get_devfund_pubkey(network: str) -> str:
"""
session = get_session()
- url = "https://raw.githubusercontent.com/RoboSats/robosats/main/devfund_pubey.json"
+ url = "https://raw.githubusercontent.com/RoboSats/robosats/main/devfund_pubkey.json"
try:
response = session.get(url)
@@ -188,8 +188,7 @@ def get_exchange_rates(currencies):
blockchain_rates.append(
float(blockchain_prices[currency]["last"])
)
- except Exception as e:
- print(e)
+ except Exception:
blockchain_rates.append(np.nan)
api_rates.append(blockchain_rates)
diff --git a/api/views.py b/api/views.py
index 49a21a2c..a03dbd33 100644
--- a/api/views.py
+++ b/api/views.py
@@ -5,6 +5,8 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Q, Sum
from django.utils import timezone
+from django.utils.dateparse import parse_datetime
+from django.http import HttpResponseBadRequest
from drf_spectacular.utils import extend_schema
from rest_framework import status, viewsets
from rest_framework.authentication import TokenAuthentication
@@ -14,8 +16,15 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from api.logics import Logics
-from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
-from api.notifications import Telegram
+from api.models import (
+ Currency,
+ LNPayment,
+ MarketTick,
+ OnchainPayment,
+ Order,
+ Notification,
+)
+from api.notifications import Notifications
from api.oas_schemas import (
BookViewSchema,
HistoricalViewSchema,
@@ -28,6 +37,7 @@ from api.oas_schemas import (
RobotViewSchema,
StealthViewSchema,
TickViewSchema,
+ NotificationSchema,
)
from api.serializers import (
ClaimRewardSerializer,
@@ -39,6 +49,7 @@ from api.serializers import (
StealthSerializer,
TickSerializer,
UpdateOrderSerializer,
+ ListNotificationSerializer,
)
from api.utils import (
compute_avg_premium,
@@ -659,7 +670,7 @@ class RobotView(APIView):
context["last_login"] = user.last_login
# Adds/generate telegram token and whether it is enabled
- context = {**context, **Telegram.get_context(user)}
+ context = {**context, **Notifications.get_context(user)}
# return active order or last made order if any
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
@@ -730,6 +741,32 @@ class BookView(ListAPIView):
return Response(book_data, status=status.HTTP_200_OK)
+class NotificationsView(ListAPIView):
+ authentication_classes = [TokenAuthentication]
+ permission_classes = [IsAuthenticated]
+ serializer_class = ListNotificationSerializer
+
+ @extend_schema(**NotificationSchema.get)
+ def get(self, request, format=None):
+ robot = request.user.robot
+ queryset = Notification.objects.filter(robot=robot).order_by("-created_at")
+ created_at = request.GET.get("created_at")
+
+ if created_at:
+ created_at = parse_datetime(created_at)
+ if not created_at:
+ return HttpResponseBadRequest("Invalid date format")
+ queryset = queryset.filter(created_at__gte=created_at)
+
+ notification_data = []
+ for notification in queryset:
+ data = self.serializer_class(notification).data
+ data["order_id"] = notification.order.id
+ notification_data.append(data)
+
+ return Response(notification_data, status=status.HTTP_200_OK)
+
+
class InfoView(viewsets.ViewSet):
serializer_class = InfoSerializer
diff --git a/docker-tests.yml b/docker-tests.yml
index 0a4af45e..975747e8 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 67735029..04a0577f 100644
--- a/docs/assets/schemas/api-latest.yaml
+++ b/docs/assets/schemas/api-latest.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: RoboSats REST API
- version: 0.6.2
+ version: 0.6.3
x-logo:
url: https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png
backgroundColor: '#FFFFFF'
@@ -284,6 +284,30 @@ paths:
type: string
description: Reason for the failure
description: ''
+ /api/notifications/:
+ get:
+ operationId: notifications_list
+ description: Get a list of notifications sent to the robot.
+ summary: Get robot notifications
+ parameters:
+ - in: query
+ name: created_at
+ schema:
+ type: string
+ description: Shows notifications created AFTER this date.
+ tags:
+ - notifications
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/ListNotification'
+ description: ''
/api/order/:
get:
operationId: order_retrieve
@@ -341,7 +365,7 @@ paths:
- `11` "In dispute"
- `12` "Collaboratively cancelled"
- `13` "Sending satoshis to buyer"
- - `14` "Sucessful trade"
+ - `14` "Successful trade"
- `15` "Failed lightning network routing"
- `16` "Wait for dispute resolution"
- `17` "Maker lost dispute"
@@ -1070,6 +1094,28 @@ components:
- swap_enabled
- taker_fee
- version
+ ListNotification:
+ type: object
+ properties:
+ title:
+ type: string
+ maxLength: 240
+ description:
+ type: string
+ maxLength: 240
+ order_id:
+ type: integer
+ readOnly: true
+ status:
+ 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
ListOrder:
type: object
properties:
@@ -1787,7 +1833,7 @@ components:
* `11` - In dispute
* `12` - Collaboratively cancelled
* `13` - Sending satoshis to buyer
- * `14` - Sucessful trade
+ * `14` - Successful trade
* `15` - Failed lightning network routing
* `16` - Wait for dispute resolution
* `17` - Maker lost dispute
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ec3cec07..1251e685 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "frontend",
- "version": "0.6.2",
+ "version": "0.6.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
- "version": "0.6.2",
+ "version": "0.6.3",
"license": "ISC",
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
@@ -23,23 +23,23 @@
"@nivo/core": "^0.86.0",
"@nivo/line": "^0.86.0",
"base-ex": "^0.8.1",
- "country-flag-icons": "^1.5.11",
+ "country-flag-icons": "^1.5.13",
"date-fns": "^2.30.0",
"file-replace-loader": "^1.4.0",
"i18next": "^23.2.11",
- "i18next-browser-languagedetector": "^7.2.1",
+ "i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.0",
"install": "^0.13.0",
"js-sha256": "^0.11.0",
"leaflet": "^1.9.4",
"light-bolt11-decoder": "^3.1.1",
- "npm": "^10.5.0",
+ "npm": "^10.8.1",
"openpgp": "^5.11.0",
"react": "^18.2.0",
"react-countdown": "^2.3.5",
"react-dom": "^18.2.0",
"react-grid-layout": "^1.4.4",
- "react-i18next": "^14.1.0",
+ "react-i18next": "^14.1.2",
"react-image": "^4.1.0",
"react-leaflet": "^4.2.1",
"react-qr-code": "^2.0.11",
@@ -81,7 +81,7 @@
"jest": "^29.6.1",
"prettier": "^3.3.2",
"ts-node": "^10.9.2",
- "typescript": "^5.4.2",
+ "typescript": "^5.5.4",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
@@ -6411,9 +6411,9 @@
}
},
"node_modules/country-flag-icons": {
- "version": "1.5.11",
- "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.11.tgz",
- "integrity": "sha512-B+mvFywunkRJs270k7kCBjhogvIA0uNn6GAXv6m2cPn3rrwqZzZVr2gBWcz+Cz7OGVWlcbERlYRIX0S6OGr8Bw=="
+ "version": "1.5.13",
+ "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.13.tgz",
+ "integrity": "sha512-4JwHNqaKZ19doQoNcBjsoYA+I7NqCH/mC/6f5cBWvdKzcK5TMmzLpq3Z/syVHMHJuDGFwJ+rPpGizvrqJybJow=="
},
"node_modules/create-require": {
"version": "1.1.1",
@@ -8823,9 +8823,9 @@
}
},
"node_modules/i18next-browser-languagedetector": {
- "version": "7.2.1",
- "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz",
- "integrity": "sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
+ "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
@@ -11978,9 +11978,9 @@
}
},
"node_modules/npm": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/npm/-/npm-10.5.0.tgz",
- "integrity": "sha512-Ejxwvfh9YnWVU2yA5FzoYLTW52vxHCz+MHrOFg9Cc8IFgF/6f5AGPAvb5WTay5DIUP1NIfN3VBZ0cLlGO0Ys+A==",
+ "version": "10.8.1",
+ "resolved": "https://registry.npmjs.org/npm/-/npm-10.8.1.tgz",
+ "integrity": "sha512-Dp1C6SvSMYQI7YHq/y2l94uvI+59Eqbu1EpuKQHQ8p16txXRuRit5gH3Lnaagk2aXDIjg/Iru9pd05bnneKgdw==",
"bundleDependencies": [
"@isaacs/string-locale-compare",
"@npmcli/arborist",
@@ -11989,6 +11989,7 @@
"@npmcli/map-workspaces",
"@npmcli/package-json",
"@npmcli/promise-spawn",
+ "@npmcli/redact",
"@npmcli/run-script",
"@sigstore/tuf",
"abbrev",
@@ -11997,8 +11998,6 @@
"chalk",
"ci-info",
"cli-columns",
- "cli-table3",
- "columnify",
"fastest-levenshtein",
"fs-minipass",
"glob",
@@ -12034,7 +12033,6 @@
"npm-profile",
"npm-registry-fetch",
"npm-user-validate",
- "npmlog",
"p-map",
"pacote",
"parse-conflict-json",
@@ -12055,73 +12053,71 @@
],
"dependencies": {
"@isaacs/string-locale-compare": "^1.1.0",
- "@npmcli/arborist": "^7.2.1",
- "@npmcli/config": "^8.0.2",
- "@npmcli/fs": "^3.1.0",
- "@npmcli/map-workspaces": "^3.0.4",
- "@npmcli/package-json": "^5.0.0",
- "@npmcli/promise-spawn": "^7.0.1",
- "@npmcli/run-script": "^7.0.4",
- "@sigstore/tuf": "^2.3.1",
+ "@npmcli/arborist": "^7.5.3",
+ "@npmcli/config": "^8.3.3",
+ "@npmcli/fs": "^3.1.1",
+ "@npmcli/map-workspaces": "^3.0.6",
+ "@npmcli/package-json": "^5.1.1",
+ "@npmcli/promise-spawn": "^7.0.2",
+ "@npmcli/redact": "^2.0.0",
+ "@npmcli/run-script": "^8.1.0",
+ "@sigstore/tuf": "^2.3.4",
"abbrev": "^2.0.0",
"archy": "~1.0.0",
- "cacache": "^18.0.2",
+ "cacache": "^18.0.3",
"chalk": "^5.3.0",
"ci-info": "^4.0.0",
"cli-columns": "^4.0.0",
- "cli-table3": "^0.6.3",
- "columnify": "^1.6.0",
"fastest-levenshtein": "^1.0.16",
"fs-minipass": "^3.0.3",
- "glob": "^10.3.10",
+ "glob": "^10.4.1",
"graceful-fs": "^4.2.11",
- "hosted-git-info": "^7.0.1",
- "ini": "^4.1.1",
- "init-package-json": "^6.0.0",
- "is-cidr": "^5.0.3",
- "json-parse-even-better-errors": "^3.0.1",
- "libnpmaccess": "^8.0.1",
- "libnpmdiff": "^6.0.3",
- "libnpmexec": "^7.0.4",
- "libnpmfund": "^5.0.1",
- "libnpmhook": "^10.0.0",
- "libnpmorg": "^6.0.1",
- "libnpmpack": "^6.0.3",
- "libnpmpublish": "^9.0.2",
- "libnpmsearch": "^7.0.0",
- "libnpmteam": "^6.0.0",
- "libnpmversion": "^5.0.1",
- "make-fetch-happen": "^13.0.0",
- "minimatch": "^9.0.3",
- "minipass": "^7.0.4",
+ "hosted-git-info": "^7.0.2",
+ "ini": "^4.1.3",
+ "init-package-json": "^6.0.3",
+ "is-cidr": "^5.1.0",
+ "json-parse-even-better-errors": "^3.0.2",
+ "libnpmaccess": "^8.0.6",
+ "libnpmdiff": "^6.1.3",
+ "libnpmexec": "^8.1.2",
+ "libnpmfund": "^5.0.11",
+ "libnpmhook": "^10.0.5",
+ "libnpmorg": "^6.0.6",
+ "libnpmpack": "^7.0.3",
+ "libnpmpublish": "^9.0.9",
+ "libnpmsearch": "^7.0.6",
+ "libnpmteam": "^6.0.5",
+ "libnpmversion": "^6.0.3",
+ "make-fetch-happen": "^13.0.1",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.1",
"minipass-pipeline": "^1.2.4",
"ms": "^2.1.2",
- "node-gyp": "^10.0.1",
- "nopt": "^7.2.0",
- "normalize-package-data": "^6.0.0",
+ "node-gyp": "^10.1.0",
+ "nopt": "^7.2.1",
+ "normalize-package-data": "^6.0.1",
"npm-audit-report": "^5.0.0",
"npm-install-checks": "^6.3.0",
- "npm-package-arg": "^11.0.1",
- "npm-pick-manifest": "^9.0.0",
- "npm-profile": "^9.0.0",
- "npm-registry-fetch": "^16.1.0",
- "npm-user-validate": "^2.0.0",
- "npmlog": "^7.0.1",
+ "npm-package-arg": "^11.0.2",
+ "npm-pick-manifest": "^9.0.1",
+ "npm-profile": "^10.0.0",
+ "npm-registry-fetch": "^17.0.1",
+ "npm-user-validate": "^2.0.1",
"p-map": "^4.0.0",
- "pacote": "^17.0.6",
+ "pacote": "^18.0.6",
"parse-conflict-json": "^3.0.1",
- "proc-log": "^3.0.0",
+ "proc-log": "^4.2.0",
"qrcode-terminal": "^0.12.0",
- "read": "^2.1.0",
- "semver": "^7.6.0",
- "spdx-expression-parse": "^3.0.1",
- "ssri": "^10.0.5",
+ "read": "^3.0.1",
+ "semver": "^7.6.2",
+ "spdx-expression-parse": "^4.0.0",
+ "ssri": "^10.0.6",
"supports-color": "^9.4.0",
- "tar": "^6.2.0",
+ "tar": "^6.2.1",
"text-table": "~0.2.0",
"tiny-relative-date": "^1.3.0",
"treeverse": "^3.0.0",
- "validate-npm-package-name": "^5.0.0",
+ "validate-npm-package-name": "^5.0.1",
"which": "^4.0.0",
"write-file-atomic": "^5.0.1"
},
@@ -12145,15 +12141,6 @@
"node": ">=8"
}
},
- "node_modules/npm/node_modules/@colors/colors": {
- "version": "1.5.0",
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=0.1.90"
- }
- },
"node_modules/npm/node_modules/@isaacs/cliui": {
"version": "8.0.2",
"inBundle": true,
@@ -12222,7 +12209,7 @@
"license": "ISC"
},
"node_modules/npm/node_modules/@npmcli/agent": {
- "version": "2.2.1",
+ "version": "2.2.2",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12230,48 +12217,50 @@
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.1",
"lru-cache": "^10.0.1",
- "socks-proxy-agent": "^8.0.1"
+ "socks-proxy-agent": "^8.0.3"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/@npmcli/arborist": {
- "version": "7.4.0",
+ "version": "7.5.3",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@isaacs/string-locale-compare": "^1.1.0",
- "@npmcli/fs": "^3.1.0",
- "@npmcli/installed-package-contents": "^2.0.2",
+ "@npmcli/fs": "^3.1.1",
+ "@npmcli/installed-package-contents": "^2.1.0",
"@npmcli/map-workspaces": "^3.0.2",
- "@npmcli/metavuln-calculator": "^7.0.0",
+ "@npmcli/metavuln-calculator": "^7.1.1",
"@npmcli/name-from-folder": "^2.0.0",
"@npmcli/node-gyp": "^3.0.0",
- "@npmcli/package-json": "^5.0.0",
+ "@npmcli/package-json": "^5.1.0",
"@npmcli/query": "^3.1.0",
- "@npmcli/run-script": "^7.0.2",
- "bin-links": "^4.0.1",
- "cacache": "^18.0.0",
+ "@npmcli/redact": "^2.0.0",
+ "@npmcli/run-script": "^8.1.0",
+ "bin-links": "^4.0.4",
+ "cacache": "^18.0.3",
"common-ancestor-path": "^1.0.1",
- "hosted-git-info": "^7.0.1",
- "json-parse-even-better-errors": "^3.0.0",
+ "hosted-git-info": "^7.0.2",
+ "json-parse-even-better-errors": "^3.0.2",
"json-stringify-nice": "^1.1.4",
- "minimatch": "^9.0.0",
- "nopt": "^7.0.0",
+ "lru-cache": "^10.2.2",
+ "minimatch": "^9.0.4",
+ "nopt": "^7.2.1",
"npm-install-checks": "^6.2.0",
- "npm-package-arg": "^11.0.1",
- "npm-pick-manifest": "^9.0.0",
- "npm-registry-fetch": "^16.0.0",
- "npmlog": "^7.0.1",
- "pacote": "^17.0.4",
+ "npm-package-arg": "^11.0.2",
+ "npm-pick-manifest": "^9.0.1",
+ "npm-registry-fetch": "^17.0.1",
+ "pacote": "^18.0.6",
"parse-conflict-json": "^3.0.0",
- "proc-log": "^3.0.0",
+ "proc-log": "^4.2.0",
+ "proggy": "^2.0.0",
"promise-all-reject-late": "^1.0.0",
"promise-call-limit": "^3.0.1",
"read-package-json-fast": "^3.0.2",
"semver": "^7.3.7",
- "ssri": "^10.0.5",
+ "ssri": "^10.0.6",
"treeverse": "^3.0.0",
"walk-up-path": "^3.0.1"
},
@@ -12283,15 +12272,15 @@
}
},
"node_modules/npm/node_modules/@npmcli/config": {
- "version": "8.2.0",
+ "version": "8.3.3",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@npmcli/map-workspaces": "^3.0.2",
"ci-info": "^4.0.0",
- "ini": "^4.1.0",
- "nopt": "^7.0.0",
- "proc-log": "^3.0.0",
+ "ini": "^4.1.2",
+ "nopt": "^7.2.1",
+ "proc-log": "^4.2.0",
"read-package-json-fast": "^3.0.2",
"semver": "^7.3.5",
"walk-up-path": "^3.0.1"
@@ -12300,33 +12289,8 @@
"node": "^16.14.0 || >=18.0.0"
}
},
- "node_modules/npm/node_modules/@npmcli/disparity-colors": {
- "version": "3.0.0",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "ansi-styles": "^4.3.0"
- },
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
- "node_modules/npm/node_modules/@npmcli/disparity-colors/node_modules/ansi-styles": {
- "version": "4.3.0",
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
"node_modules/npm/node_modules/@npmcli/fs": {
- "version": "3.1.0",
+ "version": "3.1.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12337,14 +12301,14 @@
}
},
"node_modules/npm/node_modules/@npmcli/git": {
- "version": "5.0.4",
+ "version": "5.0.7",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@npmcli/promise-spawn": "^7.0.0",
"lru-cache": "^10.0.1",
"npm-pick-manifest": "^9.0.0",
- "proc-log": "^3.0.0",
+ "proc-log": "^4.0.0",
"promise-inflight": "^1.0.1",
"promise-retry": "^2.0.1",
"semver": "^7.3.5",
@@ -12355,7 +12319,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/installed-package-contents": {
- "version": "2.0.2",
+ "version": "2.1.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12363,14 +12327,14 @@
"npm-normalize-package-bin": "^3.0.0"
},
"bin": {
- "installed-package-contents": "lib/index.js"
+ "installed-package-contents": "bin/index.js"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/@npmcli/map-workspaces": {
- "version": "3.0.4",
+ "version": "3.0.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12384,13 +12348,14 @@
}
},
"node_modules/npm/node_modules/@npmcli/metavuln-calculator": {
- "version": "7.0.0",
+ "version": "7.1.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
"cacache": "^18.0.0",
"json-parse-even-better-errors": "^3.0.0",
- "pacote": "^17.0.0",
+ "pacote": "^18.0.0",
+ "proc-log": "^4.1.0",
"semver": "^7.3.5"
},
"engines": {
@@ -12414,7 +12379,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/package-json": {
- "version": "5.0.0",
+ "version": "5.1.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12423,7 +12388,7 @@
"hosted-git-info": "^7.0.0",
"json-parse-even-better-errors": "^3.0.0",
"normalize-package-data": "^6.0.0",
- "proc-log": "^3.0.0",
+ "proc-log": "^4.0.0",
"semver": "^7.5.3"
},
"engines": {
@@ -12431,7 +12396,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/promise-spawn": {
- "version": "7.0.1",
+ "version": "7.0.2",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12452,8 +12417,16 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/npm/node_modules/@npmcli/redact": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
"node_modules/npm/node_modules/@npmcli/run-script": {
- "version": "7.0.4",
+ "version": "8.1.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12461,6 +12434,7 @@
"@npmcli/package-json": "^5.0.0",
"@npmcli/promise-spawn": "^7.0.0",
"node-gyp": "^10.0.0",
+ "proc-log": "^4.0.0",
"which": "^4.0.0"
},
"engines": {
@@ -12477,18 +12451,18 @@
}
},
"node_modules/npm/node_modules/@sigstore/bundle": {
- "version": "2.2.0",
+ "version": "2.3.2",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
- "@sigstore/protobuf-specs": "^0.3.0"
+ "@sigstore/protobuf-specs": "^0.3.2"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/@sigstore/core": {
- "version": "1.0.0",
+ "version": "1.1.0",
"inBundle": true,
"license": "Apache-2.0",
"engines": {
@@ -12496,47 +12470,49 @@
}
},
"node_modules/npm/node_modules/@sigstore/protobuf-specs": {
- "version": "0.3.0",
+ "version": "0.3.2",
"inBundle": true,
"license": "Apache-2.0",
"engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ "node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/@sigstore/sign": {
- "version": "2.2.3",
+ "version": "2.3.2",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
- "@sigstore/bundle": "^2.2.0",
+ "@sigstore/bundle": "^2.3.2",
"@sigstore/core": "^1.0.0",
- "@sigstore/protobuf-specs": "^0.3.0",
- "make-fetch-happen": "^13.0.0"
+ "@sigstore/protobuf-specs": "^0.3.2",
+ "make-fetch-happen": "^13.0.1",
+ "proc-log": "^4.2.0",
+ "promise-retry": "^2.0.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/@sigstore/tuf": {
- "version": "2.3.1",
+ "version": "2.3.4",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
- "@sigstore/protobuf-specs": "^0.3.0",
- "tuf-js": "^2.2.0"
+ "@sigstore/protobuf-specs": "^0.3.2",
+ "tuf-js": "^2.2.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/@sigstore/verify": {
- "version": "1.1.0",
+ "version": "1.2.1",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
- "@sigstore/bundle": "^2.2.0",
- "@sigstore/core": "^1.0.0",
- "@sigstore/protobuf-specs": "^0.3.0"
+ "@sigstore/bundle": "^2.3.2",
+ "@sigstore/core": "^1.1.0",
+ "@sigstore/protobuf-specs": "^0.3.2"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
@@ -12551,12 +12527,12 @@
}
},
"node_modules/npm/node_modules/@tufjs/models": {
- "version": "2.0.0",
+ "version": "2.0.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
"@tufjs/canonical-json": "2.0.0",
- "minimatch": "^9.0.3"
+ "minimatch": "^9.0.4"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
@@ -12571,7 +12547,7 @@
}
},
"node_modules/npm/node_modules/agent-base": {
- "version": "7.1.0",
+ "version": "7.1.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -12622,21 +12598,13 @@
"inBundle": true,
"license": "MIT"
},
- "node_modules/npm/node_modules/are-we-there-yet": {
- "version": "4.0.2",
- "inBundle": true,
- "license": "ISC",
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/npm/node_modules/balanced-match": {
"version": "1.0.2",
"inBundle": true,
"license": "MIT"
},
"node_modules/npm/node_modules/bin-links": {
- "version": "4.0.3",
+ "version": "4.0.4",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12650,11 +12618,14 @@
}
},
"node_modules/npm/node_modules/binary-extensions": {
- "version": "2.2.0",
+ "version": "2.3.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm/node_modules/brace-expansion": {
@@ -12665,16 +12636,8 @@
"balanced-match": "^1.0.0"
}
},
- "node_modules/npm/node_modules/builtins": {
- "version": "5.0.1",
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.0.0"
- }
- },
"node_modules/npm/node_modules/cacache": {
- "version": "18.0.2",
+ "version": "18.0.3",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -12729,7 +12692,7 @@
}
},
"node_modules/npm/node_modules/cidr-regex": {
- "version": "4.0.3",
+ "version": "4.1.1",
"inBundle": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -12759,30 +12722,8 @@
"node": ">= 10"
}
},
- "node_modules/npm/node_modules/cli-table3": {
- "version": "0.6.3",
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "string-width": "^4.2.0"
- },
- "engines": {
- "node": "10.* || >= 12.*"
- },
- "optionalDependencies": {
- "@colors/colors": "1.5.0"
- }
- },
- "node_modules/npm/node_modules/clone": {
- "version": "1.0.4",
- "inBundle": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8"
- }
- },
"node_modules/npm/node_modules/cmd-shim": {
- "version": "6.0.2",
+ "version": "6.0.3",
"inBundle": true,
"license": "ISC",
"engines": {
@@ -12805,36 +12746,11 @@
"inBundle": true,
"license": "MIT"
},
- "node_modules/npm/node_modules/color-support": {
- "version": "1.1.3",
- "inBundle": true,
- "license": "ISC",
- "bin": {
- "color-support": "bin.js"
- }
- },
- "node_modules/npm/node_modules/columnify": {
- "version": "1.6.0",
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "strip-ansi": "^6.0.1",
- "wcwidth": "^1.0.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
"node_modules/npm/node_modules/common-ancestor-path": {
"version": "1.0.1",
"inBundle": true,
"license": "ISC"
},
- "node_modules/npm/node_modules/console-control-strings": {
- "version": "1.1.0",
- "inBundle": true,
- "license": "ISC"
- },
"node_modules/npm/node_modules/cross-spawn": {
"version": "7.0.3",
"inBundle": true,
@@ -12894,17 +12810,6 @@
"inBundle": true,
"license": "MIT"
},
- "node_modules/npm/node_modules/defaults": {
- "version": "1.0.4",
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "clone": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/npm/node_modules/diff": {
"version": "5.2.0",
"inBundle": true,
@@ -12992,40 +12897,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/npm/node_modules/gauge": {
- "version": "5.0.1",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "aproba": "^1.0.3 || ^2.0.0",
- "color-support": "^1.1.3",
- "console-control-strings": "^1.1.0",
- "has-unicode": "^2.0.1",
- "signal-exit": "^4.0.1",
- "string-width": "^4.2.3",
- "strip-ansi": "^6.0.1",
- "wide-align": "^1.1.5"
- },
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/npm/node_modules/glob": {
- "version": "10.3.10",
+ "version": "10.4.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
- "jackspeak": "^2.3.5",
- "minimatch": "^9.0.1",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
- "path-scurry": "^1.10.1"
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -13036,13 +12923,8 @@
"inBundle": true,
"license": "ISC"
},
- "node_modules/npm/node_modules/has-unicode": {
- "version": "2.0.1",
- "inBundle": true,
- "license": "ISC"
- },
"node_modules/npm/node_modules/hasown": {
- "version": "2.0.1",
+ "version": "2.0.2",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -13053,7 +12935,7 @@
}
},
"node_modules/npm/node_modules/hosted-git-info": {
- "version": "7.0.1",
+ "version": "7.0.2",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -13105,7 +12987,7 @@
}
},
"node_modules/npm/node_modules/ignore-walk": {
- "version": "6.0.4",
+ "version": "6.0.5",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -13132,7 +13014,7 @@
}
},
"node_modules/npm/node_modules/ini": {
- "version": "4.1.1",
+ "version": "4.1.3",
"inBundle": true,
"license": "ISC",
"engines": {
@@ -13140,14 +13022,14 @@
}
},
"node_modules/npm/node_modules/init-package-json": {
- "version": "6.0.0",
+ "version": "6.0.3",
"inBundle": true,
"license": "ISC",
"dependencies": {
+ "@npmcli/package-json": "^5.0.0",
"npm-package-arg": "^11.0.0",
"promzard": "^1.0.0",
- "read": "^2.0.0",
- "read-package-json": "^7.0.0",
+ "read": "^3.0.1",
"semver": "^7.3.5",
"validate-npm-package-license": "^3.0.4",
"validate-npm-package-name": "^5.0.0"
@@ -13168,11 +13050,6 @@
"node": ">= 12"
}
},
- "node_modules/npm/node_modules/ip-address/node_modules/sprintf-js": {
- "version": "1.1.3",
- "inBundle": true,
- "license": "BSD-3-Clause"
- },
"node_modules/npm/node_modules/ip-regex": {
"version": "5.0.0",
"inBundle": true,
@@ -13185,11 +13062,11 @@
}
},
"node_modules/npm/node_modules/is-cidr": {
- "version": "5.0.3",
+ "version": "5.1.0",
"inBundle": true,
"license": "BSD-2-Clause",
"dependencies": {
- "cidr-regex": "4.0.3"
+ "cidr-regex": "^4.1.1"
},
"engines": {
"node": ">=14"
@@ -13225,7 +13102,7 @@
"license": "ISC"
},
"node_modules/npm/node_modules/jackspeak": {
- "version": "2.3.6",
+ "version": "3.1.2",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -13247,7 +13124,7 @@
"license": "MIT"
},
"node_modules/npm/node_modules/json-parse-even-better-errors": {
- "version": "3.0.1",
+ "version": "3.0.2",
"inBundle": true,
"license": "MIT",
"engines": {
@@ -13281,49 +13158,47 @@
"license": "MIT"
},
"node_modules/npm/node_modules/libnpmaccess": {
- "version": "8.0.2",
+ "version": "8.0.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "npm-package-arg": "^11.0.1",
- "npm-registry-fetch": "^16.0.0"
+ "npm-package-arg": "^11.0.2",
+ "npm-registry-fetch": "^17.0.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmdiff": {
- "version": "6.0.7",
+ "version": "6.1.3",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^7.2.1",
- "@npmcli/disparity-colors": "^3.0.0",
- "@npmcli/installed-package-contents": "^2.0.2",
- "binary-extensions": "^2.2.0",
+ "@npmcli/arborist": "^7.5.3",
+ "@npmcli/installed-package-contents": "^2.1.0",
+ "binary-extensions": "^2.3.0",
"diff": "^5.1.0",
- "minimatch": "^9.0.0",
- "npm-package-arg": "^11.0.1",
- "pacote": "^17.0.4",
- "tar": "^6.2.0"
+ "minimatch": "^9.0.4",
+ "npm-package-arg": "^11.0.2",
+ "pacote": "^18.0.6",
+ "tar": "^6.2.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmexec": {
- "version": "7.0.8",
+ "version": "8.1.2",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^7.2.1",
- "@npmcli/run-script": "^7.0.2",
+ "@npmcli/arborist": "^7.5.3",
+ "@npmcli/run-script": "^8.1.0",
"ci-info": "^4.0.0",
- "npm-package-arg": "^11.0.1",
- "npmlog": "^7.0.1",
- "pacote": "^17.0.4",
- "proc-log": "^3.0.0",
- "read": "^2.0.0",
+ "npm-package-arg": "^11.0.2",
+ "pacote": "^18.0.6",
+ "proc-log": "^4.2.0",
+ "read": "^3.0.1",
"read-package-json-fast": "^3.0.2",
"semver": "^7.3.7",
"walk-up-path": "^3.0.1"
@@ -13333,104 +13208,104 @@
}
},
"node_modules/npm/node_modules/libnpmfund": {
- "version": "5.0.5",
+ "version": "5.0.11",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^7.2.1"
+ "@npmcli/arborist": "^7.5.3"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmhook": {
- "version": "10.0.1",
+ "version": "10.0.5",
"inBundle": true,
"license": "ISC",
"dependencies": {
"aproba": "^2.0.0",
- "npm-registry-fetch": "^16.0.0"
+ "npm-registry-fetch": "^17.0.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmorg": {
- "version": "6.0.2",
+ "version": "6.0.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
"aproba": "^2.0.0",
- "npm-registry-fetch": "^16.0.0"
+ "npm-registry-fetch": "^17.0.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmpack": {
- "version": "6.0.7",
+ "version": "7.0.3",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^7.2.1",
- "@npmcli/run-script": "^7.0.2",
- "npm-package-arg": "^11.0.1",
- "pacote": "^17.0.4"
+ "@npmcli/arborist": "^7.5.3",
+ "@npmcli/run-script": "^8.1.0",
+ "npm-package-arg": "^11.0.2",
+ "pacote": "^18.0.6"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmpublish": {
- "version": "9.0.4",
+ "version": "9.0.9",
"inBundle": true,
"license": "ISC",
"dependencies": {
"ci-info": "^4.0.0",
- "normalize-package-data": "^6.0.0",
- "npm-package-arg": "^11.0.1",
- "npm-registry-fetch": "^16.0.0",
- "proc-log": "^3.0.0",
+ "normalize-package-data": "^6.0.1",
+ "npm-package-arg": "^11.0.2",
+ "npm-registry-fetch": "^17.0.1",
+ "proc-log": "^4.2.0",
"semver": "^7.3.7",
"sigstore": "^2.2.0",
- "ssri": "^10.0.5"
+ "ssri": "^10.0.6"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmsearch": {
- "version": "7.0.1",
+ "version": "7.0.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "npm-registry-fetch": "^16.0.0"
+ "npm-registry-fetch": "^17.0.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmteam": {
- "version": "6.0.1",
+ "version": "6.0.5",
"inBundle": true,
"license": "ISC",
"dependencies": {
"aproba": "^2.0.0",
- "npm-registry-fetch": "^16.0.0"
+ "npm-registry-fetch": "^17.0.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/libnpmversion": {
- "version": "5.0.2",
+ "version": "6.0.3",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/git": "^5.0.3",
- "@npmcli/run-script": "^7.0.2",
- "json-parse-even-better-errors": "^3.0.0",
- "proc-log": "^3.0.0",
+ "@npmcli/git": "^5.0.7",
+ "@npmcli/run-script": "^8.1.0",
+ "json-parse-even-better-errors": "^3.0.2",
+ "proc-log": "^4.2.0",
"semver": "^7.3.7"
},
"engines": {
@@ -13438,7 +13313,7 @@
}
},
"node_modules/npm/node_modules/lru-cache": {
- "version": "10.2.0",
+ "version": "10.2.2",
"inBundle": true,
"license": "ISC",
"engines": {
@@ -13446,7 +13321,7 @@
}
},
"node_modules/npm/node_modules/make-fetch-happen": {
- "version": "13.0.0",
+ "version": "13.0.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -13459,6 +13334,7 @@
"minipass-flush": "^1.0.5",
"minipass-pipeline": "^1.2.4",
"negotiator": "^0.6.3",
+ "proc-log": "^4.2.0",
"promise-retry": "^2.0.1",
"ssri": "^10.0.0"
},
@@ -13467,7 +13343,7 @@
}
},
"node_modules/npm/node_modules/minimatch": {
- "version": "9.0.3",
+ "version": "9.0.4",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -13481,7 +13357,7 @@
}
},
"node_modules/npm/node_modules/minipass": {
- "version": "7.0.4",
+ "version": "7.1.2",
"inBundle": true,
"license": "ISC",
"engines": {
@@ -13500,7 +13376,7 @@
}
},
"node_modules/npm/node_modules/minipass-fetch": {
- "version": "3.0.4",
+ "version": "3.0.5",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -13657,7 +13533,7 @@
}
},
"node_modules/npm/node_modules/node-gyp": {
- "version": "10.0.1",
+ "version": "10.1.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -13679,8 +13555,16 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/npm/node_modules/node-gyp/node_modules/proc-log": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/npm/node_modules/nopt": {
- "version": "7.2.0",
+ "version": "7.2.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -13694,7 +13578,7 @@
}
},
"node_modules/npm/node_modules/normalize-package-data": {
- "version": "6.0.0",
+ "version": "6.0.1",
"inBundle": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -13716,7 +13600,7 @@
}
},
"node_modules/npm/node_modules/npm-bundled": {
- "version": "3.0.0",
+ "version": "3.0.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -13746,12 +13630,12 @@
}
},
"node_modules/npm/node_modules/npm-package-arg": {
- "version": "11.0.1",
+ "version": "11.0.2",
"inBundle": true,
"license": "ISC",
"dependencies": {
"hosted-git-info": "^7.0.0",
- "proc-log": "^3.0.0",
+ "proc-log": "^4.0.0",
"semver": "^7.3.5",
"validate-npm-package-name": "^5.0.0"
},
@@ -13771,7 +13655,7 @@
}
},
"node_modules/npm/node_modules/npm-pick-manifest": {
- "version": "9.0.0",
+ "version": "9.0.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -13785,56 +13669,43 @@
}
},
"node_modules/npm/node_modules/npm-profile": {
- "version": "9.0.0",
+ "version": "10.0.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "npm-registry-fetch": "^16.0.0",
- "proc-log": "^3.0.0"
+ "npm-registry-fetch": "^17.0.1",
+ "proc-log": "^4.0.0"
},
"engines": {
- "node": "^16.14.0 || >=18.0.0"
+ "node": ">=18.0.0"
}
},
"node_modules/npm/node_modules/npm-registry-fetch": {
- "version": "16.1.0",
+ "version": "17.0.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
+ "@npmcli/redact": "^2.0.0",
"make-fetch-happen": "^13.0.0",
"minipass": "^7.0.2",
"minipass-fetch": "^3.0.0",
"minipass-json-stream": "^1.0.1",
"minizlib": "^2.1.2",
"npm-package-arg": "^11.0.0",
- "proc-log": "^3.0.0"
+ "proc-log": "^4.0.0"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/npm/node_modules/npm-user-validate": {
- "version": "2.0.0",
+ "version": "2.0.1",
"inBundle": true,
"license": "BSD-2-Clause",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/npm/node_modules/npmlog": {
- "version": "7.0.1",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "are-we-there-yet": "^4.0.0",
- "console-control-strings": "^1.1.0",
- "gauge": "^5.0.0",
- "set-blocking": "^2.0.0"
- },
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/npm/node_modules/p-map": {
"version": "4.0.0",
"inBundle": true,
@@ -13850,31 +13721,30 @@
}
},
"node_modules/npm/node_modules/pacote": {
- "version": "17.0.6",
+ "version": "18.0.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@npmcli/git": "^5.0.0",
"@npmcli/installed-package-contents": "^2.0.1",
+ "@npmcli/package-json": "^5.1.0",
"@npmcli/promise-spawn": "^7.0.0",
- "@npmcli/run-script": "^7.0.0",
+ "@npmcli/run-script": "^8.0.0",
"cacache": "^18.0.0",
"fs-minipass": "^3.0.0",
"minipass": "^7.0.2",
"npm-package-arg": "^11.0.0",
"npm-packlist": "^8.0.0",
"npm-pick-manifest": "^9.0.0",
- "npm-registry-fetch": "^16.0.0",
- "proc-log": "^3.0.0",
+ "npm-registry-fetch": "^17.0.0",
+ "proc-log": "^4.0.0",
"promise-retry": "^2.0.1",
- "read-package-json": "^7.0.0",
- "read-package-json-fast": "^3.0.0",
"sigstore": "^2.2.0",
"ssri": "^10.0.0",
"tar": "^6.1.11"
},
"bin": {
- "pacote": "lib/bin.js"
+ "pacote": "bin/index.js"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
@@ -13902,22 +13772,22 @@
}
},
"node_modules/npm/node_modules/path-scurry": {
- "version": "1.10.1",
+ "version": "1.11.1",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
- "lru-cache": "^9.1.1 || ^10.0.0",
+ "lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/npm/node_modules/postcss-selector-parser": {
- "version": "6.0.15",
+ "version": "6.1.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -13929,7 +13799,15 @@
}
},
"node_modules/npm/node_modules/proc-log": {
- "version": "3.0.0",
+ "version": "4.2.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/proggy": {
+ "version": "2.0.0",
"inBundle": true,
"license": "ISC",
"engines": {
@@ -13970,11 +13848,11 @@
}
},
"node_modules/npm/node_modules/promzard": {
- "version": "1.0.0",
+ "version": "1.0.2",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "read": "^2.0.0"
+ "read": "^3.0.1"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
@@ -13988,11 +13866,11 @@
}
},
"node_modules/npm/node_modules/read": {
- "version": "2.1.0",
+ "version": "3.0.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "mute-stream": "~1.0.0"
+ "mute-stream": "^1.0.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
@@ -14006,20 +13884,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/npm/node_modules/read-package-json": {
- "version": "7.0.0",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "glob": "^10.2.2",
- "json-parse-even-better-errors": "^3.0.0",
- "normalize-package-data": "^6.0.0",
- "npm-normalize-package-bin": "^3.0.0"
- },
- "engines": {
- "node": "^16.14.0 || >=18.0.0"
- }
- },
"node_modules/npm/node_modules/read-package-json-fast": {
"version": "3.0.2",
"inBundle": true,
@@ -14047,12 +13911,9 @@
"optional": true
},
"node_modules/npm/node_modules/semver": {
- "version": "7.6.0",
+ "version": "7.6.2",
"inBundle": true,
"license": "ISC",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
"bin": {
"semver": "bin/semver.js"
},
@@ -14060,22 +13921,6 @@
"node": ">=10"
}
},
- "node_modules/npm/node_modules/semver/node_modules/lru-cache": {
- "version": "6.0.0",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/npm/node_modules/set-blocking": {
- "version": "2.0.0",
- "inBundle": true,
- "license": "ISC"
- },
"node_modules/npm/node_modules/shebang-command": {
"version": "2.0.0",
"inBundle": true,
@@ -14107,16 +13952,16 @@
}
},
"node_modules/npm/node_modules/sigstore": {
- "version": "2.2.2",
+ "version": "2.3.1",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
- "@sigstore/bundle": "^2.2.0",
+ "@sigstore/bundle": "^2.3.2",
"@sigstore/core": "^1.0.0",
- "@sigstore/protobuf-specs": "^0.3.0",
- "@sigstore/sign": "^2.2.3",
- "@sigstore/tuf": "^2.3.1",
- "@sigstore/verify": "^1.1.0"
+ "@sigstore/protobuf-specs": "^0.3.2",
+ "@sigstore/sign": "^2.3.2",
+ "@sigstore/tuf": "^2.3.4",
+ "@sigstore/verify": "^1.2.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
@@ -14132,7 +13977,7 @@
}
},
"node_modules/npm/node_modules/socks": {
- "version": "2.8.0",
+ "version": "2.8.3",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -14140,16 +13985,16 @@
"smart-buffer": "^4.2.0"
},
"engines": {
- "node": ">= 16.0.0",
+ "node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/npm/node_modules/socks-proxy-agent": {
- "version": "8.0.2",
+ "version": "8.0.3",
"inBundle": true,
"license": "MIT",
"dependencies": {
- "agent-base": "^7.0.2",
+ "agent-base": "^7.1.1",
"debug": "^4.3.4",
"socks": "^2.7.1"
},
@@ -14166,12 +14011,7 @@
"spdx-license-ids": "^3.0.0"
}
},
- "node_modules/npm/node_modules/spdx-exceptions": {
- "version": "2.5.0",
- "inBundle": true,
- "license": "CC-BY-3.0"
- },
- "node_modules/npm/node_modules/spdx-expression-parse": {
+ "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": {
"version": "3.0.1",
"inBundle": true,
"license": "MIT",
@@ -14180,13 +14020,32 @@
"spdx-license-ids": "^3.0.0"
}
},
+ "node_modules/npm/node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "inBundle": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/npm/node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
"node_modules/npm/node_modules/spdx-license-ids": {
- "version": "3.0.17",
+ "version": "3.0.18",
"inBundle": true,
"license": "CC0-1.0"
},
+ "node_modules/npm/node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "inBundle": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/npm/node_modules/ssri": {
- "version": "10.0.5",
+ "version": "10.0.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -14258,7 +14117,7 @@
}
},
"node_modules/npm/node_modules/tar": {
- "version": "6.2.0",
+ "version": "6.2.1",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -14322,13 +14181,13 @@
}
},
"node_modules/npm/node_modules/tuf-js": {
- "version": "2.2.0",
+ "version": "2.2.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
- "@tufjs/models": "2.0.0",
+ "@tufjs/models": "2.0.1",
"debug": "^4.3.4",
- "make-fetch-happen": "^13.0.0"
+ "make-fetch-happen": "^13.0.1"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
@@ -14370,13 +14229,19 @@
"spdx-expression-parse": "^3.0.0"
}
},
+ "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
"node_modules/npm/node_modules/validate-npm-package-name": {
- "version": "5.0.0",
+ "version": "5.0.1",
"inBundle": true,
"license": "ISC",
- "dependencies": {
- "builtins": "^5.0.0"
- },
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
@@ -14386,14 +14251,6 @@
"inBundle": true,
"license": "ISC"
},
- "node_modules/npm/node_modules/wcwidth": {
- "version": "1.0.1",
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "defaults": "^1.0.3"
- }
- },
"node_modules/npm/node_modules/which": {
"version": "4.0.0",
"inBundle": true,
@@ -14416,14 +14273,6 @@
"node": ">=16"
}
},
- "node_modules/npm/node_modules/wide-align": {
- "version": "1.1.5",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^1.0.2 || 2 || 3 || 4"
- }
- },
"node_modules/npm/node_modules/wrap-ansi": {
"version": "8.1.0",
"inBundle": true,
@@ -15357,9 +15206,9 @@
}
},
"node_modules/react-i18next": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz",
- "integrity": "sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==",
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz",
+ "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
@@ -16881,9 +16730,9 @@
}
},
"node_modules/typescript": {
- "version": "5.4.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
- "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
+ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
diff --git a/frontend/package.json b/frontend/package.json
index 4a6c85f3..52b7b9e8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "frontend",
- "version": "0.6.2",
+ "version": "0.6.3",
"description": "",
"main": "index.js",
"scripts": {
@@ -43,7 +43,7 @@
"jest": "^29.6.1",
"prettier": "^3.3.2",
"ts-node": "^10.9.2",
- "typescript": "^5.4.2",
+ "typescript": "^5.5.4",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
},
@@ -62,23 +62,23 @@
"@nivo/core": "^0.86.0",
"@nivo/line": "^0.86.0",
"base-ex": "^0.8.1",
- "country-flag-icons": "^1.5.11",
+ "country-flag-icons": "^1.5.13",
"date-fns": "^2.30.0",
"file-replace-loader": "^1.4.0",
"i18next": "^23.2.11",
- "i18next-browser-languagedetector": "^7.2.1",
+ "i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.0",
"install": "^0.13.0",
"js-sha256": "^0.11.0",
"leaflet": "^1.9.4",
"light-bolt11-decoder": "^3.1.1",
- "npm": "^10.5.0",
+ "npm": "^10.8.1",
"openpgp": "^5.11.0",
"react": "^18.2.0",
"react-countdown": "^2.3.5",
"react-dom": "^18.2.0",
"react-grid-layout": "^1.4.4",
- "react-i18next": "^14.1.0",
+ "react-i18next": "^14.1.2",
"react-image": "^4.1.0",
"react-leaflet": "^4.2.1",
"react-qr-code": "^2.0.11",
diff --git a/frontend/src/basic/OrderPage/index.tsx b/frontend/src/basic/OrderPage/index.tsx
index b47a46e5..aa2f3e74 100644
--- a/frontend/src/basic/OrderPage/index.tsx
+++ b/frontend/src/basic/OrderPage/index.tsx
@@ -39,24 +39,26 @@ const OrderPage = (): JSX.Element => {
useEffect(() => {
const shortAlias = params.shortAlias;
const coordinator = federation.getCoordinator(shortAlias ?? '');
- const { url, basePath } = coordinator.getEndpoint(
- settings.network,
- origin,
- settings.selfhostedClient,
- hostUrl,
- );
+ if (coordinator) {
+ const endpoint = coordinator?.getEndpoint(
+ settings.network,
+ origin,
+ settings.selfhostedClient,
+ hostUrl,
+ );
- setBaseUrl(`${url}${basePath}`);
+ if (endpoint) setBaseUrl(`${endpoint?.url}${endpoint?.basePath}`);
- const orderId = Number(params.orderId);
- if (
- orderId &&
- currentOrderId.id !== orderId &&
- currentOrderId.shortAlias !== shortAlias &&
- shortAlias
- )
- setCurrentOrderId({ id: orderId, shortAlias });
- if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
+ const orderId = Number(params.orderId);
+ if (
+ orderId &&
+ currentOrderId.id !== orderId &&
+ currentOrderId.shortAlias !== shortAlias &&
+ shortAlias
+ )
+ setCurrentOrderId({ id: orderId, shortAlias });
+ if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
+ }
}, [params, currentOrderId]);
const onClickCoordinator = function (): void {
@@ -98,7 +100,7 @@ const OrderPage = (): JSX.Element => {
setOpen(closeAll);
setAcknowledgedWarning(true);
}}
- longAlias={federation.getCoordinator(params.shortAlias ?? '').longAlias}
+ longAlias={federation.getCoordinator(params.shortAlias ?? '')?.longAlias}
/>
{currentOrder === null && badOrder === undefined && }
{badOrder !== undefined ? (
diff --git a/frontend/src/basic/SettingsPage/index.tsx b/frontend/src/basic/SettingsPage/index.tsx
index b618a2ed..a47360e5 100644
--- a/frontend/src/basic/SettingsPage/index.tsx
+++ b/frontend/src/basic/SettingsPage/index.tsx
@@ -1,12 +1,38 @@
-import React, { useContext } from 'react';
-import { Grid, Paper } from '@mui/material';
+import React, { useContext, useState } from 'react';
+import { Button, Grid, List, ListItem, Paper, TextField, Typography } from '@mui/material';
import SettingsForm from '../../components/SettingsForm';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import FederationTable from '../../components/FederationTable';
+import { t } from 'i18next';
+import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext(AppContext);
+ const { federation, addNewCoordinator } = useContext(FederationContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
+ const [newAlias, setNewAlias] = useState('');
+ const [newUrl, setNewUrl] = useState('');
+ const [error, setError] = useState();
+ // Regular expression to match a valid .onion URL
+ const onionUrlPattern = /^((http|https):\/\/)?[a-zA-Z2-7]{16,56}\.onion$/;
+
+ const addCoordinator: () => void = () => {
+ if (federation.coordinators[newAlias]) {
+ setError(t('Alias already exists'));
+ } else {
+ if (onionUrlPattern.test(newUrl)) {
+ let fullNewUrl = newUrl;
+ if (!/^((http|https):\/\/)/.test(fullNewUrl)) {
+ fullNewUrl = `http://${newUrl}`;
+ }
+ addNewCoordinator(newAlias, fullNewUrl);
+ setNewAlias('');
+ setNewUrl('');
+ } else {
+ setError(t('Invalid Onion URL'));
+ }
+ }
+ };
return (
{
+
+
+ {error}
+
+
+
+
+ {
+ setNewAlias(e.target.value);
+ }}
+ />
+ {
+ setNewUrl(e.target.value);
+ }}
+ />
+
+
+
);
diff --git a/frontend/src/components/BookTable/BookControl.tsx b/frontend/src/components/BookTable/BookControl.tsx
index ad4518d3..44ba81a5 100644
--- a/frontend/src/components/BookTable/BookControl.tsx
+++ b/frontend/src/components/BookTable/BookControl.tsx
@@ -359,7 +359,8 @@ const BookControl = ({
>
{
+ const coordinator = federation.coordinators[params.row.coordinatorShortAlias];
return (
JSX.Element;
- NoResultsOverlay?: (props: any) => JSX.Element;
- NoRowsOverlay?: (props: any) => JSX.Element;
- Footer?: (props: any) => JSX.Element;
- Toolbar?: (props: any) => JSX.Element;
- }
-
const NoResultsOverlay = function (): JSX.Element {
return (
)}
- {pgp !== undefined && (
+ {pgp && fingerprint && (
{
);
};
-const CoordinatorDialog = ({ open = false, onClose, network, shortAlias }: Props): JSX.Element => {
+const CoordinatorDialog = ({ open = false, onClose, shortAlias }: Props): JSX.Element => {
const { t } = useTranslation();
const { clientVersion, page, settings, origin } = useContext(AppContext);
const { federation } = useContext(FederationContext);
@@ -368,7 +368,8 @@ const CoordinatorDialog = ({ open = false, onClose, network, shortAlias }: Props
diff --git a/frontend/src/components/Dialogs/Exchange.tsx b/frontend/src/components/Dialogs/Exchange.tsx
index 04e1ef8c..f7be51d3 100644
--- a/frontend/src/components/Dialogs/Exchange.tsx
+++ b/frontend/src/components/Dialogs/Exchange.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useMemo, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
diff --git a/frontend/src/components/FederationTable/index.tsx b/frontend/src/components/FederationTable/index.tsx
index 34b15228..9e64483b 100644
--- a/frontend/src/components/FederationTable/index.tsx
+++ b/frontend/src/components/FederationTable/index.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState, useContext } from 'react';
+import React, { useCallback, useEffect, useState, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, useTheme, Checkbox, CircularProgress, Typography, Grid } from '@mui/material';
import { DataGrid, type GridColDef, type GridValidRowModel } from '@mui/x-data-grid';
@@ -21,9 +21,9 @@ const FederationTable = ({
fillContainer = false,
}: FederationTableProps): JSX.Element => {
const { t } = useTranslation();
- const { federation, sortedCoordinators, coordinatorUpdatedAt } =
+ const { federation, sortedCoordinators, coordinatorUpdatedAt, federationUpdatedAt } =
useContext(FederationContext);
- const { setOpen } = useContext(AppContext);
+ const { setOpen, settings } = useContext(AppContext);
const theme = useTheme();
const [pageSize, setPageSize] = useState(0);
@@ -43,7 +43,7 @@ const FederationTable = ({
if (useDefaultPageSize) {
setPageSize(defaultPageSize);
}
- }, [coordinatorUpdatedAt]);
+ }, [coordinatorUpdatedAt, federationUpdatedAt]);
const localeText = {
MuiTablePagination: { labelRowsPerPage: t('Coordinators per page:') },
@@ -62,6 +62,7 @@ const FederationTable = ({
headerName: t('Coordinator'),
width: width * fontSize,
renderCell: (params: any) => {
+ const coordinator = federation.coordinators[params.row.shortAlias];
return (
{
- coordinators[key] = federation.coordinators[key];
- return coordinators;
- }, {});
+ const reorderedCoordinators = useMemo(() => {
+ return sortedCoordinators.reduce((coordinators, key) => {
+ coordinators[key] = federation.coordinators[key];
+
+ return coordinators;
+ }, {});
+ }, [settings.network, federationUpdatedAt]);
return (
;
if (code === 'BDT') flag = ;
if (code === 'ALL') flag = ;
+ if (code === 'DZD') flag = ;
if (code === 'ANY') flag = ;
if (code === 'XAU') flag = ;
if (code === 'BTC') flag = ;
diff --git a/frontend/src/components/MakerForm/AutocompletePayments.tsx b/frontend/src/components/MakerForm/AutocompletePayments.tsx
index 642151e3..78337d39 100644
--- a/frontend/src/components/MakerForm/AutocompletePayments.tsx
+++ b/frontend/src/components/MakerForm/AutocompletePayments.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useAutocomplete } from '@mui/base/useAutocomplete';
+import useAutocomplete from '@mui/base/useAutocomplete';
import { styled } from '@mui/material/styles';
import {
Button,
@@ -409,9 +409,8 @@ const AutocompletePayments: React.FC = (props) => {
))}
{qttHiddenTags > 0 ? (
) : null}
>
diff --git a/frontend/src/components/MakerForm/SelectCoordinator.tsx b/frontend/src/components/MakerForm/SelectCoordinator.tsx
index 36a37ccc..9a117a6c 100644
--- a/frontend/src/components/MakerForm/SelectCoordinator.tsx
+++ b/frontend/src/components/MakerForm/SelectCoordinator.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useMemo } from 'react';
+import React, { useContext } from 'react';
import {
Grid,
Select,
@@ -26,8 +26,7 @@ const SelectCoordinator: React.FC = ({
setCoordinator,
}) => {
const { setOpen } = useContext(AppContext);
- const { federation, sortedCoordinators, coordinatorUpdatedAt } =
- useContext(FederationContext);
+ const { federation, sortedCoordinators } = useContext(FederationContext);
const theme = useTheme();
const { t } = useTranslation();
@@ -41,10 +40,7 @@ const SelectCoordinator: React.FC = ({
setCoordinator(e.target.value);
};
- const coordinator = useMemo(
- () => federation.getCoordinator(coordinatorAlias),
- [coordinatorUpdatedAt],
- );
+ const coordinator = federation.getCoordinator(coordinatorAlias);
return (
@@ -83,7 +79,8 @@ const SelectCoordinator: React.FC = ({
>
-
+
diff --git a/frontend/src/components/RobotAvatar/index.tsx b/frontend/src/components/RobotAvatar/index.tsx
index aacf98ae..9f74db0a 100644
--- a/frontend/src/components/RobotAvatar/index.tsx
+++ b/frontend/src/components/RobotAvatar/index.tsx
@@ -70,8 +70,8 @@ const RobotAvatar: React.FC = ({
}, [hashId]);
useEffect(() => {
- if (shortAlias !== undefined) {
- if (window.NativeRobosats === undefined) {
+ if (shortAlias && shortAlias !== '') {
+ if (!window.NativeRobosats) {
setAvatarSrc(
`${hostUrl}/static/federation/avatars/${shortAlias}${small ? '.small' : ''}.webp`,
);
diff --git a/frontend/src/components/SettingsForm/index.tsx b/frontend/src/components/SettingsForm/index.tsx
index 22eaef92..76f994b0 100644
--- a/frontend/src/components/SettingsForm/index.tsx
+++ b/frontend/src/components/SettingsForm/index.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect } from 'react';
+import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
import {
@@ -30,8 +30,7 @@ import {
import { systemClient } from '../../services/System';
import { TorIcon } from '../Icons';
import SwapCalls from '@mui/icons-material/SwapCalls';
-import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
-import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
+import { apiClient } from '../../services/api';
interface SettingsFormProps {
dense?: boolean;
@@ -39,8 +38,6 @@ interface SettingsFormProps {
const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
const { fav, setFav, settings, setSettings } = useContext(AppContext);
- const { federation } = useContext(FederationContext);
- const { garage } = useContext(GarageContext);
const theme = useTheme();
const { t } = useTranslation();
const fontSizes = [
@@ -251,6 +248,7 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
onChange={(_e, useProxy) => {
setSettings({ ...settings, useProxy });
systemClient.setItem('settings_use_proxy', String(useProxy));
+ apiClient.useProxy = useProxy;
}}
>
diff --git a/frontend/src/components/TradeBox/index.tsx b/frontend/src/components/TradeBox/index.tsx
index 28d79061..bdfa70c8 100644
--- a/frontend/src/components/TradeBox/index.tsx
+++ b/frontend/src/components/TradeBox/index.tsx
@@ -639,7 +639,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
}
break;
- // 14: 'Sucessful trade'
+ // 14: 'Successful trade'
case 14:
baseContract.title = 'Trade finished!';
baseContract.titleColor = 'success';
diff --git a/frontend/src/contexts/FederationContext.tsx b/frontend/src/contexts/FederationContext.tsx
index b7998111..b9ce609b 100644
--- a/frontend/src/contexts/FederationContext.tsx
+++ b/frontend/src/contexts/FederationContext.tsx
@@ -4,7 +4,6 @@ import React, {
useEffect,
useState,
type SetStateAction,
- useMemo,
useContext,
type ReactNode,
} from 'react';
@@ -15,7 +14,7 @@ import { federationLottery } from '../utils';
import { AppContext, type UseAppStoreType } from './AppContext';
import { GarageContext, type UseGarageStoreType } from './GarageContext';
-import NativeRobosats from '../services/Native';
+import { type Origin, type Origins } from '../models/Coordinator.model';
// Refresh delays (ms) according to Order status
const defaultDelay = 5000;
@@ -34,7 +33,7 @@ const statusToDelay = [
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
- 60000, // 'Sucessful trade'
+ 60000, // 'Successful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
@@ -59,6 +58,7 @@ export interface UseFederationStoreType {
currentOrder: Order | null;
coordinatorUpdatedAt: string;
federationUpdatedAt: string;
+ addNewCoordinator: (alias: string, url: string) => void;
}
export const initialFederationContext: UseFederationStoreType = {
@@ -70,6 +70,7 @@ export const initialFederationContext: UseFederationStoreType = {
currentOrder: null,
coordinatorUpdatedAt: '',
federationUpdatedAt: '',
+ addNewCoordinator: () => {},
};
export const FederationContext = createContext(initialFederationContext);
@@ -81,7 +82,7 @@ export const FederationContextProvider = ({
useContext(AppContext);
const { setMaker, garage, setBadOrder } = useContext(GarageContext);
const [federation] = useState(new Federation(origin, settings, hostUrl));
- const sortedCoordinators = useMemo(() => federationLottery(federation), []);
+ const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState(
new Date().toISOString(),
);
@@ -164,6 +165,35 @@ export const FederationContextProvider = ({
}
};
+ const addNewCoordinator: (alias: string, url: string) => void = (alias, url) => {
+ if (!federation.coordinators[alias]) {
+ const attributes: Record = {
+ longAlias: alias,
+ shortAlias: alias,
+ federated: false,
+ enabled: true,
+ };
+ const origins: Origins = {
+ clearnet: undefined,
+ onion: url as Origin,
+ i2p: undefined,
+ };
+ if (settings.network === 'mainnet') {
+ attributes.mainnet = origins;
+ } else {
+ attributes.testnet = origins;
+ }
+ federation.addCoordinator(origin, settings, hostUrl, attributes);
+ const newCoordinator = federation.coordinators[alias];
+ newCoordinator.update(() => {
+ setCoordinatorUpdatedAt(new Date().toISOString());
+ });
+ garage.syncCoordinator(newCoordinator);
+ setSortedCoordinators(federationLottery(federation));
+ setFederationUpdatedAt(new Date().toISOString());
+ }
+ };
+
useEffect(() => {
if (currentOrderId.id && currentOrderId.shortAlias) {
setCurrentOrder(null);
@@ -200,6 +230,7 @@ export const FederationContextProvider = ({
setDelay,
coordinatorUpdatedAt,
federationUpdatedAt,
+ addNewCoordinator,
}}
>
{children}
diff --git a/frontend/src/models/Coordinator.model.ts b/frontend/src/models/Coordinator.model.ts
index c07e771b..09b74fa1 100644
--- a/frontend/src/models/Coordinator.model.ts
+++ b/frontend/src/models/Coordinator.model.ts
@@ -72,6 +72,39 @@ export interface Info {
export type Origin = 'onion' | 'i2p' | 'clearnet';
+export const coordinatorDefaultValues = {
+ longAlias: '',
+ shortAlias: '',
+ description: '',
+ motto: '',
+ color: '#000',
+ size_limit: 21 * 100000000,
+ established: new Date(),
+ policies: {},
+ contact: {
+ email: '',
+ telegram: '',
+ simplex: '',
+ matrix: '',
+ website: '',
+ nostr: '',
+ pgp: '',
+ fingerprint: '',
+ },
+ badges: {
+ isFounder: false,
+ donatesToDevFund: 0,
+ hasGoodOpSec: false,
+ robotsLove: false,
+ hasLargeLimits: false,
+ },
+ mainnet: undefined,
+ testnet: undefined,
+ mainnetNodesPubkeys: '',
+ testnetNodesPubkeys: '',
+ federated: true,
+};
+
export interface Origins {
clearnet: Origin | undefined;
onion: Origin | undefined;
@@ -102,6 +135,7 @@ export class Coordinator {
this.longAlias = value.longAlias;
this.shortAlias = value.shortAlias;
this.description = value.description;
+ this.federated = value.federated;
this.motto = value.motto;
this.color = value.color;
this.size_limit = value.badges.isFounder ? 21 * 100000000 : calculateSizeLimit(established);
@@ -122,6 +156,7 @@ export class Coordinator {
// These properties are loaded from federation.json
public longAlias: string;
public shortAlias: string;
+ public federated: boolean;
public enabled?: boolean = true;
public description: string;
public motto: string;
@@ -175,7 +210,7 @@ export class Coordinator {
generateAllMakerAvatars = async (data: [PublicOrder]): Promise => {
for (const order of data) {
- roboidentitiesClient.generateRobohash(order.maker_hash_id, 'small');
+ void roboidentitiesClient.generateRobohash(order.maker_hash_id, 'small');
}
};
diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts
index f5d507cb..d39b25ee 100644
--- a/frontend/src/models/Federation.model.ts
+++ b/frontend/src/models/Federation.model.ts
@@ -9,6 +9,7 @@ import {
} from '.';
import defaultFederation from '../../static/federation.json';
import { getHost } from '../utils';
+import { coordinatorDefaultValues } from './Coordinator.model';
import { updateExchangeInfo } from './Exchange.model';
type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate';
@@ -38,11 +39,19 @@ export class Federation {
onFederationUpdate: [],
};
- this.loading = true;
+ Object.keys(defaultFederation).forEach((key) => {
+ if (key !== 'local' || getHost() === '127.0.0.1:8000') {
+ // Do not add `Local Dev` unless it is running on localhost
+ this.addCoordinator(origin, settings, hostUrl, defaultFederation[key]);
+ }
+ });
+
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
+ this.loading = true;
const host = getHost();
const url = `${window.location.protocol}//${host}`;
+
const tesnetHost = Object.values(this.coordinators).find((coor) => {
return Object.values(coor.testnet).includes(url);
});
@@ -56,6 +65,22 @@ export class Federation {
public hooks: Record void>>;
+ addCoordinator = (
+ origin: Origin,
+ settings: Settings,
+ hostUrl: string,
+ attributes: Record,
+ ): void => {
+ const value = {
+ ...coordinatorDefaultValues,
+ ...attributes,
+ };
+ this.coordinators[value.shortAlias] = new Coordinator(value, origin, settings, hostUrl);
+ this.exchange.totalCoordinators = Object.keys(this.coordinators).length;
+ this.updateEnabledCoordinators();
+ this.triggerHook('onFederationUpdate');
+ };
+
// Hooks
registerHook = (hookName: FederationHooks, fn: () => void): void => {
this.hooks[hookName].push(fn);
@@ -101,7 +126,7 @@ export class Federation {
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
this.updateEnabledCoordinators();
for (const coor of Object.values(this.coordinators)) {
- coor.update(() => {
+ void coor.update(() => {
this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1;
this.onCoordinatorSaved();
});
@@ -114,7 +139,7 @@ export class Federation {
this.triggerHook('onCoordinatorUpdate');
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
for (const coor of Object.values(this.coordinators)) {
- coor.updateBook(() => {
+ void coor.updateBook(() => {
this.onCoordinatorSaved();
});
}
diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts
index 2fa2b017..b1f35f6c 100644
--- a/frontend/src/models/Garage.model.ts
+++ b/frontend/src/models/Garage.model.ts
@@ -1,4 +1,4 @@
-import { type Order } from '.';
+import { type Coordinator, type Order } from '.';
import { systemClient } from '../services/System';
import { saveAsJson } from '../utils';
import Slot from './Slot.model';
@@ -59,15 +59,18 @@ class Garage {
const rawSlots = JSON.parse(slotsDump);
Object.values(rawSlots).forEach((rawSlot: Record) => {
if (rawSlot?.token) {
- this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {}, () =>
- { this.triggerHook('onRobotUpdate'); },
+ this.slots[rawSlot.token] = new Slot(
+ rawSlot.token,
+ Object.keys(rawSlot.robots),
+ {},
+ () => {
+ this.triggerHook('onRobotUpdate');
+ },
);
-
Object.keys(rawSlot.robots).forEach((shortAlias) => {
const rawRobot = rawSlot.robots[shortAlias];
this.updateRobot(rawSlot.token, shortAlias, rawRobot);
});
-
this.currentSlot = rawSlot?.token;
}
});
@@ -80,7 +83,7 @@ class Garage {
// Slots
getSlot: (token?: string) => Slot | null = (token) => {
const currentToken = token ?? this.currentSlot;
- return currentToken ? this.slots[currentToken] ?? null : null;
+ return currentToken ? (this.slots[currentToken] ?? null) : null;
};
deleteSlot: (token?: string) => void = (token) => {
@@ -120,9 +123,9 @@ class Garage {
if (!token || !shortAliases) return;
if (this.getSlot(token) === null) {
- this.slots[token] = new Slot(token, shortAliases, attributes, () =>
- { this.triggerHook('onRobotUpdate'); },
- );
+ this.slots[token] = new Slot(token, shortAliases, attributes, () => {
+ this.triggerHook('onRobotUpdate');
+ });
this.save();
}
};
@@ -165,6 +168,13 @@ class Garage {
this.triggerHook('onOrderUpdate');
}
};
+
+ // Coordinators
+ syncCoordinator: (coordinator: Coordinator) => void = (coordinator) => {
+ Object.values(this.slots).forEach((slot) => {
+ slot.syncCoordinator(coordinator, this);
+ });
+ };
}
export default Garage;
diff --git a/frontend/src/models/Slot.model.ts b/frontend/src/models/Slot.model.ts
index 76909bf0..4d3fa904 100644
--- a/frontend/src/models/Slot.model.ts
+++ b/frontend/src/models/Slot.model.ts
@@ -1,5 +1,5 @@
import { sha256 } from 'js-sha256';
-import { Robot, type Order } from '.';
+import { type Coordinator, type Garage, Robot, type Order } from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
class Slot {
@@ -13,12 +13,12 @@ class Slot {
this.hashId = sha256(sha256(this.token));
this.nickname = null;
- roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
+ void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
this.nickname = nickname;
onRobotUpdate();
});
- roboidentitiesClient.generateRobohash(this.hashId, 'small');
- roboidentitiesClient.generateRobohash(this.hashId, 'large');
+ void roboidentitiesClient.generateRobohash(this.hashId, 'small');
+ void roboidentitiesClient.generateRobohash(this.hashId, 'large');
this.robots = shortAliases.reduce((acc: Record, shortAlias: string) => {
acc[shortAlias] = new Robot(robotAttributes);
@@ -73,6 +73,18 @@ class Slot {
return this.robots[shortAlias];
};
+
+ syncCoordinator: (coordinator: Coordinator, garage: Garage) => void = (coordinator, garage) => {
+ const defaultRobot = this.getRobot();
+ if (defaultRobot?.token) {
+ this.robots[coordinator.shortAlias] = new Robot({
+ token: defaultRobot.token,
+ pubKey: defaultRobot.pubKey,
+ encPrivKey: defaultRobot.encPrivKey,
+ });
+ void coordinator.fetchRobot(garage, defaultRobot.token);
+ }
+ };
}
export default Slot;
diff --git a/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts b/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts
index 85b55b8b..0b67b4c2 100644
--- a/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts
+++ b/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts
@@ -31,8 +31,8 @@ class RoboidentitiesNativeClient implements RoboidentitiesClient {
type: 'robohash',
detail: key,
});
- const result = response ? Object.values(response)[0] : '';
- const image = `data:image/png;base64,${result}`;
+ const result: string = response ? Object.values(response)[0] : '';
+ const image: string = `data:image/png;base64,${result}`;
this.robohashes[key] = image;
return image;
}
diff --git a/frontend/src/services/Roboidentities/RoboidentitiesWebClient/index.ts b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/index.ts
index 1bc976e8..f297343b 100644
--- a/frontend/src/services/Roboidentities/RoboidentitiesWebClient/index.ts
+++ b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/index.ts
@@ -4,7 +4,7 @@ import { robohash } from './RobohashGenerator';
class RoboidentitiesClientWebClient implements RoboidentitiesClient {
public generateRoboname: (initialString: string) => Promise = async (initialString) => {
- return await new Promise(async (resolve, _reject) => {
+ return await new Promise((resolve, _reject) => {
resolve(generate_roboname(initialString));
});
};
diff --git a/frontend/src/services/api/ApiNativeClient/index.ts b/frontend/src/services/api/ApiNativeClient/index.ts
index 93f8ba39..ea927cb8 100644
--- a/frontend/src/services/api/ApiNativeClient/index.ts
+++ b/frontend/src/services/api/ApiNativeClient/index.ts
@@ -5,7 +5,7 @@ import ApiWebClient from '../ApiWebClient';
class ApiNativeClient implements ApiClient {
public useProxy = true;
- private webClient: ApiClient = new ApiWebClient();
+ private readonly webClient: ApiClient = new ApiWebClient();
private readonly assetsPromises = new Map>();
@@ -55,7 +55,7 @@ class ApiNativeClient implements ApiClient {
public delete: (baseUrl: string, path: string, auth?: Auth) => Promise