This commit is contained in:
koalasat
2025-04-24 12:46:18 +02:00
parent 3cb5439781
commit 26904f7cc2
9 changed files with 155 additions and 16 deletions

View File

@ -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

View File

@ -2,9 +2,9 @@ import pygeohash
import hashlib
import uuid
from secp256k1 import PrivateKey, PublicKey, ALL_FLAGS
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
@ -113,23 +113,23 @@ class Nostr:
return ["onchain", "lightning"]
else:
return ["lightning"]
return False
def is_valid_public_key(public_key_hex):
try:
public_key_bytes = bytes.fromhex(public_key_hex)
PublicKey(public_key_bytes, raw=True)
PublicKey.from_hex(public_key_hex)
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
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
)
hashed_message = hashlib.sha256(text.encode("utf-8")).digest()
signature = privkey.schnorr_sign(hashed_message)
return signature.hex()
except Exception:

View File

@ -1046,9 +1046,6 @@ class ReviewView(APIView):
@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():

View File

@ -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:
@ -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
@ -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
@ -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:

View File

@ -0,0 +1 @@
aa19b79461dbd92673900cd50f51934ec648f8dbb79cb2e3e59929e798d41e86

View File

@ -0,0 +1 @@
b905e7adcee82cc7fe3ba05c8f0922c9098a872f71db4ba70c216e4396d73be8

View File

@ -0,0 +1 @@
86127e010ee323f30c7935e70393a9acef7f269b88cc3b1610e336becfbfc8f2

View File

@ -965,6 +965,46 @@ class TradeTest(BaseAPITestCase):
self.assert_order_logs(data["id"])
def test_review_order(self):
"""
Tests a trade review token generation after the trade ends
"""
trade = Trade(self.client)
trade.publish_order()
trade.get_review()
self.assertEqual(trade.response.status_code, 400)
trade.take_order()
trade.take_order_third()
trade.lock_taker_bond()
trade.get_review(trade.maker_index)
self.assertEqual(trade.response.status_code, 400)
trade.get_review(trade.taker_index)
self.assertEqual(trade.response.status_code, 400)
trade.lock_escrow(trade.taker_index)
trade.submit_payout_address(trade.maker_index)
trade.confirm_fiat(trade.maker_index)
trade.confirm_fiat(trade.taker_index)
trade.process_payouts(mine_a_block=True)
trade.get_review(trade.maker_index)
self.assertEqual(trade.response.status_code, 200)
nostr_pubkey = read_file(f"tests/robots/{trade.maker_index}/nostr_pubkey")
data = trade.response.json()
self.assertEqual(data["pubkey"], nostr_pubkey)
self.assertIsInstance(data["token"], str)
trade.get_review(trade.taker_index)
self.assertEqual(trade.response.status_code, 200)
nostr_pubkey = read_file(f"tests/robots/{trade.taker_index}/nostr_pubkey")
data = trade.response.json()
self.assertEqual(data["pubkey"], nostr_pubkey)
self.assertIsInstance(data["token"], str)
def test_cancel_public_order(self):
"""
Tests the cancellation of a public order
@ -993,6 +1033,9 @@ class TradeTest(BaseAPITestCase):
f"❌ Hey {maker_nick}, you have cancelled your public order with ID {trade.order_id}.",
)
trade.get_review()
self.assertEqual(trade.response.status_code, 400)
def test_cancel_public_order_by_taker(self):
"""
Tests the cancellation of a public order by a pretaker
@ -1121,7 +1164,8 @@ class TradeTest(BaseAPITestCase):
self.assertResponse(trade.response)
self.assertEqual(
trade.response.json()["bad_request"], "This order has been cancelled by the maker"
trade.response.json()["bad_request"],
"This order has been cancelled by the maker",
)
def test_cancel_order_different_cancel_status(self):
@ -1147,7 +1191,7 @@ class TradeTest(BaseAPITestCase):
self.assertEqual(
trade.response.json()["bad_request"],
f"Current order status is {Order.Status.PAU}, not {Order.Status.PUB}."
f"Current order status is {Order.Status.PAU}, not {Order.Status.PUB}.",
)
# Cancel order to avoid leaving pending HTLCs after a successful test
@ -1278,6 +1322,9 @@ class TradeTest(BaseAPITestCase):
f"😪 Hey {data['maker_nick']}, your order with ID {str(trade.order_id)} has expired without a taker.",
)
trade.get_review(trade.maker_index)
self.assertEqual(trade.response.status_code, 400)
def test_taken_order_expires(self):
"""
Tests the expiration of a public order
@ -1708,6 +1755,11 @@ class TradeTest(BaseAPITestCase):
f"⚖️ Hey {data['taker_nick']}, a dispute has been opened on your order with ID {str(trade.order_id)}.",
)
trade.get_review(trade.maker_index)
self.assertEqual(trade.response.status_code, 400)
trade.get_review(trade.taker_index)
self.assertEqual(trade.response.status_code, 400)
def test_ticks(self):
"""
Tests the historical ticks serving endpoint after creating a contract

View File

@ -113,6 +113,16 @@ class Trade:
headers = self.get_robot_auth(robot_index, first_encounter)
self.response = self.client.get(path + params, **headers)
def get_review(self, robot_index=1):
"""
Generates coordinator's review signature
"""
path = reverse("review")
headers = self.get_robot_auth(robot_index)
nostr_pubkey = read_file(f"tests/robots/{robot_index}/nostr_pubkey")
body = {"pubkey": nostr_pubkey}
self.response = self.client.post(path, body, **headers)
@patch("api.tasks.send_notification.delay", send_notification)
def cancel_order(self, robot_index=1, cancel_status=None):
path = reverse("order")