This commit is contained in:
koalasat
2025-04-22 17:21:30 +02:00
parent 3da49fd812
commit 3cb5439781
8 changed files with 153 additions and 0 deletions

View File

@ -0,0 +1,42 @@
# Generated by Django 5.1.4 on 2025-04-22 14:40
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0051_takeorder'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='robot',
name='nostr_pubkey',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AlterField(
model_name='takeorder',
name='last_satoshis',
field=models.PositiveBigIntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000000)]),
),
migrations.AlterField(
model_name='takeorder',
name='order',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='api.order'),
),
migrations.AlterField(
model_name='takeorder',
name='taker',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='pretaker', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='takeorder',
name='taker_bond',
field=models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='take_order', to='api.lnpayment'),
),
]

View File

@ -42,6 +42,9 @@ class Robot(models.Model):
telegram_lang_code = models.CharField(max_length=10, null=True, blank=True)
telegram_welcomed = models.BooleanField(default=False, null=False)
# nostr
nostr_pubkey = models.CharField(max_length=64, null=True, blank=True)
# Claimable rewards
earned_rewards = models.PositiveIntegerField(null=False, default=0)
# Total claimed rewards

View File

@ -2,6 +2,7 @@ import pygeohash
import hashlib
import uuid
from secp256k1 import PrivateKey, PublicKey, ALL_FLAGS
from asgiref.sync import sync_to_async
from nostr_sdk import Keys, Client, EventBuilder, NostrSigner, Kind, Tag
from api.models import Order
@ -112,3 +113,24 @@ class Nostr:
return ["onchain", "lightning"]
else:
return ["lightning"]
def is_valid_public_key(public_key_hex):
try:
public_key_bytes = bytes.fromhex(public_key_hex)
PublicKey(public_key_bytes, raw=True)
return True
except Exception:
return False
def sign_message(text: str) -> str:
try:
private_key = config("NOSTR_NSEC", cast=str)
privkey = PrivateKey(
bytes.fromhex(private_key), raw=True, ctx_flags=ALL_FLAGS
)
hashed_message = hashlib.sha256(text.encode("utf-8")).digest()
signature = privkey.schnorr_sign(hashed_message)
return signature.hex()
except Exception:
return ""

View File

@ -9,6 +9,7 @@ from api.serializers import (
ListOrderSerializer,
OrderDetailSerializer,
StealthSerializer,
ReviewSerializer,
)
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
@ -786,3 +787,22 @@ class StealthViewSchema:
},
},
}
class ReviewViewSchema:
post = {
"summary": "Generates a review token",
"description": "Generates the token necesary for reviews of robot's latest order",
"responses": {
200: ReviewSerializer,
400: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
},
}

View File

@ -679,5 +679,14 @@ class TickSerializer(serializers.ModelSerializer):
depth = 0
class ReviewSerializer(serializers.Serializer):
pubkey = serializers.CharField(
help_text="Robot's nostr hex pubkey",
allow_null=False,
allow_blank=False,
required=True,
)
class StealthSerializer(serializers.Serializer):
wantsStealth = serializers.BooleanField()

View File

@ -15,6 +15,7 @@ from .views import (
RobotView,
StealthView,
TickView,
ReviewView,
NotificationsView,
)
@ -38,4 +39,5 @@ urlpatterns = [
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"),
path("review/", ReviewView.as_view(), name="review"),
]

View File

@ -37,6 +37,7 @@ from api.oas_schemas import (
RewardViewSchema,
RobotViewSchema,
StealthViewSchema,
ReviewViewSchema,
TickViewSchema,
NotificationSchema,
)
@ -49,6 +50,7 @@ from api.serializers import (
PriceSerializer,
StealthSerializer,
TickSerializer,
ReviewSerializer,
UpdateOrderSerializer,
ListNotificationSerializer,
)
@ -60,6 +62,7 @@ from api.utils import (
get_robosats_commit,
verify_signed_message,
)
from api.nostr import Nostr
from chat.models import Message
from control.models import AccountingDay, BalanceLog
@ -1033,3 +1036,54 @@ class StealthView(APIView):
request.user.robot.save(update_fields=["wants_stealth"])
return Response({"wantsStealth": stealth})
class ReviewView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = ReviewSerializer
@extend_schema(**ReviewViewSchema.post)
def post(self, request):
if config("NOSTR_NSEC", cast=str, default="") == "":
return Response(status=status.HTTP_400_BAD_REQUEST)
serializer = self.serializer_class(data=request.data)
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
pubkey = serializer.data.get("pubkey")
last_order = Order.objects.filter(
Q(maker=request.user) | Q(taker=request.user)
).last()
if not last_order or last_order.status not in [
Order.Status.SUC,
Order.Status.MLD,
Order.Status.TLD,
]:
return Response(
{"bad_request": "Robot has no finished order"},
status.HTTP_400_BAD_REQUEST,
)
if not request.user.robot.nostr_pubkey:
verified = Nostr.is_valid_public_key(pubkey)
if verified:
request.user.robot.nostr_pubkey = pubkey
request.user.robot.save(update_fields=["nostr_pubkey"])
else:
return Response(
{"bad_request": "Invalid hex pubkey"},
status.HTTP_400_BAD_REQUEST,
)
if request.user.robot.nostr_pubkey is not pubkey:
return Response(
{"bad_request": "Wrong hex pubkey"},
status.HTTP_400_BAD_REQUEST,
)
token = Nostr.sign_message(f"{pubkey}{last_order.id}")
return Response({"pubkey": pubkey, "token": token}, status.HTTP_200_OK)

View File

@ -31,3 +31,4 @@ base91==1.0.1
nostr-sdk==0.35.1
pygeohash==1.2.0
asgiref == 3.8.1
secp256k1