diff --git a/.env-sample b/.env-sample new file mode 100644 index 00000000..2e88f3ec --- /dev/null +++ b/.env-sample @@ -0,0 +1,17 @@ +# Market price public API +MARKET_PRICE_API = 'https://blockchain.info/ticker' + +# Trade fee in percentage % +FEE = 0.002 +# Bond size in percentage % +BOND_SIZE = 0.01 + +# Trade limits in satoshis +MIN_TRADE = 10000 +MAX_TRADE = 500000 + +# Expiration time in minutes +EXPIRATION_MAKE = 5 + +# Username for HTLCs escrows +ESCROW_USERNAME = 'admin' \ No newline at end of file diff --git a/api/admin.py b/api/admin.py index b45ca85b..a5b32506 100644 --- a/api/admin.py +++ b/api/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django_admin_relation_links import AdminChangeLinksMixin from django.contrib.auth.models import Group, User from django.contrib.auth.admin import UserAdmin -from .models import Order, LNPayment, Profile +from .models import Order, LNPayment, Profile, MarketTick admin.site.unregister(Group) admin.site.unregister(User) @@ -17,26 +17,34 @@ class ProfileInline(admin.StackedInline): @admin.register(User) class EUserAdmin(UserAdmin): inlines = [ProfileInline] - list_display = ('avatar_tag',) + UserAdmin.list_display - list_display_links = ['username'] + list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff') + list_display_links = ('id','username') def avatar_tag(self, obj): return obj.profile.avatar_tag() @admin.register(Order) class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') + list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') list_display_links = ('id','type') change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow') + list_filter = ('is_disputed','is_fiat_sent','type','currency','status') @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link') list_display_links = ('id','concept') change_links = ('sender','receiver') + list_filter = ('type','concept','status') @admin.register(Profile) class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes') list_display_links = ('avatar_tag','id') change_links =['user'] - readonly_fields = ['avatar_tag'] \ No newline at end of file + readonly_fields = ['avatar_tag'] + +@admin.register(MarketTick) +class MarketTickAdmin(admin.ModelAdmin): + list_display = ('timestamp','price','volume','premium','currency','fee') + readonly_fields = ('timestamp','price','volume','premium','currency','fee') + list_filter = ['currency'] \ No newline at end of file diff --git a/api/lightning.py b/api/lightning.py index cf66a12f..a10f258b 100644 --- a/api/lightning.py +++ b/api/lightning.py @@ -22,32 +22,36 @@ class LNNode(): return invoice, payment_hash, expires_at - def validate_hodl_invoice_locked(): + def validate_hodl_invoice_locked(payment_hash): '''Generates hodl invoice to publish an order''' return True - def validate_ln_invoice(invoice): # num_satoshis + def validate_ln_invoice(invoice, num_satoshis): # num_satoshis '''Checks if the submited LN invoice is as expected''' valid = True - num_satoshis = 50000 # TODO decrypt and confirm sats are as expected + context = None description = 'Placeholder desc' # TODO decrypt from LN invoice - payment_hash = '567126' # TODO decrypt + payment_hash = '567&*GIHU126' # TODO decrypt expires_at = timezone.now() # TODO decrypt - return valid, num_satoshis, description, payment_hash, expires_at + return valid, context, description, payment_hash, expires_at - def pay_buyer_invoice(invoice): - '''Sends sats to buyer''' + def pay_invoice(invoice): + '''Sends sats to buyer, or cancelinvoices''' return True - def charge_hodl_htlcs(invoice): + def settle_hodl_htlcs(payment_hash): '''Charges a LN hodl invoice''' return True - def free_hodl_htlcs(invoice): + def return_hodl_htlcs(payment_hash): '''Returns sats''' return True + def double_check_htlc_is_settled(payment_hash): + ''' Just as it sounds. Better safe than sorry!''' + return True + diff --git a/api/logics.py b/api/logics.py index 4324b2b8..855f6c67 100644 --- a/api/logics.py +++ b/api/logics.py @@ -1,10 +1,10 @@ from datetime import timedelta from django.utils import timezone -import requests from .lightning import LNNode -from .models import Order, LNPayment, User +from .models import Order, LNPayment, MarketTick, User from decouple import config +from .utils import get_exchange_rate FEE = float(config('FEE')) BOND_SIZE = float(config('BOND_SIZE')) @@ -23,8 +23,6 @@ ESCROW_EXPIRY = int(config('ESCROW_EXPIRY')) class Logics(): - # escrow_user = User.objects.get(username=ESCROW_USERNAME) - def validate_already_maker_or_taker(user): '''Checks if the user is already partipant of an order''' queryset = Order.objects.filter(maker=user) @@ -63,11 +61,9 @@ class Logics(): if order.is_explicit: satoshis_now = order.satoshis else: - # TODO Add fallback Public APIs and error handling - # Think about polling price data in a different way (e.g. store locally every t seconds) - market_prices = requests.get(MARKET_PRICE_API).json() - exchange_rate = float(market_prices[Order.currency_dict[str(order.currency)]]['last']) - satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 + exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) + premium_rate = exchange_rate * (1+float(order.premium)/100) + satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000 return int(satoshis_now) @@ -89,10 +85,19 @@ class Logics(): @classmethod def update_invoice(cls, order, user, invoice): - is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) - # only user is the buyer and a valid LN invoice - if not (cls.is_buyer(order, user) or is_valid_invoice): - return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'} + + # only the buyer can post a buyer invoice + if not cls.is_buyer(order, user): + return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'} + if not order.taker_bond: + return False, {'bad_request':'Wait for your order to be taken.'} + if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED): + return False, {'bad_request':'You cannot a invoice while bonds are not posted.'} + + num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount'] + valid, context, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice, num_satoshis) + if not valid: + return False, context order.buyer_invoice, _ = LNPayment.objects.update_or_create( concept = LNPayment.Concepts.PAYBUYER, @@ -130,22 +135,29 @@ class Logics(): @classmethod def rate_counterparty(cls, order, user, rating): - # if maker, rates taker - if order.maker == user: - order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1 - last_ratings = list(order.taker.profile.last_ratings).append(rating) - order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings) - # if taker, rates maker - if order.taker == user: - order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1 - last_ratings = list(order.maker.profile.last_ratings).append(rating) - order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings) + + # If the trade is finished + if order.status > Order.Status.PAY: + + # if maker, rates taker + if order.maker == user: + order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1 + last_ratings = list(order.taker.profile.last_ratings).append(rating) + order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings) + + # if taker, rates maker + if order.taker == user: + order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1 + last_ratings = list(order.maker.profile.last_ratings).append(rating) + order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings) + else: + return False, {'bad_request':'You cannot rate your counterparty yet.'} order.save() return True, None @classmethod - def cancel_order(cls, order, user, state): + def cancel_order(cls, order, user, state=None): # 1) When maker cancels before bond '''The order never shows up on the book and order @@ -291,4 +303,55 @@ class Logics(): expires_at = expires_at) order.save() - return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis} \ No newline at end of file + return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis} + + def settle_escrow(order): + ''' Settles the trade escrow HTLC''' + # TODO ERROR HANDLING + + valid = LNNode.settle_hodl_htlcs(order.trade_escrow.payment_hash) + return valid + + def pay_buyer_invoice(order): + ''' Settles the trade escrow HTLC''' + # TODO ERROR HANDLING + + valid = LNNode.pay_invoice(order.buyer_invoice.payment_hash) + return valid + + @classmethod + def confirm_fiat(cls, order, user): + ''' If Order is in the CHAT states: + If user is buyer: mark FIAT SENT and settle escrow! + If User is the seller and FIAT is SENT: Pay buyer invoice!''' + + if order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Alternatively, if all collateral is locked? test out + + # If buyer, settle escrow and mark fiat sent + if cls.is_buyer(order, user): + if cls.settle_escrow(order): # KEY LINE - SETTLES THE TRADE ESCROW !! + order.trade_escrow.status = LNPayment.Status.SETLED + order.status = Order.Status.FSE + order.is_fiat_sent = True + + # If seller and fiat sent, pay buyer invoice + elif cls.is_seller(order, user): + if not order.is_fiat_sent: + return False, {'bad_request':'You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer.'} + + # Double check the escrow is settled. + if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): + + # Make sure the trade escrow is at least as big as the buyer invoice + if order.trade_escrow.num_satoshis <= order.buyer_invoice.num_satoshis: + return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'} + + # Double check the trade escrow is settled + elif cls.pay_buyer_invoice(order): # KEY LINE - PAYS THE BUYER !! + order.status = Order.Status.PAY + order.buyer_invoice.status = LNPayment.Status.PAYING + else: + return False, {'bad_request':'You cannot confirm the fiat payment at this stage'} + + order.save() + return True, None diff --git a/api/models.py b/api/models.py index bda4d442..adcffda9 100644 --- a/api/models.py +++ b/api/models.py @@ -7,19 +7,14 @@ from django.utils.html import mark_safe from decouple import config from pathlib import Path +from .utils import get_exchange_rate import json -############################# -# TODO -# Load hparams from .env file - MIN_TRADE = int(config('MIN_TRADE')) MAX_TRADE = int(config('MAX_TRADE')) FEE = float(config('FEE')) BOND_SIZE = float(config('BOND_SIZE')) - - class LNPayment(models.Model): class Types(models.IntegerChoices): @@ -39,7 +34,8 @@ class LNPayment(models.Model): RETNED = 3, 'Returned' MISSNG = 4, 'Missing' VALIDI = 5, 'Valid' - INFAIL = 6, 'Failed routing' + PAYING = 6, 'Paying ongoing' + FAILRO = 7, 'Failed routing' # payment use details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL) @@ -74,24 +70,24 @@ class Order(models.Model): DEL = 2, 'Deleted' TAK = 3, 'Waiting for taker bond' UCA = 4, 'Cancelled' - WF2 = 5, 'Waiting for trade collateral and buyer invoice' - WFE = 6, 'Waiting only for seller trade collateral' - WFI = 7, 'Waiting only for buyer invoice' - CHA = 8, 'Sending fiat - In chatroom' - CCA = 9, 'Collaboratively cancelled' + EXP = 5, 'Expired' + WF2 = 6, 'Waiting for trade collateral and buyer invoice' + WFE = 7, 'Waiting only for seller trade collateral' + WFI = 8, 'Waiting only for buyer invoice' + CHA = 9, 'Sending fiat - In chatroom' FSE = 10, 'Fiat sent - In chatroom' - FCO = 11, 'Fiat confirmed' - SUC = 12, 'Sucessfully settled' - FAI = 13, 'Failed lightning network routing' - UPI = 14, 'Updated invoice' - DIS = 15, 'In dispute' + DIS = 11, 'In dispute' + CCA = 12, 'Collaboratively cancelled' + PAY = 13, 'Sending satoshis to buyer' + SUC = 14, 'Sucessfully settled' + FAI = 15, 'Failed lightning network routing' MLD = 16, 'Maker lost dispute' TLD = 17, 'Taker lost dispute' - EXP = 18, 'Expired' - + currency_dict = json.load(open('./api/currencies.json')) currency_choices = [(int(val), label) for val, label in list(currency_dict.items())] print(currency_choices) + # order info status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB) created_at = models.DateTimeField(auto_now_add=True) @@ -101,7 +97,7 @@ class Order(models.Model): type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False) amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)]) - payment_method = models.CharField(max_length=30, null=False, default="not specified", blank=True) + payment_method = models.CharField(max_length=50, null=False, default="not specified", blank=True) # order pricing method. A explicit amount of sats, or a relative premium above/below market. is_explicit = models.BooleanField(default=False, null=False) @@ -117,8 +113,11 @@ class Order(models.Model): maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled. + is_disputed = models.BooleanField(default=False, null=False) + is_fiat_sent = models.BooleanField(default=False, null=False) - # order collateral + # HTLCs + # Order collateral maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True) @@ -126,9 +125,13 @@ class Order(models.Model): # buyer payment LN invoice buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) + # cancel LN invoice // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing. + maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True) + taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True) + def __str__(self): # Make relational back to ORDER - return (f'Order {self.id}: {self.Types(self.type).label} BTC for {self.amount} {self.currency_dict[str(self.currency)]}') + return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}') @receiver(pre_delete, sender=Order) def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): @@ -183,3 +186,48 @@ class Profile(models.Model): def avatar_tag(self): return mark_safe('' % self.get_avatar()) +class MarketTick(models.Model): + ''' + Records tick by tick Non-KYC Bitcoin price. + Data to be aggregated and offered via public API. + + It is checked against current cex prices for nice + insight on the historical premium of Non-KYC BTC + + Price is set when both taker bond is locked. Both + maker and taker are commited with bonds (contract + is finished and cancellation has a cost) + ''' + + price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)]) + volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)]) + premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True) + currency = models.PositiveSmallIntegerField(choices=Order.currency_choices, null=True) + timestamp = models.DateTimeField(auto_now_add=True) + + # Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed + fee = models.DecimalField(max_digits=4, decimal_places=4, default=FEE, validators=[MinValueValidator(0), MaxValueValidator(1)]) + + def log_a_tick(order): + ''' + Adds a new tick + ''' + if not order.taker_bond: + return None + + elif order.taker_bond.status == LNPayment.Status.LOCKED: + volume = order.last_satoshis / 100000000 + price = float(order.amount) / volume # Amount Fiat / Amount BTC + premium = 100 * (price / get_exchange_rate(Order.currency_dict[str(order.currency)]) - 1) + + tick = MarketTick.objects.create( + price=price, + volume=volume, + premium=premium, + currency=order.currency) + tick.save() + + def __str__(self): + return f'Tick: {self.id}' + + diff --git a/api/urls.py b/api/urls.py index 1b2897d5..1e235cc0 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import OrderMakerView, OrderView, UserView, BookView, get_currencies_json +from .views import MakerView, OrderView, UserView, BookView, InfoView, get_currencies_json urlpatterns = [ - path('make/', OrderMakerView.as_view()), + path('make/', MakerView.as_view()), path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})), path('usergen/', UserView.as_view()), path('book/', BookView.as_view()), + path('info/', InfoView.as_view()), path('currencies/', get_currencies_json), ] \ No newline at end of file diff --git a/api/utils.py b/api/utils.py new file mode 100644 index 00000000..0b1b934b --- /dev/null +++ b/api/utils.py @@ -0,0 +1,12 @@ + +from decouple import config +import requests + +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) + + market_prices = requests.get(config('MARKET_PRICE_API')).json() + exchange_rate = float(market_prices[currency]['last']) + + return exchange_rate \ No newline at end of file diff --git a/api/views.py b/api/views.py index 4674cd29..b58759c5 100644 --- a/api/views.py +++ b/api/views.py @@ -25,13 +25,14 @@ import json from django.http import HttpResponse EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) +FEE = float(config('FEE')) avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) # Create your views here. -class OrderMakerView(CreateAPIView): +class MakerView(CreateAPIView): serializer_class = MakeOrderSerializer def post(self,request): @@ -124,7 +125,19 @@ class OrderView(viewsets.ViewSet): data['is_seller'] = Logics.is_seller(order,request.user) data['maker_nick'] = str(order.maker) data['taker_nick'] = str(order.taker) - data['status_message'] = Order.Status(order.status).label + data['status_message'] = Order.Status(order.status).label + data['is_fiat_sent'] = order.is_fiat_sent + data['is_disputed'] = order.is_disputed + + # If both bonds are locked, participants can see the trade in sats is also final. + if order.taker_bond: + if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED: + # Seller sees the amount he pays + if data['is_seller']: + data['trade_satoshis'] = order.last_satoshis + # Buyer sees the amount he receives + elif data['is_buyer']: + data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount'] # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice. if order.status == Order.Status.WFB and data['is_maker']: @@ -169,7 +182,11 @@ class OrderView(viewsets.ViewSet): if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED: # add whether a collaborative cancel is pending data['pending_cancel'] = order.is_pending_cancel - + + # 9) if buyer confirmed FIAT SENT + elif order.status == Order.Status.FSE: + data['buyer_confirmed'] + return Response(data, status.HTTP_200_OK) def take_update_confirm_dispute_cancel(self, request, format=None): @@ -198,32 +215,42 @@ class OrderView(viewsets.ViewSet): Logics.take(order, request.user) else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST) - # 2) If action is update (invoice) - elif action == 'update_invoice' and invoice: + # Any other action is only allowed if the user is a participant + if not (order.maker == request.user or order.taker == request.user): + return Response({'bad_request':'You are not a participant in this order'}, status.HTTP_403_FORBIDDEN) + + # 2) If action is 'update invoice' + if action == 'update_invoice' and invoice: valid, context = Logics.update_invoice(order,request.user,invoice) - if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) + if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 3) If action is cancel elif action == 'cancel': valid, context = Logics.cancel_order(order,request.user) - if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) + if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 4) If action is confirm elif action == 'confirm': - pass + valid, context = Logics.confirm_fiat(order,request.user) + if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 5) If action is dispute elif action == 'dispute': - pass + valid, context = Logics.open_dispute(order,request.user, rating) + if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) - # 6) If action is dispute + # 6) If action is rate elif action == 'rate' and rating: valid, context = Logics.rate_counterparty(order,request.user, rating) - if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) + if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) - # If nothing... something else is going on. Probably not allowed! + # If nothing of the above... something else is going on. Probably not allowed! else: - return Response({'bad_request':'The Robotic Satoshis working in the warehouse did not understand you'}) + return Response( + {'bad_request': + 'The Robotic Satoshis working in the warehouse did not understand you. ' + + 'Please, fill a Bug Issue in Github https://github.com/Reckless-Satoshi/robosats/issues'}, + status.HTTP_501_NOT_IMPLEMENTED) return self.get(request) @@ -298,23 +325,27 @@ class UserView(APIView): # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) context['found'] = 'Bad luck, this nickname is taken' context['bad_request'] = 'Enter a different token' - return Response(context, status=status.HTTP_403_FORBIDDEN) + return Response(context, status.HTTP_403_FORBIDDEN) def delete(self,request): - user = User.objects.get(id = request.user.id) + ''' Pressing "give me another" deletes the logged in user ''' + user = request.user + if not user: + return Response(status.HTTP_403_FORBIDDEN) - # TO DO. Pressing "give me another" deletes the logged in user - # However it might be a long time recovered user - # Only delete if user live is < 5 minutes + # Only delete if user life is shorter than 30 minutes. Helps deleting users by mistake + if user.date_joined < (timezone.now() - timedelta(minutes=30)): + return Response(status.HTTP_400_BAD_REQUEST) - # TODO check if user exists AND it is not a maker or taker! - if user is not None: - logout(request) - user.delete() + # Check if it is not a maker or taker! + if not Logics.validate_already_maker_or_taker(user): + return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST) - return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_302_FOUND) + logout(request) + user.delete() + return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY) - return Response(status=status.HTTP_403_FORBIDDEN) + class BookView(ListAPIView): serializer_class = ListOrderSerializer @@ -342,6 +373,19 @@ class BookView(ListAPIView): return Response(book_data, status=status.HTTP_200_OK) +class InfoView(ListAPIView): + + def get(self): + context = {} + + context['num_public_buy_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB)) + context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB)) + context['last_day_avg_btc_premium'] = None # Todo + context['num_active_robots'] = None + context['total_volume'] = None + + return Response(context, status.HTTP_200_OK) + def get_currencies_json(request): currency_dict = json.load(open('./api/currencies.json')) return HttpResponse(json.dumps(currency_dict),content_type="application/json") diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index f0635c59..72a6a511 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -193,7 +193,8 @@ export default class MakerPage extends Component { type="text" require={true} inputProps={{ - style: {textAlign:"center"} + style: {textAlign:"center"}, + maxLength: 50 }} onChange={this.handlePaymentMethodChange} />