Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ RUN apt-get update -qq && \
libpq-dev \
curl \
build-essential \
gnupg2
gnupg2 \
pkg-config \
libsecp256k1-dev

RUN python -m pip install --upgrade pip

Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
),
]
3 changes: 3 additions & 0 deletions api/models/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion api/nostr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import hashlib
import uuid

from secp256k1 import PrivateKey
from asgiref.sync import sync_to_async
from nostr_sdk import Keys, Client, EventBuilder, NostrSigner, Kind, Tag
from nostr_sdk import Keys, Client, EventBuilder, NostrSigner, Kind, Tag, PublicKey
from api.models import Order
from decouple import config

Expand Down Expand Up @@ -112,3 +113,24 @@ def get_layer_tag(self, order):
return ["onchain", "lightning"]
else:
return ["lightning"]
return False

def is_valid_public_key(public_key_hex):
try:
PublicKey.from_hex(public_key_hex)
return True
except Exception:
return False

def sign_message(text: str) -> str:
try:
keys = Keys.parse(config("NOSTR_NSEC", cast=str))
secret_key_hex = keys.secret_key().to_hex()
private_key = PrivateKey(bytes.fromhex(secret_key_hex))
signature = private_key.schnorr_sign(
text.encode("utf-8"), bip340tag=None, raw=True
)

return signature.hex()
except Exception:
return ""
20 changes: 20 additions & 0 deletions api/oas_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ListOrderSerializer,
OrderDetailSerializer,
StealthSerializer,
ReviewSerializer,
)

EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
Expand Down Expand Up @@ -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",
},
},
},
},
}
9 changes: 9 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,5 +679,14 @@ class Meta:
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()
2 changes: 2 additions & 0 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
RobotView,
StealthView,
TickView,
ReviewView,
NotificationsView,
)

Expand All @@ -38,4 +39,5 @@
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"),
]
51 changes: 51 additions & 0 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
RewardViewSchema,
RobotViewSchema,
StealthViewSchema,
ReviewViewSchema,
TickViewSchema,
NotificationSchema,
)
Expand All @@ -49,6 +50,7 @@
PriceSerializer,
StealthSerializer,
TickSerializer,
ReviewSerializer,
UpdateOrderSerializer,
ListNotificationSerializer,
)
Expand All @@ -60,6 +62,7 @@
get_robosats_commit,
verify_signed_message,
)
from api.nostr import Nostr
from chat.models import Message
from control.models import AccountingDay, BalanceLog

Expand Down Expand Up @@ -1033,3 +1036,51 @@ def post(self, request):
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):
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 != 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)
77 changes: 76 additions & 1 deletion docs/assets/schemas/api-latest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,10 @@ paths:
- `17` - Maker lost dispute
- `18` - Taker lost dispute

The client can use `cancel_status` to cancel the order only
if it is in the specified status. The server will
return an error without cancelling the trade otherwise.

Note that there are penalties involved for cancelling a order
mid-trade so use this action carefully:

Expand Down Expand Up @@ -652,6 +656,44 @@ paths:
timestamp: '2022-09-13T14:32:40.591774Z'
summary: Truncated example. Real response contains all the currencies
description: ''
/api/review/:
post:
operationId: review_create
description: Generates the token necesary for reviews of robot's latest order
summary: Generates a review token
tags:
- review
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Review'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Review'
multipart/form-data:
schema:
$ref: '#/components/schemas/Review'
required: true
security:
- tokenAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Review'
description: ''
'400':
content:
application/json:
schema:
type: object
properties:
bad_request:
type: string
description: Reason for the failure
description: ''
/api/reward/:
post:
operationId: reward_create
Expand Down Expand Up @@ -1775,6 +1817,14 @@ components:
* `3` - 3
* `4` - 4
* `5` - 5
Review:
type: object
properties:
pubkey:
type: string
description: Robot's nostr hex pubkey
required:
- pubkey
StatusEnum:
enum:
- 0
Expand Down Expand Up @@ -1975,8 +2025,33 @@ components:
pattern: ^-?\d{0,3}(?:\.\d{0,3})?$
nullable: true
cancel_status:
allOf:
nullable: true
description: |-
Status the order should have for it to be cancelled.

* `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
oneOf:
- $ref: '#/components/schemas/StatusEnum'
- $ref: '#/components/schemas/BlankEnum'
- $ref: '#/components/schemas/NullEnum'
required:
- action
Version:
Expand Down
Loading