diff --git a/.env-sample b/.env-sample
index a80d8ef6..77f06ce3 100644
--- a/.env-sample
+++ b/.env-sample
@@ -5,6 +5,8 @@ MARKET_PRICE_API = 'https://blockchain.info/ticker'
FEE = 0.002
# Bond size in percentage %
BOND_SIZE = 0.01
+# Time out penalty for canceling takers in MINUTES
+PENALTY_TIMEOUT = 2
# Trade limits in satoshis
MIN_TRADE = 10000
diff --git a/api/logics.py b/api/logics.py
index 8374f17b..7cdc48d7 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -10,6 +10,7 @@ FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE'))
MARKET_PRICE_API = config('MARKET_PRICE_API')
ESCROW_USERNAME = config('ESCROW_USERNAME')
+PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT'))
MIN_TRADE = int(config('MIN_TRADE'))
MAX_TRADE = int(config('MAX_TRADE'))
@@ -21,6 +22,7 @@ EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE'))
BOND_EXPIRY = int(config('BOND_EXPIRY'))
ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
+
class Logics():
def validate_already_maker_or_taker(user):
@@ -40,11 +42,17 @@ class Logics():
if order.t0_satoshis < MIN_TRADE:
return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'}
return True, None
-
- def take(order, user):
- order.taker = user
- order.status = Order.Status.TAK
- order.save()
+
+ @classmethod
+ def take(cls, order, user):
+ is_penalized, time_out = cls.is_penalized(user)
+ if is_penalized:
+ return False, {'bad_request',f'You need to wait {time_out} seconds to take an order'}
+ else:
+ order.taker = user
+ order.status = Order.Status.TAK
+ order.save()
+ return True, None
def is_buyer(order, user):
is_maker = order.maker == user
@@ -167,6 +175,18 @@ class Logics():
order.save()
return True, None
+ def is_penalized(user):
+ ''' Checks if a user that is not participant of orders
+ has a limit on taking or making a order'''
+
+ if user.profile.penalty_expiration:
+ if user.profile.penalty_expiration > timezone.now():
+ time_out = (user.profile.penalty_expiration - timezone.now()).seconds
+ return True, time_out
+
+ return False, None
+
+
@classmethod
def cancel_order(cls, order, user, state=None):
@@ -181,12 +201,24 @@ class Logics():
# 2) When maker cancels after bond
'''The order dissapears from book and goes to cancelled.
- Maker is charged a small amount of sats, to prevent DDOS
- on the LN node and order book'''
+ Maker is charged the bond to prevent DDOS
+ on the LN node and order book. TODO Only charge a small part
+ of the bond (requires maker submitting an invoice)'''
+
# 3) When taker cancels before bond
''' The order goes back to the book as public.
LNPayment "order.taker_bond" is deleted() '''
+ elif order.status == Order.Status.TAK and order.taker == user:
+ # adds a timeout penalty
+ user.profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
+ user.save()
+
+ order.taker = None
+ order.status = Order.Status.PUB
+ order.save()
+
+ return True, None
# 4) When taker or maker cancel after bond (before escrow)
'''The order goes into cancelled status if maker cancels.
diff --git a/api/models.py b/api/models.py
index db6d9cf8..36b926de 100644
--- a/api/models.py
+++ b/api/models.py
@@ -158,6 +158,9 @@ class Profile(models.Model):
# RoboHash
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
+ # Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
+ penalty_expiration = models.DateTimeField(null=True)
+
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
diff --git a/api/utils.py b/api/utils.py
index e185c3f9..3cf948c5 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -5,7 +5,7 @@ import ring
storage = {}
-@ring.dict(storage, expire=10) #keeps in cache for 10 seconds
+@ring.dict(storage, expire=30) #keeps in cache for 30 seconds
def get_exchange_rate(currency):
# TODO Add fallback Public APIs and error handling
# Think about polling price data in a different way (e.g. store locally every t seconds)
diff --git a/api/views.py b/api/views.py
index a4f474d4..f92c5e7a 100644
--- a/api/views.py
+++ b/api/views.py
@@ -105,6 +105,11 @@ class OrderView(viewsets.ViewSet):
data = ListOrderSerializer(order).data
+ # if user is under a limit (penalty), inform him
+ is_penalized, time_out = Logics.is_penalized(request.user)
+ if is_penalized:
+ data['penalty'] = time_out
+
# Add booleans if user is maker, taker, partipant, buyer or seller
data['is_maker'] = order.maker == request.user
data['is_taker'] = order.taker == request.user
@@ -207,7 +212,9 @@ class OrderView(viewsets.ViewSet):
valid, context = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
- Logics.take(order, request.user)
+ valid, context = Logics.take(order, request.user)
+ if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
+
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
# Any other action is only allowed if the user is a participant
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js
index e0703490..65340f0d 100644
--- a/frontend/src/components/OrderPage.js
+++ b/frontend/src/components/OrderPage.js
@@ -1,5 +1,5 @@
import React, { Component } from "react";
-import { Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress} from "@mui/material"
+import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress} from "@mui/material"
import TradeBox from "./TradeBox";
function msToTime(duration) {
@@ -101,6 +101,7 @@ export default class OrderPage extends Component {
isTaker: data.is_taker,
isBuyer: data.is_buyer,
isSeller: data.is_seller,
+ penalty: data.penalty,
expiresAt: data.expires_at,
badRequest: data.bad_request,
bondInvoice: data.bond_invoice,
@@ -249,6 +250,18 @@ export default class OrderPage extends Component {
+
+ {/* If the user has a penalty/limit */}
+ {this.state.penalty ?
+ <>
+
+
+
+ You cannot take an order yet! Wait {this.state.penalty} seconds
+
+
+ >
+ : null}