robosats/api/views.py

787 lines
32 KiB
Python
Raw Normal View History

2022-02-07 16:37:16 +03:00
import os
from re import T
from django.db.models import Sum
2022-01-07 00:36:22 +03:00
from rest_framework import status, viewsets
from rest_framework.generics import CreateAPIView, ListAPIView
from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer
from api.models import LNPayment, MarketTick, Order, Currency, Profile
from api.logics import Logics
from api.messages import Telegram
from secrets import token_urlsafe
from api.utils import get_lnd_version, get_commit_robosats, compute_premium_percentile, compute_avg_premium
2022-01-02 01:13:27 +03:00
from .nick_generator.nick_generator import NickGenerator
from robohash import Robohash
from scipy.stats import entropy
from math import log2
import numpy as np
import hashlib
from pathlib import Path
from datetime import timedelta, datetime
from django.utils import timezone
2022-02-12 18:46:58 +03:00
from django.conf import settings
2022-01-06 19:54:37 +03:00
from decouple import config
2022-02-17 22:50:10 +03:00
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
RETRY_TIME = int(config("RETRY_TIME"))
2022-03-19 01:09:38 +03:00
PUBLIC_DURATION = 60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1
BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
2022-02-07 16:37:16 +03:00
2022-02-12 18:46:58 +03:00
avatar_path = Path(settings.AVATAR_ROOT)
avatar_path.mkdir(parents=True, exist_ok=True)
2022-01-02 01:13:27 +03:00
# Create your views here.
2022-02-17 22:50:10 +03:00
class MakerView(CreateAPIView):
2022-02-17 22:50:10 +03:00
serializer_class = MakeOrderSerializer
2022-02-17 22:50:10 +03:00
def post(self, request):
serializer = self.serializer_class(data=request.data)
if not request.user.is_authenticated:
2022-02-17 22:50:10 +03:00
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)
2022-02-25 23:08:22 +03:00
# 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":
"Woah! RoboSats' book is at full capacity! Try again later"
},
status.HTTP_400_BAD_REQUEST,
)
2022-02-17 22:50:10 +03:00
type = serializer.data.get("type")
currency = serializer.data.get("currency")
amount = serializer.data.get("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")
2022-03-19 00:21:13 +03:00
public_duration = serializer.data.get("public_duration")
2022-03-19 01:09:38 +03:00
bond_size = serializer.data.get("bond_size")
# Optional params
if public_duration == None:
public_duration = PUBLIC_DURATION
if bond_size == None:
bond_size = BOND_SIZE
2022-02-17 22:50:10 +03:00
valid, context, _ = Logics.validate_already_maker_or_taker(
request.user)
if not valid:
return Response(context, status.HTTP_409_CONFLICT)
2022-01-06 23:33:40 +03:00
# Creates a new order
order = Order(
type=type,
currency=Currency.objects.get(id=currency),
2022-01-06 23:33:40 +03:00
amount=amount,
payment_method=payment_method,
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
2022-02-17 22:50:10 +03:00
expires_at=timezone.now() + timedelta(
2022-03-19 01:09:38 +03:00
seconds=EXP_MAKER_BOND_INVOICE),
2022-02-17 22:50:10 +03:00
maker=request.user,
2022-03-19 00:21:13 +03:00
public_duration=public_duration,
2022-03-19 01:09:38 +03:00
bond_size=bond_size,
2022-02-17 22:50:10 +03:00
)
2022-01-07 00:36:22 +03:00
# TODO move to Order class method when new instance is created!
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
2022-01-06 23:33:40 +03:00
2022-01-07 00:36:22 +03:00
valid, context = Logics.validate_order_size(order)
2022-02-17 22:50:10 +03:00
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-01-06 23:33:40 +03:00
order.save()
2022-02-17 22:50:10 +03:00
return Response(ListOrderSerializer(order).data,
status=status.HTTP_201_CREATED)
class OrderView(viewsets.ViewSet):
2022-01-06 23:33:40 +03:00
serializer_class = UpdateOrderSerializer
2022-02-17 22:50:10 +03:00
lookup_url_kwarg = "order_id"
def get(self, request, format=None):
2022-02-17 22:50:10 +03:00
"""
Full trade pipeline takes place while looking/refreshing the order page.
2022-02-17 22:50:10 +03:00
"""
order_id = request.GET.get(self.lookup_url_kwarg)
2022-01-28 17:30:45 +03:00
if not request.user.is_authenticated:
2022-02-17 22:50:10 +03:00
return Response(
{
"bad_request":
"You must have a robot avatar to see the order details"
},
status=status.HTTP_400_BAD_REQUEST,
)
2022-01-28 17:30:45 +03:00
if order_id == None:
2022-02-17 22:50:10 +03:00
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
2022-02-17 22:50:10 +03:00
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:
2022-02-17 22:50:10 +03:00
return Response(
{"bad_request": "This order has been cancelled by the maker"},
status.HTTP_400_BAD_REQUEST,
)
if order.status == Order.Status.CCA:
2022-02-17 22:50:10 +03:00
return Response(
{
"bad_request":
"This order has been cancelled collaborativelly"
},
status.HTTP_400_BAD_REQUEST,
)
data = ListOrderSerializer(order).data
2022-03-19 00:21:13 +03:00
data["total_secs_exp"] = order.t_to_expire(order.status)
# if user is under a limit (penalty), inform him.
2022-01-10 15:10:32 +03:00
is_penalized, time_out = Logics.is_penalized(request.user)
if is_penalized:
2022-02-17 22:50:10 +03:00
data["penalty"] = request.user.profile.penalty_expiration
2022-01-10 15:10:32 +03:00
# Add booleans if user is maker, taker, partipant, buyer or seller
2022-02-17 22:50:10 +03:00
data["is_maker"] = order.maker == request.user
data["is_taker"] = order.taker == request.user
data["is_participant"] = data["is_maker"] or data["is_taker"]
2022-01-14 15:00:53 +03:00
# 3.a) If not a participant and order is not public, forbid.
2022-02-17 22:50:10 +03:00
if not data["is_participant"] and order.status != Order.Status.PUB:
return Response(
{"bad_request": "You are not allowed to see this order"},
status.HTTP_403_FORBIDDEN,
)
# WRITE Update last_seen for maker and taker.
# Note down that the taker/maker was here recently, so counterpart knows if the user is paying attention.
if order.maker == request.user:
order.maker_last_seen = timezone.now()
order.save()
if order.taker == request.user:
order.taker_last_seen = timezone.now()
order.save()
# Add activity status of participants based on last_seen
if order.taker_last_seen != None:
2022-02-17 22:50:10 +03:00
data["taker_status"] = Logics.user_activity_status(
order.taker_last_seen)
if order.maker_last_seen != None:
2022-02-17 22:50:10 +03:00
data["maker_status"] = Logics.user_activity_status(
order.maker_last_seen)
2022-01-14 17:19:25 +03:00
# 3.b If order is between public and WF2
2022-01-18 03:50:54 +03:00
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
data["price_now"], data["premium_now"] = Logics.price_and_premium_now(order)
2022-02-17 22:50:10 +03:00
# 3. c) If maker and Public, add num robots in book, premium percentile
# num similar orders, and maker information to enable telegram notifications.
2022-02-17 22:50:10 +03:00
if data["is_maker"] and order.status == Order.Status.PUB:
data["premium_percentile"] = compute_premium_percentile(order)
data["num_similar_orders"] = len(
Order.objects.filter(currency=order.currency,
status=Order.Status.PUB))
# Adds/generate telegram token and whether it is enabled
data = {**data,**Telegram.get_context(request.user)}
2022-01-14 15:00:53 +03:00
# 4) Non participants can view details (but only if PUB)
2022-02-17 22:50:10 +03:00
elif not data["is_participant"] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
2022-01-18 03:50:54 +03:00
# For participants add positions, nicks and status as a message and hold invoices status
2022-02-17 22:50:10 +03:00
data["is_buyer"] = Logics.is_buyer(order, request.user)
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["is_fiat_sent"] = order.is_fiat_sent
data["is_disputed"] = order.is_disputed
data["ur_nick"] = request.user.username
2022-01-18 03:50:54 +03:00
# Add whether hold invoices are LOCKED (ACCEPTED)
# Is there a maker bond? If so, True if locked, False otherwise
if order.maker_bond:
2022-02-17 22:50:10 +03:00
data[
"maker_locked"] = order.maker_bond.status == LNPayment.Status.LOCKED
2022-01-18 03:50:54 +03:00
else:
2022-02-17 22:50:10 +03:00
data["maker_locked"] = False
2022-01-18 03:50:54 +03:00
# Is there a taker bond? If so, True if locked, False otherwise
if order.taker_bond:
2022-02-17 22:50:10 +03:00
data[
"taker_locked"] = order.taker_bond.status == LNPayment.Status.LOCKED
2022-01-18 03:50:54 +03:00
else:
2022-02-17 22:50:10 +03:00
data["taker_locked"] = False
2022-01-18 03:50:54 +03:00
# Is there an escrow? If so, True if locked, False otherwise
if order.trade_escrow:
2022-02-17 22:50:10 +03:00
data[
"escrow_locked"] = order.trade_escrow.status == LNPayment.Status.LOCKED
2022-01-18 03:50:54 +03:00
else:
2022-02-17 22:50:10 +03:00
data["escrow_locked"] = False
2022-01-18 03:50:54 +03:00
# If both bonds are locked, participants can see the final trade amount in sats.
2022-01-18 18:45:04 +03:00
if order.taker_bond:
2022-02-17 22:50:10 +03:00
if (order.maker_bond.status == order.taker_bond.status ==
LNPayment.Status.LOCKED):
2022-01-18 18:45:04 +03:00
# Seller sees the amount he sends
2022-02-17 22:50:10 +03:00
if data["is_seller"]:
data["trade_satoshis"] = Logics.escrow_amount(
order, request.user)[1]["escrow_amount"]
2022-01-18 18:45:04 +03:00
# Buyer sees the amount he receives
2022-02-17 22:50:10 +03:00
elif data["is_buyer"]:
data["trade_satoshis"] = Logics.payout_amount(
order, request.user)[1]["invoice_amount"]
2022-01-09 23:05:19 +03:00
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
2022-02-17 22:50:10 +03:00
if order.status == Order.Status.WFB and data["is_maker"]:
2022-01-09 23:05:19 +03:00
valid, context = Logics.gen_maker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-02-17 22:50:10 +03:00
2022-01-09 23:05:19 +03:00
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice.
2022-02-17 22:50:10 +03:00
elif order.status == Order.Status.TAK and data["is_taker"]:
2022-01-09 23:05:19 +03:00
valid, context = Logics.gen_taker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-02-17 22:50:10 +03:00
# 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):
2022-01-09 23:05:19 +03:00
# If the two bonds are locked, reply with an ESCROW hold invoice.
2022-02-17 22:50:10 +03:00
if (order.maker_bond.status == order.taker_bond.status ==
LNPayment.Status.LOCKED):
valid, context = Logics.gen_escrow_hold_invoice(
order, request.user)
2022-01-08 20:19:30 +03:00
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-02-17 22:50:10 +03:00
# 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):
2022-01-08 20:19:30 +03:00
# If the two bonds are locked, reply with an AMOUNT so he can send the buyer invoice.
2022-02-17 22:50:10 +03:00
if (order.maker_bond.status == order.taker_bond.status ==
LNPayment.Status.LOCKED):
valid, context = Logics.payout_amount(order, request.user)
2022-01-08 20:19:30 +03:00
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
2022-02-17 22:50:10 +03:00
elif order.status in [
Order.Status.WFI, Order.Status.CHA, Order.Status.FSE
]:
# If all bonds are locked.
2022-02-17 22:50:10 +03:00
if (order.maker_bond.status == order.taker_bond.status ==
order.trade_escrow.status == LNPayment.Status.LOCKED):
2022-01-23 22:02:25 +03:00
# add whether a collaborative cancel is pending or has been asked
2022-02-17 22:50:10 +03:00
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
2022-01-23 22:02:25 +03:00
else:
2022-02-17 22:50:10 +03:00
data["asked_for_cancel"] = False
2022-01-10 00:24:48 +03:00
# 9) If status is 'DIS' and all HTLCS are in LOCKED
2022-01-23 22:02:25 +03:00
elif order.status == Order.Status.DIS:
# add whether the dispute statement has been received
2022-02-17 22:50:10 +03:00
if data["is_maker"]:
data["statement_submitted"] = (order.maker_statement != None
and order.maker_statement != "")
elif data["is_taker"]:
data["statement_submitted"] = (order.taker_statement != 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.
2022-02-17 22:50:10 +03:00
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.status == LNPayment.Status.EXPIRE:
2022-02-17 22:50:10 +03:00
data["invoice_expired"] = True
# Add invoice amount once again if invoice was expired.
data["invoice_amount"] = Logics.payout_amount(order,request.user)[1]["invoice_amount"]
2022-02-17 22:50:10 +03:00
return Response(data, status.HTTP_200_OK)
2022-01-06 23:33:40 +03:00
def take_update_confirm_dispute_cancel(self, request, format=None):
2022-02-17 22:50:10 +03:00
"""
Here takes place all of the updates to the order object.
2022-01-07 01:39:59 +03:00
That is: take, confim, cancel, dispute, update_invoice or rate.
2022-02-17 22:50:10 +03:00
"""
order_id = request.GET.get(self.lookup_url_kwarg)
2022-01-06 23:33:40 +03:00
serializer = UpdateOrderSerializer(data=request.data)
2022-02-17 22:50:10 +03:00
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
order = Order.objects.get(id=order_id)
2022-02-17 22:50:10 +03:00
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
# 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform'
2022-02-17 22:50:10 +03:00
action = serializer.data.get("action")
invoice = serializer.data.get("invoice")
statement = serializer.data.get("statement")
rating = serializer.data.get("rating")
# 1) If action is take, it is a taker request!
2022-02-17 22:50:10 +03:00
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)
2022-01-10 15:10:32 +03:00
valid, context = Logics.take(order, request.user)
2022-02-17 22:50:10 +03:00
if not valid:
return Response(context, status=status.HTTP_403_FORBIDDEN)
2022-01-10 15:10:32 +03:00
2022-01-14 17:19:25 +03:00
return self.get(request)
2022-02-17 22:50:10 +03:00
else:
Response(
{"bad_request": "This order is not public anymore."},
status.HTTP_400_BAD_REQUEST,
)
2022-01-06 23:33:40 +03:00
# Any other action is only allowed if the user is a participant
if not (order.maker == request.user or order.taker == request.user):
2022-02-17 22:50:10 +03:00
return Response(
{"bad_request": "You are not a participant in this order"},
status.HTTP_403_FORBIDDEN,
)
# 2) If action is 'update invoice'
2022-02-17 22:50:10 +03:00
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)
2022-01-06 23:33:40 +03:00
# 3) If action is cancel
2022-02-17 22:50:10 +03:00
elif action == "cancel":
valid, context = Logics.cancel_order(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-01-06 23:33:40 +03:00
# 4) If action is confirm
2022-02-17 22:50:10 +03:00
elif action == "confirm":
valid, context = Logics.confirm_fiat(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-01-06 23:33:40 +03:00
# 5) If action is dispute
2022-02-17 22:50:10 +03:00
elif action == "dispute":
valid, context = Logics.open_dispute(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-02-17 22:50:10 +03:00
elif action == "submit_statement":
valid, context = Logics.dispute_statement(order, request.user,
statement)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-01-06 23:33:40 +03:00
# 6) If action is rate
2022-02-17 22:50:10 +03:00
elif action == "rate_user" and rating:
valid, context = Logics.rate_counterparty(order, request.user,
rating)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-01-06 23:33:40 +03:00
# 6) If action is rate_platform
2022-02-17 22:50:10 +03:00
elif action == "rate_platform" and rating:
valid, context = Logics.rate_platform(request.user, rating)
2022-02-17 22:50:10 +03:00
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(
2022-02-17 22:50:10 +03:00
{
"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)
2022-02-17 22:50:10 +03:00
class UserView(APIView):
2022-02-17 22:50:10 +03:00
NickGen = NickGenerator(lang="English",
use_adv=False,
use_adj=True,
use_noun=True,
max_num=999)
# Probably should be turned into a post method
2022-02-17 22:50:10 +03:00
def get(self, request, format=None):
"""
Get a new user derived from a high entropy token
2022-02-17 22:50:10 +03:00
- Request has a high-entropy token,
- Generates new nickname and avatar.
- Creates login credentials (new User object)
Response with Avatar and Nickname.
2022-02-17 22:50:10 +03:00
"""
# If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
if request.user.is_authenticated:
2022-02-17 22:50:10 +03:00
context = {"nickname": request.user.username}
not_participant, _, _ = Logics.validate_already_maker_or_taker(
request.user)
# Does not allow this 'mistake' if an active order
if not not_participant:
2022-02-17 22:50:10 +03:00
context[
"bad_request"] = f"You are already logged in as {request.user} and have an active order"
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-02-17 22:50:10 +03:00
token = request.GET.get("token")
ref_code = request.GET.get("ref_code")
# Compute token entropy
value, counts = np.unique(list(token), return_counts=True)
shannon_entropy = entropy(counts, base=62)
bits_entropy = log2(len(value)**len(token))
# Payload
2022-02-17 22:50:10 +03:00
context = {
"token_shannon_entropy": shannon_entropy,
"token_bits_entropy": bits_entropy,
}
# Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity
if bits_entropy < 128 or shannon_entropy < 0.7:
2022-02-17 22:50:10 +03:00
context["bad_request"] = "The token does not have enough entropy"
return Response(context, status=status.HTTP_400_BAD_REQUEST)
2022-01-18 20:52:48 +03:00
# Hash the token, only 1 iteration.
2022-02-17 22:50:10 +03:00
hash = hashlib.sha256(str.encode(token)).hexdigest()
2022-01-18 20:52:48 +03:00
# Generate nickname deterministically
2022-02-17 22:50:10 +03:00
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
context["nickname"] = nickname
# Generate avatar
rh = Robohash(hash)
2022-02-17 22:50:10 +03:00
rh.assemble(roboset="set1", bgset="any") # for backgrounds ON
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
2022-02-17 22:50:10 +03:00
image_path = avatar_path.joinpath(nickname + ".png")
if not image_path.exists():
with open(image_path, "wb") as f:
rh.img.save(f, format="png")
2022-01-18 20:52:48 +03:00
# Create new credentials and login if nickname is new
if len(User.objects.filter(username=nickname)) == 0:
2022-02-17 22:50:10 +03:00
User.objects.create_user(username=nickname,
password=token,
is_staff=False)
user = authenticate(request, username=nickname, password=token)
login(request, user)
context['referral_code'] = token_urlsafe(8)
user.profile.referral_code = context['referral_code']
user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
2022-03-06 14:45:06 +03:00
# If the ref_code was created by another robot, this robot was referred.
queryset = Profile.objects.filter(referral_code=ref_code)
if len(queryset) == 1:
user.profile.is_referred = True
2022-03-06 14:45:06 +03:00
user.profile.referred_by = queryset[0]
user.profile.save()
return Response(context, status=status.HTTP_201_CREATED)
else:
user = authenticate(request, username=nickname, password=token)
if user is not None:
login(request, user)
# Sends the welcome back message, only if created +3 mins ago
2022-02-17 22:50:10 +03:00
if request.user.date_joined < (timezone.now() -
timedelta(minutes=3)):
context["found"] = "We found your Robot avatar. Welcome back!"
return Response(context, status=status.HTTP_202_ACCEPTED)
else:
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
2022-02-17 22:50:10 +03:00
context["found"] = "Bad luck, this nickname is taken"
context["bad_request"] = "Enter a different token"
return Response(context, status.HTTP_403_FORBIDDEN)
2022-02-17 22:50:10 +03:00
def delete(self, request):
"""Pressing "give me another" deletes the logged in user"""
user = request.user
if not user.is_authenticated:
return Response(status.HTTP_403_FORBIDDEN)
# Only delete if user life is shorter than 30 minutes. Helps to avoid deleting users by mistake
if user.date_joined < (timezone.now() - timedelta(minutes=30)):
return Response(status.HTTP_400_BAD_REQUEST)
# Check if it is not a maker or taker!
not_participant, _, _ = Logics.validate_already_maker_or_taker(user)
if not not_participant:
2022-02-17 22:50:10 +03:00
return Response(
{
"bad_request":
"Maybe a mistake? User cannot be deleted while he is part of an order"
},
status.HTTP_400_BAD_REQUEST,
)
# Check if has already a profile with
if user.profile.total_contracts > 0:
2022-02-17 22:50:10 +03:00
return Response(
{
"bad_request":
"Maybe a mistake? User cannot be deleted as it has completed trades"
},
status.HTTP_400_BAD_REQUEST,
)
logout(request)
user.delete()
2022-02-17 22:50:10 +03:00
return Response(
{"user_deleted": "User deleted permanently"},
status.HTTP_301_MOVED_PERMANENTLY,
)
class BookView(ListAPIView):
serializer_class = ListOrderSerializer
2022-02-17 22:50:10 +03:00
queryset = Order.objects.filter(status=Order.Status.PUB)
def get(self, request, format=None):
currency = request.GET.get("currency")
type = request.GET.get("type")
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:
2022-02-17 22:50:10 +03:00
queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
elif int(type) == 2 and int(currency) != 0:
2022-02-17 22:50:10 +03:00
queryset = Order.objects.filter(currency=currency,
status=Order.Status.PUB)
elif not (int(currency) == 0 and int(type) == 2):
2022-02-17 22:50:10 +03:00
queryset = Order.objects.filter(currency=currency,
type=type,
status=Order.Status.PUB)
2022-01-10 00:24:48 +03:00
2022-02-17 22:50:10 +03:00
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
2022-02-17 22:50:10 +03:00
data["maker_nick"] = str(order.maker)
2022-01-10 04:12:58 +03:00
# Compute current premium for those orders that are explicitly priced.
2022-02-17 22:50:10 +03:00
data["price"], data["premium"] = Logics.price_and_premium_now(
order)
data["maker_status"] = Logics.user_activity_status(
order.maker_last_seen)
for key in (
"status",
"taker",
): # Non participants should not see the status or who is the taker
del data[key]
2022-02-17 22:50:10 +03:00
book_data.append(data)
2022-02-17 22:50:10 +03:00
return Response(book_data, status=status.HTTP_200_OK)
class InfoView(ListAPIView):
2022-01-09 04:23:13 +03:00
def get(self, request):
context = {}
2022-02-17 22:50:10 +03:00
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"] == None else context["book_liquidity"]
# Number of active users (logged in in last 30 minutes)
today = datetime.today()
2022-02-17 22:50:10 +03:00
context["active_robots_today"] = len(
User.objects.filter(last_login__day=today.day))
2022-01-18 20:52:48 +03:00
# Compute average premium and volume of today
2022-03-13 15:00:21 +03:00
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()
2022-03-13 15:00:21 +03:00
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)
lifetime_volume = sum(volume_contracted)
else:
lifetime_volume = 0
2022-03-13 15:00:21 +03:00
context["last_day_nonkyc_btc_premium"] = round(avg_premium, 2)
context["last_day_volume"] = total_volume *100000000
context["lifetime_volume"] = lifetime_volume
2022-02-17 22:50:10 +03:00
context["lnd_version"] = get_lnd_version()
context["robosats_running_commit_hash"] = get_commit_robosats()
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")
context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT"))
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
2022-03-19 01:09:38 +03:00
context["bond_size"] = float(config("DEFAULT_BOND_SIZE"))
if request.user.is_authenticated:
2022-02-17 22:50:10 +03:00
context["nickname"] = request.user.username
context["referral_code"] = str(request.user.profile.referral_code)
context["earned_rewards"] = request.user.profile.earned_rewards
2022-02-17 22:50:10 +03:00
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user)
if not has_no_active_order:
2022-02-17 22:50:10 +03:00
context["active_order_id"] = order.id
return Response(context, status.HTTP_200_OK)
class RewardView(CreateAPIView):
serializer_class = ClaimRewardSerializer
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)
invoice = serializer.data.get("invoice")
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(CreateAPIView):
serializer_class = PriceSerializer
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:
payload[code] = None
2022-03-21 02:46:36 +03:00
return Response(payload, status.HTTP_200_OK)
class LimitView(ListAPIView):
def get(self, request):
# Trade limits as BTC
min_trade = float(config('MIN_TRADE')) / 100000000
max_trade = float(config('MAX_TRADE')) / 100000000
max_bondless_trade = float(config('MAX_TRADE_BONDLESS_TAKER')) / 100000000
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,
'min_amount': min_trade * exchange_rate,
'max_amount': max_trade * exchange_rate,
'max_bondless_amount': max_bondless_trade * exchange_rate,
}
return Response(payload, status.HTTP_200_OK)