from datetime import datetime, timedelta from decouple import config from django.conf import settings from django.contrib.auth.models import User from django.db.models import Q, Sum from django.utils import timezone from drf_spectacular.utils import extend_schema from rest_framework import status, viewsets from rest_framework.authentication import TokenAuthentication from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from api.logics import Logics from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order from api.notifications import Telegram from api.oas_schemas import ( BookViewSchema, HistoricalViewSchema, InfoViewSchema, LimitViewSchema, MakerViewSchema, OrderViewSchema, PriceViewSchema, RewardViewSchema, RobotViewSchema, StealthViewSchema, TickViewSchema, ) from api.serializers import ( ClaimRewardSerializer, InfoSerializer, ListOrderSerializer, MakeOrderSerializer, OrderPublicSerializer, PriceSerializer, StealthSerializer, TickSerializer, UpdateOrderSerializer, ) from api.utils import ( compute_avg_premium, compute_premium_percentile, get_cln_version, get_lnd_version, get_robosats_commit, verify_signed_message, ) from chat.models import Message from control.models import AccountingDay, BalanceLog EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE")) RETRY_TIME = int(config("RETRY_TIME")) class MakerView(CreateAPIView): serializer_class = MakeOrderSerializer authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] @extend_schema(**MakerViewSchema.post) def post(self, request): serializer = self.serializer_class(data=request.data) if not request.user.is_authenticated: return Response( {"bad_request": "Woops! It seems you do not have a robot avatar"}, status.HTTP_400_BAD_REQUEST, ) if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST) # In case it gets overwhelming. Limit the number of public orders. if Order.objects.filter(status=Order.Status.PUB).count() >= int( config("MAX_PUBLIC_ORDERS") ): return Response( { "bad_request": f"The RoboSats {config('COORDINATOR_ALIAS', cast=str, default='NoAlias')} coordinator book is at full capacity! Current limit is {config('MAX_PUBLIC_ORDERS', cast=str)} orders" }, status.HTTP_400_BAD_REQUEST, ) # Only allow users who are not already engaged in an order valid, context, _ = Logics.validate_already_maker_or_taker(request.user) if not valid: return Response(context, status.HTTP_409_CONFLICT) type = serializer.data.get("type") currency = serializer.data.get("currency") amount = serializer.data.get("amount") has_range = serializer.data.get("has_range") min_amount = serializer.data.get("min_amount") max_amount = serializer.data.get("max_amount") payment_method = serializer.data.get("payment_method") premium = serializer.data.get("premium") satoshis = serializer.data.get("satoshis") is_explicit = serializer.data.get("is_explicit") public_duration = serializer.data.get("public_duration") escrow_duration = serializer.data.get("escrow_duration") bond_size = serializer.data.get("bond_size") latitude = serializer.data.get("latitude") longitude = serializer.data.get("longitude") # Optional params if public_duration is None: public_duration = 60 * 60 * settings.DEFAULT_PUBLIC_ORDER_DURATION if escrow_duration is None: escrow_duration = 60 * settings.INVOICE_AND_ESCROW_DURATION if bond_size is None: bond_size = settings.DEFAULT_BOND_SIZE if has_range is None: has_range = False # An order can either have an amount or a range (min_amount and max_amount) if has_range: amount = None else: min_amount = None max_amount = None # Either amount or min_max has to be specified. if has_range and (min_amount is None or max_amount is None): return Response( { "bad_request": "You must specify min_amount and max_amount for a range order" }, status.HTTP_400_BAD_REQUEST, ) elif not has_range and amount is None: return Response( {"bad_request": "You must specify an order amount"}, status.HTTP_400_BAD_REQUEST, ) # Creates a new order order = Order( type=type, currency=Currency.objects.get(id=currency), amount=amount, has_range=has_range, min_amount=min_amount, max_amount=max_amount, payment_method=payment_method, premium=premium, satoshis=satoshis, is_explicit=is_explicit, expires_at=timezone.now() + timedelta(seconds=EXP_MAKER_BOND_INVOICE), maker=request.user, public_duration=public_duration, escrow_duration=escrow_duration, bond_size=bond_size, latitude=latitude, longitude=longitude, ) order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order) valid, context = Logics.validate_order_size(order) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) valid, context = Logics.validate_location(order) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) order.save() order.log( f"Order({order.id},{order}) created by Robot({request.user.robot.id},{request.user})" ) return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED) class OrderView(viewsets.ViewSet): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] serializer_class = UpdateOrderSerializer lookup_url_kwarg = "order_id" @extend_schema(**OrderViewSchema.get) def get(self, request, format=None): """ Full trade pipeline takes place while looking/refreshing the order page. """ order_id = request.GET.get(self.lookup_url_kwarg) if not request.user.is_authenticated: return Response( { "bad_request": "You must have a robot avatar to see the order details" }, status=status.HTTP_400_BAD_REQUEST, ) if order_id is None: return Response( {"bad_request": "Order ID parameter not found in request"}, status=status.HTTP_400_BAD_REQUEST, ) order = Order.objects.filter(id=order_id) # check if exactly one order is found in the db if len(order) != 1: return Response( {"bad_request": "Invalid Order Id"}, status.HTTP_404_NOT_FOUND ) # This is our order. order = order[0] # 2) If order has been cancelled if order.status == Order.Status.UCA: return Response( {"bad_request": "This order has been cancelled by the maker"}, status.HTTP_400_BAD_REQUEST, ) if order.status == Order.Status.CCA: return Response( {"bad_request": "This order has been cancelled collaborativelly"}, status.HTTP_400_BAD_REQUEST, ) data = ListOrderSerializer(order).data data["total_secs_exp"] = order.t_to_expire(order.status) # if user is under a limit (penalty), inform him. is_penalized, time_out = Logics.is_penalized(request.user) if is_penalized: data["penalty"] = request.user.robot.penalty_expiration # 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 data["is_participant"] = data["is_maker"] or data["is_taker"] # 3.a) If not a participant and order is not public, forbid. if not data["is_participant"] and order.status != Order.Status.PUB: return Response( {"bad_request": "This order is not available"}, status.HTTP_403_FORBIDDEN, ) data["maker_nick"] = str(order.maker) data["maker_hash_id"] = str(order.maker.robot.hash_id) # Add activity status of participants based on last_seen data["maker_status"] = Logics.user_activity_status(order.maker.last_login) if order.taker is not None: data["taker_status"] = Logics.user_activity_status(order.taker.last_login) # 3.b) Non participants can view details (but only if PUB) if not data["is_participant"] and order.status == Order.Status.PUB: data["price_now"], data["premium_now"] = Logics.price_and_premium_now(order) data["satoshis_now"] = Logics.satoshis_now(order) return Response(data, status=status.HTTP_200_OK) # 4) If order is between public and WF2 if order.status >= Order.Status.PUB and order.status < Order.Status.WF2: data["price_now"], data["premium_now"] = Logics.price_and_premium_now(order) data["satoshis_now"] = Logics.satoshis_now(order) # 4. a) If maker and Public/Paused, add premium percentile # num similar orders, and maker information to enable telegram notifications. if data["is_maker"] and order.status in [ Order.Status.PUB, Order.Status.PAU, ]: data["premium_percentile"] = compute_premium_percentile(order) data["num_similar_orders"] = len( Order.objects.filter( currency=order.currency, status=Order.Status.PUB ) ) # For participants add positions, nicks and status as a message and hold invoices status data["is_buyer"] = Logics.is_buyer(order, request.user) data["is_seller"] = Logics.is_seller(order, request.user) data["taker_nick"] = str(order.taker) if order.taker: data["taker_hash_id"] = str(order.taker.robot.hash_id) data["status_message"] = Order.Status(order.status).label data["is_fiat_sent"] = order.is_fiat_sent data["latitude"] = order.latitude data["longitude"] = order.longitude data["is_disputed"] = order.is_disputed data["ur_nick"] = request.user.username data["satoshis_now"] = order.last_satoshis # Add whether hold invoices are LOCKED (ACCEPTED) # Is there a maker bond? If so, True if locked, False otherwise if order.maker_bond: data["maker_locked"] = order.maker_bond.status == LNPayment.Status.LOCKED else: data["maker_locked"] = False # Is there a taker bond? If so, True if locked, False otherwise if order.taker_bond: data["taker_locked"] = order.taker_bond.status == LNPayment.Status.LOCKED else: data["taker_locked"] = False # Is there an escrow? If so, True if locked, False otherwise if order.trade_escrow: data["escrow_locked"] = order.trade_escrow.status == LNPayment.Status.LOCKED else: data["escrow_locked"] = False # If both bonds are locked, participants can see the final trade amount in sats. if order.status in [ Order.Status.WF2, Order.Status.WFI, Order.Status.WFE, Order.Status.CCA, Order.Status.FSE, Order.Status.DIS, Order.Status.PAY, Order.Status.SUC, Order.Status.FAI, ]: if ( order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED ): # Seller sees the amount he sends if data["is_seller"]: data["trade_satoshis"] = Logics.escrow_amount(order, request.user)[ 1 ]["escrow_amount"] # Buyer sees the amount he receives elif data["is_buyer"]: data["trade_satoshis"] = Logics.payout_amount(order, request.user)[ 1 ]["invoice_amount"] # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice. if order.status == Order.Status.WFB and data["is_maker"]: valid, context = Logics.gen_maker_hold_invoice(order, request.user) if valid: data = {**data, **context} else: return Response(context, status.HTTP_400_BAD_REQUEST) # 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice. elif order.status == Order.Status.TAK and data["is_taker"]: valid, context = Logics.gen_taker_hold_invoice(order, request.user) if valid: data = {**data, **context} else: return Response(context, status.HTTP_400_BAD_REQUEST) # 7 a. ) If seller and status is 'WF2' or 'WFE' elif data["is_seller"] and ( order.status == Order.Status.WF2 or order.status == Order.Status.WFE ): # If the two bonds are locked, reply with an ESCROW hold invoice. if ( order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED ): valid, context = Logics.gen_escrow_hold_invoice(order, request.user) if valid: data = {**data, **context} else: return Response(context, status.HTTP_400_BAD_REQUEST) # 7.b) If user is Buyer and status is 'WF2' or 'WFI' elif data["is_buyer"] and ( order.status == Order.Status.WF2 or order.status == Order.Status.WFI ): # If the two bonds are locked, reply with an AMOUNT and onchain swap cost so he can send the buyer invoice/address. if ( order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED ): valid, context = Logics.payout_amount(order, request.user) if valid: data = {**data, **context} else: return Response(context, status.HTTP_400_BAD_REQUEST) # 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]: # If all bonds are locked. if ( order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED ): # add whether a collaborative cancel is pending or has been asked if (data["is_maker"] and order.taker_asked_cancel) or ( data["is_taker"] and order.maker_asked_cancel ): data["pending_cancel"] = True elif (data["is_maker"] and order.maker_asked_cancel) or ( data["is_taker"] and order.taker_asked_cancel ): data["asked_for_cancel"] = True else: data["asked_for_cancel"] = False # Add index of last chat message. To be used by client on Chat endpoint to fetch latest messages messages = Message.objects.filter(order=order) if len(messages) == 0: data["chat_last_index"] = 0 else: data["chat_last_index"] = messages.latest().index # 9) If status is 'DIS' and all HTLCS are in LOCKED elif order.status == Order.Status.DIS: # add whether the dispute statement has been received if data["is_maker"]: data["statement_submitted"] = ( order.maker_statement is not None and order.maker_statement != "" ) elif data["is_taker"]: data["statement_submitted"] = ( order.taker_statement is not None and order.taker_statement != "" ) # 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third. elif ( order.status == Order.Status.FAI and order.payout.receiver == request.user ): # might not be the buyer if after a dispute where winner wins data["retries"] = order.payout.routing_attempts data["next_retry_time"] = order.payout.last_routing_time + timedelta( minutes=RETRY_TIME ) if order.payout.failure_reason: data["failure_reason"] = LNPayment.FailureReason( order.payout.failure_reason ).label if order.payout.status == LNPayment.Status.EXPIRE: data["invoice_expired"] = True # Add invoice amount once again if invoice was expired. data["trade_satoshis"] = Logics.payout_amount(order, request.user)[1][ "invoice_amount" ] # 10) If status is 'Expired', "Sending", "Finished" or "failed routing", add info for renewal: elif order.status in [ Order.Status.EXP, Order.Status.SUC, Order.Status.PAY, Order.Status.FAI, ]: data["public_duration"] = order.public_duration data["bond_size"] = str(order.bond_size) # Adds trade summary if order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]: valid, context = Logics.summarize_trade(order, request.user) if valid: data = {**data, **context} # If status is 'Expired' add expiry reason if order.status == Order.Status.EXP: data["expiry_reason"] = order.expiry_reason data["expiry_message"] = Order.ExpiryReasons(order.expiry_reason).label # If status is 'Succes' add final stats and txid if it is a swap if order.status == Order.Status.SUC: # If buyer and is a swap, add TXID if Logics.is_buyer(order, request.user): if order.is_swap: data["num_satoshis"] = order.payout_tx.num_satoshis data["sent_satoshis"] = order.payout_tx.sent_satoshis data["network"] = str(config("NETWORK")) if order.payout_tx.status in [ OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI, ]: data["txid"] = order.payout_tx.txid elif order.payout_tx.status == OnchainPayment.Status.QUEUE: data["tx_queued"] = True data["address"] = order.payout_tx.address return Response(data, status.HTTP_200_OK) @extend_schema(**OrderViewSchema.take_update_confirm_dispute_cancel) def take_update_confirm_dispute_cancel(self, request, format=None): """ Here takes place all of the updates to the order object. That is: take, confim, cancel, dispute, update_invoice or rate. """ order_id = request.GET.get(self.lookup_url_kwarg) serializer = UpdateOrderSerializer(data=request.data) if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST) order = Order.objects.get(id=order_id) # action is either 1)'take', 2)'confirm', 2.b)'undo_confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' # 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform' action = serializer.data.get("action") pgp_invoice = serializer.data.get("invoice") routing_budget_ppm = serializer.data.get("routing_budget_ppm", 0) pgp_address = serializer.data.get("address") mining_fee_rate = serializer.data.get("mining_fee_rate") statement = serializer.data.get("statement") rating = serializer.data.get("rating") # 1) If action is take, it is a taker request! if action == "take": if order.status == Order.Status.PUB: valid, context, _ = Logics.validate_already_maker_or_taker(request.user) if not valid: return Response(context, status=status.HTTP_409_CONFLICT) # For order with amount range, set the amount now. if order.has_range: amount = float(serializer.data.get("amount")) valid, context = Logics.validate_amount_within_range(order, amount) if not valid: return Response(context, status=status.HTTP_400_BAD_REQUEST) valid, context = Logics.take(order, request.user, amount) else: valid, context = Logics.take(order, request.user) if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN) return self.get(request) 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 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' elif action == "update_invoice": # DEPRECATE post v0.5.1. valid_signature, invoice = verify_signed_message( request.user.robot.public_key, pgp_invoice ) if not valid_signature: return Response( {"bad_request": "The PGP signed cleartext message is not valid."}, status.HTTP_400_BAD_REQUEST, ) valid, context = Logics.update_invoice( order, request.user, invoice, routing_budget_ppm ) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 2.b) If action is 'update address' elif action == "update_address": valid_signature, address = verify_signed_message( request.user.robot.public_key, pgp_address ) if not valid_signature: return Response( {"bad_request": "The PGP signed cleartext message is not valid."}, status.HTTP_400_BAD_REQUEST, ) valid, context = Logics.update_address( order, request.user, address, mining_fee_rate ) 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) # 4) If action is confirm elif action == "confirm": valid, context = Logics.confirm_fiat(order, request.user) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 4.b) If action is confirm elif action == "undo_confirm": valid, context = Logics.undo_confirm_fiat_sent(order, request.user) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 5) If action is dispute elif action == "dispute": valid, context = Logics.open_dispute(order, request.user) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) elif action == "submit_statement": valid, context = Logics.dispute_statement(order, request.user, statement) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 6) If action is rate elif action == "rate_user" and rating: """No user rating""" pass # 7) If action is rate_platform elif action == "rate_platform" and rating: valid, context = Logics.rate_platform(request.user, rating) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 8) If action is rate_platform elif action == "pause": valid, context = Logics.pause_unpause_public_order(order, request.user) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 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. " + "Please, fill a Bug Issue in Github https://github.com/RoboSats/robosats/issues" }, status.HTTP_501_NOT_IMPLEMENTED, ) return self.get(request) class RobotView(APIView): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] @extend_schema(**RobotViewSchema.get) def get(self, request, format=None): """ Respond with Nickname, pubKey, privKey. """ user = request.user context = {} context["nickname"] = user.username context["hash_id"] = user.robot.hash_id context["public_key"] = user.robot.public_key context["encrypted_private_key"] = user.robot.encrypted_private_key context["earned_rewards"] = user.robot.earned_rewards context["wants_stealth"] = user.robot.wants_stealth context["last_login"] = user.last_login # Adds/generate telegram token and whether it is enabled context = {**context, **Telegram.get_context(user)} # return active order or last made order if any has_no_active_order, _, order = Logics.validate_already_maker_or_taker( request.user ) if not has_no_active_order: context["active_order_id"] = order.id else: last_order = Order.objects.filter( Q(maker=request.user) | Q(taker=request.user) ).last() if last_order: context["last_order_id"] = last_order.id # Robot was found, only if created +5 mins ago if user.date_joined < (timezone.now() - timedelta(minutes=5)): context["found"] = True return Response(context, status=status.HTTP_200_OK) class BookView(ListAPIView): serializer_class = OrderPublicSerializer queryset = Order.objects.filter(status=Order.Status.PUB) @extend_schema(**BookViewSchema.get) def get(self, request, format=None): currency = request.GET.get("currency", 0) type = request.GET.get("type", 2) queryset = Order.objects.filter(status=Order.Status.PUB) # Currency 0 and type 2 are special cases treated as "ANY". (These are not really possible choices) if int(currency) == 0 and int(type) != 2: queryset = Order.objects.filter(type=type, status=Order.Status.PUB) elif int(type) == 2 and int(currency) != 0: queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB) elif not (int(currency) == 0 and int(type) == 2): queryset = Order.objects.filter( currency=currency, type=type, status=Order.Status.PUB ) if len(queryset) == 0: return Response( {"not_found": "No orders found, be the first to make one"}, status=status.HTTP_404_NOT_FOUND, ) book_data = [] for order in queryset: data = ListOrderSerializer(order).data data["maker_nick"] = str(order.maker) data["maker_hash_id"] = str(order.maker.robot.hash_id) data["satoshis_now"] = Logics.satoshis_now(order) # Compute current premium for those orders that are explicitly priced. price, premium = Logics.price_and_premium_now(order) data["price"], data["premium"] = price, str(premium) data["maker_status"] = Logics.user_activity_status(order.maker.last_login) for key in ( "status", "taker", ): # Non participants should not see the status or who is the taker del data[key] book_data.append(data) return Response(book_data, status=status.HTTP_200_OK) class InfoView(viewsets.ViewSet): serializer_class = InfoSerializer @extend_schema(**InfoViewSchema.get) def get(self, request): 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.SELL, status=Order.Status.PUB) ) context["book_liquidity"] = Order.objects.filter( status=Order.Status.PUB ).aggregate(Sum("last_satoshis"))["last_satoshis__sum"] context["book_liquidity"] = ( 0 if context["book_liquidity"] is None else context["book_liquidity"] ) # Number of active users (logged in in last 30 minutes) today = datetime.today() context["active_robots_today"] = len( User.objects.filter(last_login__day=today.day) ) # Compute average premium and volume of today last_day = timezone.now() - timedelta(days=1) queryset = MarketTick.objects.filter(timestamp__gt=last_day) if not len(queryset) == 0: avg_premium, total_volume = compute_avg_premium(queryset) # If no contracts, fallback to lifetime avg premium else: queryset = MarketTick.objects.all() avg_premium, _ = compute_avg_premium(queryset) total_volume = 0 queryset = MarketTick.objects.all() if not len(queryset) == 0: volume_contracted = [] for tick in queryset: volume_contracted.append(tick.volume if tick.volume else 0) lifetime_volume = sum(volume_contracted) else: lifetime_volume = 0 context["last_day_nonkyc_btc_premium"] = round(avg_premium, 2) context["last_day_volume"] = round(total_volume, 8) context["lifetime_volume"] = round(lifetime_volume, 8) context["lnd_version"] = get_lnd_version() context["cln_version"] = get_cln_version() context["robosats_running_commit_hash"] = get_robosats_commit() context["version"] = settings.VERSION context["alternative_site"] = config("ALTERNATIVE_SITE") context["alternative_name"] = config("ALTERNATIVE_NAME") context["node_alias"] = config("NODE_ALIAS") context["node_id"] = config("NODE_ID") context["network"] = config("NETWORK", cast=str, default="mainnet") context["maker_fee"] = float(config("FEE")) * float(config("MAKER_FEE_SPLIT")) context["maker_fee"] = float(config("FEE")) * float(config("MAKER_FEE_SPLIT")) context["taker_fee"] = float(config("FEE")) * ( 1 - float(config("MAKER_FEE_SPLIT")) ) context["bond_size"] = settings.DEFAULT_BOND_SIZE context["notice_severity"] = config("NOTICE_SEVERITY", cast=str, default="none") context["notice_message"] = config("NOTICE_MESSAGE", cast=str, default="") context["min_order_size"] = config("MIN_ORDER_SIZE", cast=int, default=20000) context["max_order_size"] = config("MAX_ORDER_SIZE", cast=int, default=250000) context["swap_enabled"] = not config("DISABLE_ONCHAIN", cast=bool, default=True) context["max_swap"] = config("MAX_SWAP_AMOUNT", cast=int, default=0) try: context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate( BalanceLog.objects.latest("time") ) except BalanceLog.DoesNotExist: context["current_swap_fee_rate"] = 0 return Response(context, status.HTTP_200_OK) class RewardView(CreateAPIView): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] serializer_class = ClaimRewardSerializer @extend_schema(**RewardViewSchema.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) pgp_invoice = serializer.data.get("invoice") valid_signature, invoice = verify_signed_message( request.user.robot.public_key, pgp_invoice ) if not valid_signature: return Response( {"bad_request": "The PGP signed cleartext message is not valid."}, status.HTTP_400_BAD_REQUEST, ) valid, context = Logics.withdraw_rewards(request.user, invoice) if not valid: context["successful_withdrawal"] = False return Response(context, status.HTTP_400_BAD_REQUEST) return Response({"successful_withdrawal": True}, status.HTTP_200_OK) class PriceView(ListAPIView): serializer_class = PriceSerializer @extend_schema(**PriceViewSchema.get) def get(self, request): payload = {} queryset = Currency.objects.all().order_by("currency") for currency in queryset: code = Currency.currency_dict[str(currency.currency)] try: last_tick = MarketTick.objects.filter(currency=currency).latest( "timestamp" ) payload[code] = { "price": last_tick.price, "volume": last_tick.volume, "premium": last_tick.premium, "timestamp": last_tick.timestamp, } except Exception: payload[code] = None return Response(payload, status.HTTP_200_OK) class TickView(ListAPIView): queryset = MarketTick.objects.all() serializer_class = TickSerializer @extend_schema(**TickViewSchema.get) def get(self, request): start_date_str = request.query_params.get("start") end_date_str = request.query_params.get("end") # Perform the query with date range filtering try: if start_date_str: naive_start_date = datetime.strptime(start_date_str, "%d-%m-%Y") aware_start_date = timezone.make_aware( naive_start_date, timezone=timezone.get_current_timezone() ) self.queryset = self.queryset.filter(timestamp__gte=aware_start_date) if end_date_str: naive_end_date = datetime.strptime(end_date_str, "%d-%m-%Y") aware_end_date = timezone.make_aware( naive_end_date, timezone=timezone.get_current_timezone() ) self.queryset = self.queryset.filter(timestamp__lte=aware_end_date) except ValueError: return Response( {"bad_request": "Invalid date format"}, status=status.HTTP_400_BAD_REQUEST, ) # Check if the number of ticks exceeds the limit if self.queryset.count() > 5000: return Response( { "bad_request": "More than 5000 market ticks have been found. Please, narrow the date range" }, status=status.HTTP_400_BAD_REQUEST, ) data = self.serializer_class(self.queryset, many=True, read_only=True).data return Response(data, status=status.HTTP_200_OK) class LimitView(ListAPIView): @extend_schema(**LimitViewSchema.get) def get(self, request): # Trade limits as BTC min_trade = config("MIN_ORDER_SIZE", cast=int, default=20_000) / 100_000_000 max_trade = config("MAX_ORDER_SIZE", cast=int, default=500_000) / 100_000_000 payload = {} queryset = Currency.objects.all().order_by("currency") for currency in queryset: code = Currency.currency_dict[str(currency.currency)] exchange_rate = float(currency.exchange_rate) payload[currency.currency] = { "code": code, "price": exchange_rate, "min_amount": min_trade * exchange_rate, "max_amount": max_trade * exchange_rate, } return Response(payload, status.HTTP_200_OK) class HistoricalView(ListAPIView): @extend_schema(**HistoricalViewSchema.get) def get(self, request): payload = {} queryset = AccountingDay.objects.all().order_by("day") for accounting_day in queryset: payload[str(accounting_day.day)] = { "volume": accounting_day.contracted, "num_contracts": accounting_day.num_contracts, } return Response(payload, status.HTTP_200_OK) class StealthView(APIView): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] serializer_class = StealthSerializer @extend_schema(**StealthViewSchema.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) stealth = serializer.data.get("wantsStealth") request.user.robot.wants_stealth = stealth request.user.robot.save(update_fields=["wants_stealth"]) return Response({"wantsStealth": stealth})