robosats/api/views.py

1174 lines
46 KiB
Python
Raw Normal View History

2022-10-25 21:04:12 +03:00
import hashlib
from datetime import datetime, timedelta
from math import log2
from pathlib import Path
from decouple import config
from django.conf import settings
from django.contrib.auth import authenticate, login, logout
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
2022-01-07 00:36:22 +03:00
from rest_framework import status, viewsets
from rest_framework.authentication import (
SessionAuthentication, # DEPRECATE session authentication
)
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
2022-10-25 21:04:12 +03:00
from rest_framework.views import APIView
from robohash import Robohash
from scipy.stats import entropy
2022-10-25 21:04:12 +03:00
from api.logics import Logics
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
from api.notifications import Telegram
2022-10-20 12:56:10 +03:00
from api.oas_schemas import (
BookViewSchema,
HistoricalViewSchema,
InfoViewSchema,
LimitViewSchema,
MakerViewSchema,
OrderViewSchema,
PriceViewSchema,
RewardViewSchema,
RobotViewSchema,
2022-10-20 12:56:10 +03:00
StealthViewSchema,
TickViewSchema,
UserViewSchema,
)
from api.serializers import (
2022-10-25 21:04:12 +03:00
ClaimRewardSerializer,
2022-10-20 12:56:10 +03:00
InfoSerializer,
ListOrderSerializer,
MakeOrderSerializer,
OrderPublicSerializer,
PriceSerializer,
StealthSerializer,
2022-10-25 21:04:12 +03:00
TickSerializer,
UpdateOrderSerializer,
UserGenSerializer,
2022-10-20 12:56:10 +03:00
)
from api.utils import (
2022-10-25 21:04:12 +03:00
compute_avg_premium,
compute_premium_percentile,
Add core-lightning as backend lightning node vendor (#611) * Add CLN node backend image and service (#418) * Add cln service * Add hodlvoice Dockerfile and entrypoint * Add lnnode vendor switch (#431) * Add LNNode vendor switch * Add CLN version to frontend and other fixes * init * first draft * add unsettled_local_balance and unsettled_remote_balance * gen_hold_invoice now takes 3 more variables to build a label for cln * remove unneeded payment_hash from gen_hold_invoice * remove comment * add get_cln_version * first draft of clns follow_send_payment * fix name of get_lnd_version * enable flake8 * flake8 fixes * renaming cln file, class and get_version * remove lnd specific commented code * get_version: add try/except, refactor to top to mimic lnd.py * rename htlc_cltv to htlc_expiry * add clns lookup_invoice_status * refactored double_check_htlc_is_settled to the end to match lnds file * fix generate_rpc * Add sample environmental variables, small fixes * Fix CLN gRPC port * Fix gen_hold_invoice, plus some other tiny fixes (#435) * Fix channel_balance to use int object inside Amount (#438) * Add CLN/LND volume to celery-beat service * Add CLN/LND volume to celery-beat service * Bump CLN to v23.05 * changes for 0.5 and some small fixes * change invoice expiry from absolute to relative duration * add try/except to catch timeout error * fix failure_reason to be ln_payment failure reasons, albeit inaccurate sometimes * refactor follow_send_payment and add pending check to expired case * fix status comments * add send_keysend method * fix wrong state ints in cancel and settle * switch to use hodlinvoicelookup in double_check * move pay command after lnpayment status update * remove loop in follow_send_payment and add error result for edge case * fix typeerror for payment_hash * rework follow_send_payment logic and payment_hash, watch harder if pending * use fully qualified names for status instead of raw int * missed 2 status from prev commit * Always copy the cln-grpc-hodl plugin on start up * Fix ALLOW_SELF_KEYSEND linting error * Fix missing definition of failure_reason --------- Co-authored-by: daywalker90 <admin@noserver4u.de>
2023-05-22 17:56:15 +03:00
get_cln_version,
2022-10-20 12:56:10 +03:00
get_lnd_version,
get_robosats_commit,
validate_pgp_keys,
verify_signed_message,
2022-10-20 12:56:10 +03:00
)
Add turtle chat component Squashed commit of the following: commit f60870fcfe574dc4ab1343e25241b6ef7cc2721b Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Nov 10 10:30:42 2022 -0800 Fix internal error when entering chat commit f1eeb49f2a86575eb2e85cdff20460276e71b806 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 10:08:22 2022 -0800 Fix final serializer commit d0c08ba6ad4378a9539c0be83b6f4f8b958b532e Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 09:44:57 2022 -0800 Chat API changes commit a66bf64edc06d936612db6bf75476b54e6a84334 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 09:28:29 2022 -0800 Fix param on post commit 60b18d13c2ec625497323371a2a6f64c9c911e47 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 08:56:25 2022 -0800 Fix serializer commit 11212d30eeffde37e07d2e6e5c1fb36df46916ad Author: KoalaSat <yv1vtrul@duck.com> Date: Sun Nov 6 21:07:18 2022 +0100 CR 2 commit c82790cb81dd9919de97c39f2553974784ffe92d Author: KoalaSat <yv1vtrul@duck.com> Date: Sun Nov 6 20:09:18 2022 +0100 Fix commit 605a3b69a1fcf795e45b2acba1e12436f8545f8a Author: KoalaSat <yv1vtrul@duck.com> Date: Sun Nov 6 14:44:42 2022 +0100 CR commit 09776e9c8fa85c253f28c75361829dab5df4d978 Author: KoalaSat <yv1vtrul@duck.com> Date: Wed Nov 2 18:12:29 2022 +0100 translations commit 432e4d23991164b164d2ab3e4f31790a992dc601 Author: KoalaSat <yv1vtrul@duck.com> Date: Wed Nov 2 17:39:02 2022 +0100 Switch and better UX commit df6e476613006f6a861bab68f8a4261bc8f641e0 Author: KoalaSat <yv1vtrul@duck.com> Date: Tue Nov 1 18:20:01 2022 +0100 Unused code commit 5b8d6b4d32980e31bb1d682444b53df1a8e16c47 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Mon Oct 31 09:20:20 2022 -0700 Add Chat Turtle Mode
2022-11-11 12:28:09 +03:00
from chat.models import Message
2022-10-25 21:04:12 +03:00
from control.models import AccountingDay, BalanceLog
2022-01-02 01:13:27 +03:00
from .nick_generator.nick_generator import NickGenerator
2022-01-06 19:54:37 +03:00
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-10-20 12:56:10 +03:00
PUBLIC_DURATION = 60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1
ESCROW_DURATION = 60 * int(config("INVOICE_AND_ESCROW_DURATION"))
2022-03-19 01:09:38 +03:00
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
authentication_classes = [TokenAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated]
@extend_schema(**MakerViewSchema.post)
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(
2022-10-20 12:56:10 +03:00
{"bad_request": "Woops! It seems you do not have a robot avatar"},
2022-02-17 22:50:10 +03:00
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.
2022-10-20 12:56:10 +03:00
if Order.objects.filter(status=Order.Status.PUB).count() >= int(
config("MAX_PUBLIC_ORDERS")
):
2022-02-25 23:08:22 +03:00
return Response(
{
2022-10-20 12:56:10 +03:00
"bad_request": "Woah! RoboSats' book is at full capacity! Try again later"
2022-02-25 23:08:22 +03:00
},
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)
2022-02-25 23:08:22 +03:00
2022-02-17 22:50:10 +03:00
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")
2022-02-17 22:50:10 +03:00
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")
escrow_duration = serializer.data.get("escrow_duration")
2022-03-19 01:09:38 +03:00
bond_size = serializer.data.get("bond_size")
# Optional params
if public_duration is None:
2022-10-20 12:56:10 +03:00
public_duration = PUBLIC_DURATION
if escrow_duration is None:
2022-10-20 12:56:10 +03:00
escrow_duration = ESCROW_DURATION
if bond_size is None:
2022-10-20 12:56:10 +03:00
bond_size = BOND_SIZE
if has_range is None:
2022-10-20 12:56:10 +03:00
has_range = False
# TODO add a check - if `is_explicit` is true then `satoshis` need to be specified
# 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(
{
2022-10-20 12:56:10 +03:00
"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(
2022-10-20 12:56:10 +03:00
{"bad_request": "You must specify an order amount"},
status.HTTP_400_BAD_REQUEST,
)
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,
has_range=has_range,
min_amount=min_amount,
max_amount=max_amount,
2022-01-06 23:33:40 +03:00
payment_method=payment_method,
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
2022-10-20 12:56:10 +03:00
expires_at=timezone.now() + timedelta(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,
escrow_duration=escrow_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
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-10-20 12:56:10 +03:00
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
class OrderView(viewsets.ViewSet):
authentication_classes = [TokenAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated]
2022-01-06 23:33:40 +03:00
serializer_class = UpdateOrderSerializer
2022-02-17 22:50:10 +03:00
lookup_url_kwarg = "order_id"
@extend_schema(**OrderViewSchema.get)
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(
{
2022-10-20 12:56:10 +03:00
"bad_request": "You must have a robot avatar to see the order details"
2022-02-17 22:50:10 +03:00
},
status=status.HTTP_400_BAD_REQUEST,
)
2022-01-28 17:30:45 +03:00
if order_id is 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:
2022-10-20 12:56:10 +03:00
return Response(
{"bad_request": "Invalid Order Id"}, status.HTTP_404_NOT_FOUND
)
2022-02-17 22:50:10 +03:00
# 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(
2022-10-20 12:56:10 +03:00
{"bad_request": "This order has been cancelled collaborativelly"},
2022-02-17 22:50:10 +03:00
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:
data["penalty"] = request.user.robot.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": "This order is not available"},
2022-02-17 22:50:10 +03:00
status.HTTP_403_FORBIDDEN,
)
data["maker_nick"] = str(order.maker)
# 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
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)
data["satoshis_now"] = Logics.satoshis_now(order)
2022-02-17 22:50:10 +03:00
2022-10-20 12:56:10 +03:00
# 4. a) If maker and Public/Paused, add premium percentile
# num similar orders, and maker information to enable telegram notifications.
2022-10-20 12:56:10 +03:00
if data["is_maker"] and order.status in [
Order.Status.PUB,
Order.Status.PAU,
]:
2022-02-17 22:50:10 +03:00
data["premium_percentile"] = compute_premium_percentile(order)
data["num_similar_orders"] = len(
2022-10-20 12:56:10 +03:00
Order.objects.filter(
currency=order.currency, status=Order.Status.PUB
)
)
2022-01-14 15:00:53 +03:00
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["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
data["satoshis_now"] = order.last_satoshis
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:
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:
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:
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-10-20 12:56: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"]:
2022-10-20 12:56:10 +03:00
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"]:
2022-10-20 12:56:10 +03:00
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'
2022-10-20 12:56:10 +03:00
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-10-20 12:56: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'
2022-10-20 12:56:10 +03:00
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.
2022-10-20 12:56: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-10-20 12:56:10 +03:00
elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]:
# If all bonds are locked.
2022-10-20 12:56: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 (
2022-10-20 12:56:10 +03:00
data["is_taker"] and order.maker_asked_cancel
):
2022-02-17 22:50:10 +03:00
data["pending_cancel"] = True
elif (data["is_maker"] and order.maker_asked_cancel) or (
2022-10-20 12:56:10 +03:00
data["is_taker"] and order.taker_asked_cancel
):
2022-02-17 22:50:10 +03:00
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-10-20 12:56:10 +03:00
Add turtle chat component Squashed commit of the following: commit f60870fcfe574dc4ab1343e25241b6ef7cc2721b Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Nov 10 10:30:42 2022 -0800 Fix internal error when entering chat commit f1eeb49f2a86575eb2e85cdff20460276e71b806 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 10:08:22 2022 -0800 Fix final serializer commit d0c08ba6ad4378a9539c0be83b6f4f8b958b532e Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 09:44:57 2022 -0800 Chat API changes commit a66bf64edc06d936612db6bf75476b54e6a84334 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 09:28:29 2022 -0800 Fix param on post commit 60b18d13c2ec625497323371a2a6f64c9c911e47 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Tue Nov 8 08:56:25 2022 -0800 Fix serializer commit 11212d30eeffde37e07d2e6e5c1fb36df46916ad Author: KoalaSat <yv1vtrul@duck.com> Date: Sun Nov 6 21:07:18 2022 +0100 CR 2 commit c82790cb81dd9919de97c39f2553974784ffe92d Author: KoalaSat <yv1vtrul@duck.com> Date: Sun Nov 6 20:09:18 2022 +0100 Fix commit 605a3b69a1fcf795e45b2acba1e12436f8545f8a Author: KoalaSat <yv1vtrul@duck.com> Date: Sun Nov 6 14:44:42 2022 +0100 CR commit 09776e9c8fa85c253f28c75361829dab5df4d978 Author: KoalaSat <yv1vtrul@duck.com> Date: Wed Nov 2 18:12:29 2022 +0100 translations commit 432e4d23991164b164d2ab3e4f31790a992dc601 Author: KoalaSat <yv1vtrul@duck.com> Date: Wed Nov 2 17:39:02 2022 +0100 Switch and better UX commit df6e476613006f6a861bab68f8a4261bc8f641e0 Author: KoalaSat <yv1vtrul@duck.com> Date: Tue Nov 1 18:20:01 2022 +0100 Unused code commit 5b8d6b4d32980e31bb1d682444b53df1a8e16c47 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Mon Oct 31 09:20:20 2022 -0700 Add Chat Turtle Mode
2022-11-11 12:28:09 +03:00
# 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
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"]:
2022-10-20 12:56:10 +03:00
data["statement_submitted"] = (
order.maker_statement is not None and order.maker_statement != ""
2022-10-20 12:56:10 +03:00
)
2022-02-17 22:50:10 +03:00
elif data["is_taker"]:
2022-10-20 12:56:10 +03:00
data["statement_submitted"] = (
order.taker_statement is not None and order.taker_statement != ""
2022-10-20 12:56:10 +03:00
)
# 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third.
2022-10-20 12:56: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
2022-02-17 22:50:10 +03:00
data["retries"] = order.payout.routing_attempts
data["next_retry_time"] = order.payout.last_routing_time + timedelta(
2022-10-20 12:56:10 +03:00
minutes=RETRY_TIME
)
if order.payout.failure_reason:
2022-10-20 12:56:10 +03:00
data["failure_reason"] = LNPayment.FailureReason(
order.payout.failure_reason
).label
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["trade_satoshis"] = Logics.payout_amount(order, request.user)[1][
"invoice_amount"
]
2022-02-17 22:50:10 +03:00
# 10) If status is 'Expired', "Sending", "Finished" or "failed routing", add info for renewal:
2022-10-20 12:56:10 +03:00
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"] = order.bond_size
2022-07-16 14:15:00 +03:00
# Adds trade summary
2022-10-20 12:56:10 +03:00
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}
2022-07-16 14:15:00 +03:00
# 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
2022-06-16 18:31:30 +03:00
# 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
2022-10-20 12:56:10 +03:00
if Logics.is_buyer(order, request.user):
2022-06-16 18:31:30 +03:00
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"))
2022-10-20 12:56:10 +03:00
if order.payout_tx.status in [
OnchainPayment.Status.MEMPO,
OnchainPayment.Status.CONFI,
]:
2022-06-16 18:31:30 +03:00
data["txid"] = order.payout_tx.txid
elif order.payout_tx.status == OnchainPayment.Status.QUEUE:
data["tx_queued"] = True
data["address"] = order.payout_tx.address
2022-06-16 18:31:30 +03:00
return Response(data, status.HTTP_200_OK)
@extend_schema(**OrderViewSchema.take_update_confirm_dispute_cancel)
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)
# action is either 1)'take', 2)'confirm', 2.b)'undo_confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
2022-06-06 20:57:04 +03:00
# 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform'
2022-02-17 22:50:10 +03:00
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")
2022-06-06 23:37:51 +03:00
mining_fee_rate = serializer.data.get("mining_fee_rate")
2022-02-17 22:50:10 +03:00
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:
2022-10-20 12:56:10 +03:00
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
2022-02-17 22:50:10 +03:00
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)
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-06-19 09:09:21 +03:00
elif action == "update_invoice":
# DEPRECATE post v0.5.1.
if "---" not in pgp_invoice:
valid_signature = True
invoice = pgp_invoice
else:
# END DEPRECATE.
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
)
2022-02-17 22:50:10 +03:00
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-10-20 12:56:10 +03:00
# 2.b) If action is 'update address'
2022-06-19 09:09:21 +03:00
elif action == "update_address":
# DEPRECATE post v0.5.1.
if "---" not in pgp_address:
valid_signature = True
address = pgp_address
else:
# END DEPRECATE.
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,
)
2022-10-20 12:56:10 +03:00
valid, context = Logics.update_address(
order, request.user, address, mining_fee_rate
)
2022-06-06 20:57:04 +03:00
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
2022-02-17 22:50:10 +03:00
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
# 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)
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":
2022-10-20 12:56:10 +03:00
valid, context = Logics.dispute_statement(order, request.user, statement)
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
# 6) If action is rate
2022-02-17 22:50:10 +03:00
elif action == "rate_user" and rating:
2023-05-05 15:40:29 +03:00
"""No user rating"""
pass
2022-01-06 23:33:40 +03:00
# 7) 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)
# 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(
2022-02-17 22:50:10 +03:00
{
2022-10-20 12:56:10 +03:00
"bad_request": "The Robotic Satoshis working in the warehouse did not understand you. "
2023-05-09 16:07:16 +03:00
+ "Please, fill a Bug Issue in Github https://github.com/RoboSats/robosats/issues"
2022-02-17 22:50:10 +03:00
},
status.HTTP_501_NOT_IMPLEMENTED,
)
return self.get(request)
class RobotView(APIView):
authentication_classes = [TokenAuthentication, SessionAuthentication]
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["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 UserView(APIView):
"""
Deprecated. UserView will be completely replaced by the smaller RobotView in
combination with the RobotTokenSHA256 middleware (on-the-fly robot generation)
"""
2022-10-20 12:56:10 +03:00
NickGen = NickGenerator(
lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
)
serializer_class = UserGenSerializer
@extend_schema(**UserViewSchema.post)
def post(self, request, format=None):
"""
Get a new user derived from a high entropy token
- Request has a hash of a high-entropy token
- Request includes pubKey and encrypted privKey
- Generates new nickname and avatar.
- Creates login credentials (new User object)
Response with Avatar, Nickname, pubKey, privKey.
"""
context = {}
serializer = self.serializer_class(data=request.data)
2022-10-20 12:56:10 +03:00
# Return bad request if serializer is not valid
if not serializer.is_valid():
context = {"bad_request": "Invalid serializer"}
return Response(context, status=status.HTTP_400_BAD_REQUEST)
# The new way. The token is never sent. Only its SHA256
token_sha256 = serializer.data.get("token_sha256")
public_key = serializer.data.get("public_key")
encrypted_private_key = serializer.data.get("encrypted_private_key")
2022-10-20 12:56:10 +03:00
# Now the server only receives a hash of the token. So server trusts the client
# with computing length, counts and unique_values to confirm the high entropy of the token
# In any case, it is up to the client if they want to create a bad high entropy token.
# Submitting the three params needed to compute token entropy is not mandatory
# If not submitted, avatars can be created with garbage entropy token. Frontend will always submit them.
try:
unique_values = serializer.data.get("unique_values")
counts = serializer.data.get("counts")
length = serializer.data.get("length")
shannon_entropy = entropy(counts, base=62)
bits_entropy = log2(unique_values**length)
# Payload
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:
context["bad_request"] = "The token does not have enough entropy"
return Response(context, status=status.HTTP_400_BAD_REQUEST)
except Exception:
pass
2022-05-27 00:16:02 +03:00
# Hash the token_sha256, only 1 iteration. (this is the second SHA256 of the user token, aka RoboSats ID)
2022-10-20 12:56:10 +03:00
hash = hashlib.sha256(token_sha256.encode("utf-8")).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)
# Deprecate "png" and keep "webp" only after v0.4.4
image_path = avatar_path.joinpath(nickname + ".webp")
if not image_path.exists():
with open(image_path, "wb") as f:
rh.img.save(f, format="WEBP", quality=80)
image_small_path = avatar_path.joinpath(nickname + ".small.webp")
with open(image_small_path, "wb") as f:
resized_img = rh.img.resize((80, 80))
resized_img.save(f, format="WEBP", quality=80)
png_path = avatar_path.joinpath(nickname + ".png")
with open(png_path, "wb") as f:
2022-07-13 23:40:58 +03:00
rh.img.save(f, format="png", optimize=True)
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:
Add Nav Bar, Settings Page, large refactor (#308) commit a5b63aed93e084fae19d9e444e06238a52f24f3a Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 10:46:05 2022 -0700 Small fixes commit d64adfc2bf9b9c31dca47ab113c06a1268c347c6 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 06:02:06 2022 -0700 wip work on federation settings commit ca35d6b3d2776812b07109e197d2e1d46f9f4e81 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 04:05:33 2022 -0700 Refactor confirmation Dialogs commit c660a5b0d1345d4996efb10cb8999987689bede9 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sat Oct 29 13:36:59 2022 -0700 refactor login (clean separation robot/info. Style navbar. commit b9dc7f7c95a683e3aca024ec6d7857176b4e3a25 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 09:54:38 2022 -0700 Add size slider and settings widget commit 20b2b3dcd6838b129741705f1c65d445271e231d Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 05:41:48 2022 -0700 Add show more and Dialogs commit da8b70091b5f28139cdec1a8895f4563d64d8e88 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 16:26:07 2022 -0700 Add sliding pages commit 6dd90aa1182a7a5e0f0189d1467ba474b68c28c2 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 06:34:58 2022 -0700 Add settings forms commit d3d0f3ee1a52bbf1829714050cc798d2542af8f6 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Wed Oct 26 04:16:06 2022 -0700 Refactor utils
2022-10-30 22:13:01 +03:00
if not public_key or not encrypted_private_key:
context[
"bad_request"
] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
return Response(context, status.HTTP_400_BAD_REQUEST)
(
valid,
bad_keys_context,
public_key,
encrypted_private_key,
) = validate_pgp_keys(public_key, encrypted_private_key)
Add Nav Bar, Settings Page, large refactor (#308) commit a5b63aed93e084fae19d9e444e06238a52f24f3a Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 10:46:05 2022 -0700 Small fixes commit d64adfc2bf9b9c31dca47ab113c06a1268c347c6 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 06:02:06 2022 -0700 wip work on federation settings commit ca35d6b3d2776812b07109e197d2e1d46f9f4e81 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 04:05:33 2022 -0700 Refactor confirmation Dialogs commit c660a5b0d1345d4996efb10cb8999987689bede9 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sat Oct 29 13:36:59 2022 -0700 refactor login (clean separation robot/info. Style navbar. commit b9dc7f7c95a683e3aca024ec6d7857176b4e3a25 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 09:54:38 2022 -0700 Add size slider and settings widget commit 20b2b3dcd6838b129741705f1c65d445271e231d Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 05:41:48 2022 -0700 Add show more and Dialogs commit da8b70091b5f28139cdec1a8895f4563d64d8e88 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 16:26:07 2022 -0700 Add sliding pages commit 6dd90aa1182a7a5e0f0189d1467ba474b68c28c2 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 06:34:58 2022 -0700 Add settings forms commit d3d0f3ee1a52bbf1829714050cc798d2542af8f6 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Wed Oct 26 04:16:06 2022 -0700 Refactor utils
2022-10-30 22:13:01 +03:00
if not valid:
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
2022-10-20 12:56:10 +03:00
User.objects.create_user(
username=nickname, password=token_sha256, is_staff=False
)
user = authenticate(request, username=nickname, password=token_sha256)
login(request, user)
user.robot.avatar = "static/assets/avatars/" + nickname + ".webp"
2022-10-20 12:56:10 +03:00
# Noticed some PGP keys replaced at re-login. Should not happen.
# Let's implement this sanity check "If robot has not keys..."
if not user.robot.public_key:
user.robot.public_key = public_key
if not user.robot.encrypted_private_key:
user.robot.encrypted_private_key = encrypted_private_key
user.robot.save()
2023-01-31 21:58:11 +03:00
context = {**context, **Telegram.get_context(user)}
context["public_key"] = user.robot.public_key
context["encrypted_private_key"] = user.robot.encrypted_private_key
context["wants_stealth"] = user.robot.wants_stealth
return Response(context, status=status.HTTP_201_CREATED)
# log in user and return pub/priv keys if existing
else:
user = authenticate(request, username=nickname, password=token_sha256)
if user is not None:
login(request, user)
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
# Adds/generate telegram token and whether it is enabled
2022-10-20 12:56:10 +03:00
context = {**context, **Telegram.get_context(user)}
# return active order or last made order if any
2022-10-20 12:56:10 +03:00
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:
2022-10-20 12:56:10 +03:00
last_order = Order.objects.filter(
Q(maker=request.user) | Q(taker=request.user)
).last()
if last_order:
context["last_order_id"] = last_order.id
2022-10-20 12:56:10 +03:00
# Sends the welcome back message.
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 chance)
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)
@extend_schema(**UserViewSchema.delete)
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(
{
2022-10-20 12:56:10 +03:00
"bad_request": "Maybe a mistake? User cannot be deleted while he is part of an order"
2022-02-17 22:50:10 +03:00
},
status.HTTP_400_BAD_REQUEST,
)
# Check if has already a robot with
if user.robot.total_contracts > 0:
2022-02-17 22:50:10 +03:00
return Response(
{
2022-10-20 12:56:10 +03:00
"bad_request": "Maybe a mistake? User cannot be deleted as it has completed trades"
2022-02-17 22:50:10 +03:00
},
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 = OrderPublicSerializer
2022-02-17 22:50:10 +03:00
queryset = Order.objects.filter(status=Order.Status.PUB)
@extend_schema(**BookViewSchema.get)
2022-02-17 22:50:10 +03:00
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:
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-10-20 12:56:10 +03:00
queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB)
elif not (int(currency) == 0 and int(type) == 2):
2022-10-20 12:56: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-09-10 16:51:15 +03:00
data["satoshis_now"] = Logics.satoshis_now(order)
2022-01-10 04:12:58 +03:00
# Compute current premium for those orders that are explicitly priced.
2022-10-20 12:56:10 +03:00
data["price"], data["premium"] = Logics.price_and_premium_now(order)
data["maker_status"] = Logics.user_activity_status(order.maker.last_login)
2022-10-20 12:56:10 +03:00
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):
serializer_class = InfoSerializer
@extend_schema(**InfoViewSchema.get)
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(
2022-10-20 12:56:10 +03:00
Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB)
)
2022-02-17 22:50:10 +03:00
context["num_public_sell_orders"] = len(
2022-10-20 12:56:10 +03:00
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"]
2022-10-20 12:56:10 +03:00
)
# 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(
2022-10-20 12:56:10 +03:00
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 if tick.volume else 0)
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"] = round(total_volume, 8)
context["lifetime_volume"] = round(lifetime_volume, 8)
2022-02-17 22:50:10 +03:00
context["lnd_version"] = get_lnd_version()
Add core-lightning as backend lightning node vendor (#611) * Add CLN node backend image and service (#418) * Add cln service * Add hodlvoice Dockerfile and entrypoint * Add lnnode vendor switch (#431) * Add LNNode vendor switch * Add CLN version to frontend and other fixes * init * first draft * add unsettled_local_balance and unsettled_remote_balance * gen_hold_invoice now takes 3 more variables to build a label for cln * remove unneeded payment_hash from gen_hold_invoice * remove comment * add get_cln_version * first draft of clns follow_send_payment * fix name of get_lnd_version * enable flake8 * flake8 fixes * renaming cln file, class and get_version * remove lnd specific commented code * get_version: add try/except, refactor to top to mimic lnd.py * rename htlc_cltv to htlc_expiry * add clns lookup_invoice_status * refactored double_check_htlc_is_settled to the end to match lnds file * fix generate_rpc * Add sample environmental variables, small fixes * Fix CLN gRPC port * Fix gen_hold_invoice, plus some other tiny fixes (#435) * Fix channel_balance to use int object inside Amount (#438) * Add CLN/LND volume to celery-beat service * Add CLN/LND volume to celery-beat service * Bump CLN to v23.05 * changes for 0.5 and some small fixes * change invoice expiry from absolute to relative duration * add try/except to catch timeout error * fix failure_reason to be ln_payment failure reasons, albeit inaccurate sometimes * refactor follow_send_payment and add pending check to expired case * fix status comments * add send_keysend method * fix wrong state ints in cancel and settle * switch to use hodlinvoicelookup in double_check * move pay command after lnpayment status update * remove loop in follow_send_payment and add error result for edge case * fix typeerror for payment_hash * rework follow_send_payment logic and payment_hash, watch harder if pending * use fully qualified names for status instead of raw int * missed 2 status from prev commit * Always copy the cln-grpc-hodl plugin on start up * Fix ALLOW_SELF_KEYSEND linting error * Fix missing definition of failure_reason --------- Co-authored-by: daywalker90 <admin@noserver4u.de>
2023-05-22 17:56:15 +03:00
context["cln_version"] = get_cln_version()
context["robosats_running_commit_hash"] = get_robosats_commit()
context["version"] = settings.VERSION
2022-02-17 22:50:10 +03:00
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")
2022-10-20 12:56:10 +03:00
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"))
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, SessionAuthentication]
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")
# DEPRECATE post v0.5.1.
if "---" not in pgp_invoice:
valid_signature = True
invoice = pgp_invoice
else:
# END DEPRECATE.
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:
2022-10-20 12:56:10 +03:00
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 = {}
2022-10-20 12:56:10 +03:00
queryset = Currency.objects.all().order_by("currency")
for currency in queryset:
code = Currency.currency_dict[str(currency.currency)]
try:
2022-10-20 12:56:10 +03:00
last_tick = MarketTick.objects.filter(currency=currency).latest(
"timestamp"
)
payload[code] = {
2022-10-20 12:56:10 +03:00
"price": last_tick.price,
"volume": last_tick.volume,
"premium": last_tick.premium,
"timestamp": last_tick.timestamp,
}
except Exception:
payload[code] = None
2022-03-21 02:46:36 +03:00
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):
2023-07-23 22:27:37 +03:00
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:
start_date = datetime.strptime(start_date_str, "%d-%m-%Y").date()
self.queryset = self.queryset.filter(timestamp__gte=start_date)
if end_date_str:
end_date = datetime.strptime(end_date_str, "%d-%m-%Y").date()
self.queryset = self.queryset.filter(timestamp__lte=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)
2022-03-21 02:46:36 +03:00
class LimitView(ListAPIView):
@extend_schema(**LimitViewSchema.get)
2022-03-21 02:46:36 +03:00
def get(self, request):
# Trade limits as BTC
min_trade = float(config("MIN_TRADE")) / 100_000_000
max_trade = float(config("MAX_TRADE")) / 100_000_000
2022-03-21 02:46:36 +03:00
payload = {}
2022-10-20 12:56:10 +03:00
queryset = Currency.objects.all().order_by("currency")
2022-03-21 02:46:36 +03:00
for currency in queryset:
code = Currency.currency_dict[str(currency.currency)]
exchange_rate = float(currency.exchange_rate)
payload[currency.currency] = {
2022-10-20 12:56:10 +03:00
"code": code,
"price": exchange_rate,
"min_amount": min_trade * exchange_rate,
"max_amount": max_trade * exchange_rate,
2022-03-21 02:46:36 +03:00
}
return Response(payload, status.HTTP_200_OK)
class HistoricalView(ListAPIView):
@extend_schema(**HistoricalViewSchema.get)
def get(self, request):
payload = {}
2022-10-20 12:56:10 +03:00
queryset = AccountingDay.objects.all().order_by("day")
for accounting_day in queryset:
payload[str(accounting_day.day)] = {
2022-10-20 12:56:10 +03:00
"volume": accounting_day.contracted,
"num_contracts": accounting_day.num_contracts,
}
return Response(payload, status.HTTP_200_OK)
class StealthView(APIView):
authentication_classes = [TokenAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = StealthSerializer
2022-10-20 12:56:10 +03:00
@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})