mirror of
https://github.com/RoboSats/robosats.git
synced 2025-07-23 06:33:18 +00:00
Endpoint
This commit is contained in:
@ -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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
22
api/nostr.py
22
api/nostr.py
@ -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 ""
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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"),
|
||||
]
|
||||
|
54
api/views.py
54
api/views.py
@ -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)
|
||||
|
@ -31,3 +31,4 @@ base91==1.0.1
|
||||
nostr-sdk==0.35.1
|
||||
pygeohash==1.2.0
|
||||
asgiref == 3.8.1
|
||||
secp256k1
|
Reference in New Issue
Block a user