From 4d9a5023e05772e1667ec8509eb60e3ccd117973 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 4 Jan 2022 08:27:15 -0800 Subject: [PATCH 01/17] Fix bugs, cards now link to order --- api/views.py | 3 ++- frontend/src/components/BookPage.js | 6 +++--- frontend/src/components/OrderPage.js | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/views.py b/api/views.py index e7accd36..5eaf1a95 100644 --- a/api/views.py +++ b/api/views.py @@ -97,7 +97,8 @@ class OrderView(APIView): return Response(data, status=status.HTTP_200_OK) else: # Non participants should not see the status or who is the taker - data.pop('status','status_message','taker','taker_nick') + for key in ('status','status_message','taker','taker_nick'): + del data[key] return Response(data, status=status.HTTP_200_OK) return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 13f6ef3b..055b114a 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -23,8 +23,8 @@ export default class BookPage extends Component { } handleCardClick=(e)=>{ - console.log(e.target) - this.props.history.push('/order/' + e.target); + console.log(e) + this.props.history.push('/order/' + e); } // Make these two functions sequential. getOrderDetails needs setState to be finish beforehand. @@ -107,7 +107,7 @@ export default class BookPage extends Component { {/* To fix! does not pass order.id to handleCardCLick. Instead passes the clicked */} - + this.handleCardClick(order.id)}> diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 50c526fa..2e6db655 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -103,7 +103,7 @@ export default class OrderPage extends Component { {this.state.isParticipant ? "" : } - {this.state.isParticipant ? "" : } + From 9ade961e0fce6260e2398e152b0af9cb7c2df137 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 4 Jan 2022 16:13:08 -0800 Subject: [PATCH 02/17] Work on update order endpoint and taker requests --- api/admin.py | 4 +- api/models.py | 18 ++- api/serializers.py | 9 +- api/urls.py | 8 +- api/views.py | 117 +++++++++++++----- dev_utils/reinitiate_db.sh | 15 +++ frontend/src/components/MakerPage.js | 2 +- frontend/src/components/OrderPage.js | 52 +++++++- frontend/src/components/UserGenPage.js | 2 +- .../static/assets/misc/unknown_avatar.png | Bin 0 -> 5347 bytes 10 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 dev_utils/reinitiate_db.sh create mode 100644 frontend/static/assets/misc/unknown_avatar.png diff --git a/api/admin.py b/api/admin.py index dd761dd2..15bebbaf 100644 --- a/api/admin.py +++ b/api/admin.py @@ -24,8 +24,8 @@ class EUserAdmin(UserAdmin): @admin.register(Order) class OrderAdmin(admin.ModelAdmin): - list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at') - list_display_links = ('maker','taker') + list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'invoice') + list_display_links = ['id'] pass @admin.register(Profile) diff --git a/api/models.py b/api/models.py index 150c4c35..10131d1d 100644 --- a/api/models.py +++ b/api/models.py @@ -44,11 +44,11 @@ class Order(models.Model): UPI = 15, 'Updated invoice' DIS = 16, 'In dispute' MLD = 17, 'Maker lost dispute' - TLD = 18, 'Taker lost dispute' - EXP = 19, 'Expired' + # TLD = 18, 'Taker lost dispute' + # EXP = 19, 'Expired' - # order info, id = models.CharField(max_length=64, unique=True, null=False) - status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB) + # order info + status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=int(Status.WFB)) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() @@ -79,6 +79,7 @@ class Order(models.Model): invoice = models.CharField(max_length=300, unique=False, null=True, default=None) class Profile(models.Model): + user = models.OneToOneField(User,on_delete=models.CASCADE) # Ratings stored as a comma separated integer list @@ -91,7 +92,7 @@ class Profile(models.Model): lost_disputes = models.PositiveIntegerField(null=False, default=0) # RoboHash - avatar = models.ImageField(default="static/assets/avatars/unknown.png", verbose_name='Avatar') + avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar') @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): @@ -102,13 +103,18 @@ class Profile(models.Model): def save_user_profile(sender, instance, **kwargs): instance.profile.save() + @receiver(pre_delete, sender=User) + def del_avatar_from_disk(sender, instance, **kwargs): + avatar_file=Path('frontend/' + instance.profile.avatar.url) + avatar_file.unlink() # FIX deleting user fails if avatar is not found + def __str__(self): return self.user.username # to display avatars in admin panel def get_avatar(self): if not self.avatar: - return 'static/assets/avatars/unknown.png' + return 'static/assets/misc/unknown_avatar.png' return self.avatar.url # method to create a fake table field in read only mode diff --git a/api/serializers.py b/api/serializers.py index b73cb157..722a7f4a 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from .models import Order -class OrderSerializer(serializers.ModelSerializer): +class ListOrderSerializer(serializers.ModelSerializer): class Meta: model = Order fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker') @@ -9,4 +9,9 @@ class OrderSerializer(serializers.ModelSerializer): class MakeOrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis') \ No newline at end of file + fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis') + +class UpdateOrderSerializer(serializers.ModelSerializer): + class Meta: + model = Order + fields = ('id','invoice') \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index 1af71120..eae708dd 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,9 +1,9 @@ from django.urls import path -from .views import MakeOrder, OrderView, UserGenerator, BookView +from .views import OrderMakerView, OrderView, UserView, BookView urlpatterns = [ - path('make/', MakeOrder.as_view()), - path('order/', OrderView.as_view()), - path('usergen/', UserGenerator.as_view()), + path('make/', OrderMakerView.as_view()), + path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})), + path('usergen/', UserView.as_view()), path('book/', BookView.as_view()), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 5eaf1a95..33555171 100644 --- a/api/views.py +++ b/api/views.py @@ -1,11 +1,13 @@ -from rest_framework import serializers, status +from rest_framework import status +from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.views import APIView +from rest_framework import viewsets from rest_framework.response import Response + from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User -from django.conf.urls.static import static -from .serializers import OrderSerializer, MakeOrderSerializer +from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from .models import Order from .nick_generator.nick_generator import NickGenerator @@ -24,9 +26,27 @@ expiration_time = 8 avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) +def validate_already_maker_or_taker(request): + '''Checks if the user is already partipant of an order''' + + queryset = Order.objects.filter(maker=request.user.id) + if queryset.exists(): + return False, Response({'Bad Request':'You are already maker of an order'}, status=status.HTTP_400_BAD_REQUEST) + + queryset = Order.objects.filter(taker=request.user.id) + if queryset.exists(): + return False, Response({'Bad Request':'You are already taker of an order'}, status=status.HTTP_400_BAD_REQUEST) + + return True, None + +def validate_ln_invoice(invoice): + '''Checks if a LN invoice is valid''' + #TODO + return True + # Create your views here. -class MakeOrder(APIView): +class OrderMakerView(CreateAPIView): serializer_class = MakeOrderSerializer def post(self,request): @@ -41,17 +61,14 @@ class MakeOrder(APIView): satoshis = serializer.data.get('satoshis') is_explicit = serializer.data.get('is_explicit') - # query if the user is already a maker or taker, return error - queryset = Order.objects.filter(maker=request.user.id) - if queryset.exists(): - return Response({'Bad Request':'You are already maker of an order'},status=status.HTTP_400_BAD_REQUEST) - queryset = Order.objects.filter(taker=request.user.id) - if queryset.exists(): - return Response({'Bad Request':'You are already taker of an order'},status=status.HTTP_400_BAD_REQUEST) + valid, response = validate_already_maker_or_taker(request) + if not valid: + return response # Creates a new order in db order = Order( type=otype, + status=int(Order.Status.PUB), # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) currency=currency, amount=amount, payment_method=payment_method, @@ -65,11 +82,11 @@ class MakeOrder(APIView): if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST) - return Response(OrderSerializer(order).data, status=status.HTTP_201_CREATED) + return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED) -class OrderView(APIView): - serializer_class = OrderSerializer +class OrderView(viewsets.ViewSet): + serializer_class = UpdateOrderSerializer lookup_url_kwarg = 'order_id' def get(self, request, format=None): @@ -81,15 +98,14 @@ class OrderView(APIView): # check if exactly one order is found in the db if len(order) == 1 : order = order[0] - data = self.serializer_class(order).data + data = ListOrderSerializer(order).data nickname = request.user.username - # Check if requester is participant in the order and add boolean to response - data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname) - #To do fix: data['status_message'] = Order.Status.get(order.status).label data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value. - + + # Check if requester is participant in the order and add boolean to response + data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname) data['maker_nick'] = str(order.maker) data['taker_nick'] = str(order.taker) @@ -105,7 +121,48 @@ class OrderView(APIView): return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) -class UserGenerator(APIView): + def take_or_update(self, request, format=None): + order_id = request.GET.get(self.lookup_url_kwarg) + + serializer = UpdateOrderSerializer(data=request.data) + order = Order.objects.get(id=order_id) + + if serializer.is_valid(): + invoice = serializer.data.get('invoice') + + # If this is an empty POST request (no invoice), it must be taker request! + if not invoice and order.status == int(Order.Status.PUB): + + valid, response = validate_already_maker_or_taker(request) + if not valid: + return response + + order.taker = self.request.user + order.status = int(Order.Status.TAK) + data = ListOrderSerializer(order).data + + # An invoice came in! update it + elif invoice: + if validate_ln_invoice(invoice): + order.invoice = invoice + + #TODO Validate if request comes from PARTICIPANT AND BUYER + + #If the order status was Payment Failed. Move foward to invoice Updated. + if order.status == int(Order.Status.FAI): + order.status = int(Order.Status.UPI) + + else: + return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}) + + # Something else is going on. Probably not allowed. + else: + return Response({'bad_request':'Not allowed'}) + + order.save() + return self.get(request) + +class UserView(APIView): lookup_url_kwarg = 'token' NickGen = NickGenerator( lang='English', @@ -114,6 +171,7 @@ class UserGenerator(APIView): use_noun=True, max_num=999) + # Probably should be turned into a post method def get(self,request, format=None): ''' Get a new user derived from a high entropy token @@ -181,40 +239,39 @@ class UserGenerator(APIView): def delete(self,request): user = User.objects.get(id = request.user.id) - # TO DO. Pressing give me another will delete the logged in user + # TO DO. Pressing "give me another" deletes the logged in user # However it might be a long time recovered user # Only delete if user live is < 5 minutes # TODO check if user exists AND it is not a maker or taker! if user is not None: - avatar_file = avatar_path.joinpath(str(request.user)+".png") - avatar_file.unlink() # Unsafe if avatar does not exist. logout(request) user.delete() - return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_301_MOVED_PERMANENTLY) + return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_302_FOUND) return Response(status=status.HTTP_403_FORBIDDEN) -class BookView(APIView): - serializer_class = OrderSerializer +class BookView(ListAPIView): + serializer_class = ListOrderSerializer def get(self,request, format=None): currency = request.GET.get('currency') type = request.GET.get('type') - queryset = Order.objects.filter(currency=currency, type=type, status=0) # TODO status = 1 for orders that are Public + queryset = Order.objects.filter(currency=currency, type=type, status=int(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) queryset = queryset.order_by('created_at') book_data = [] for order in queryset: - data = OrderSerializer(order).data + data = ListOrderSerializer(order).data user = User.objects.filter(id=data['maker']) if len(user) == 1: data['maker_nick'] = user[0].username - # TODO avoid sending status and takers for book views - #data.pop('status','taker') + # Non participants should not see the status or who is the taker + for key in ('status','taker'): + del data[key] book_data.append(data) return Response(book_data, status=status.HTTP_200_OK) diff --git a/dev_utils/reinitiate_db.sh b/dev_utils/reinitiate_db.sh new file mode 100644 index 00000000..32d63e95 --- /dev/null +++ b/dev_utils/reinitiate_db.sh @@ -0,0 +1,15 @@ +#!/bin/bash +rm db.sqlite3 + +rm -R api/migrations +rm -R frontend/migrations +rm -R frontend/static/assets/avatars + +python3 manage.py makemigrations +python3 manage.py makemigrations api + +python3 manage.py migrate + +python3 manage.py createsuperuser + +python3 manage.py runserver \ No newline at end of file diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index 61d38a46..fa344cbf 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -91,7 +91,7 @@ export default class MakerPage extends Component { console.log(this.state) const requestOptions = { method: 'POST', - headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, body: JSON.stringify({ type: this.state.type, currency: this.state.currency, diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 2e6db655..b3df2543 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -2,6 +2,23 @@ import React, { Component } from "react"; import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core" import { Link } from 'react-router-dom' +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} +const csrftoken = getCookie('csrftoken'); + // pretty numbers function pn(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); @@ -26,7 +43,7 @@ export default class OrderPage extends Component { statusText: data.status_message, type: data.type, currency: data.currency, - currencyCode: (data.currency== 1 ) ? "USD": ((data.currency == 2 ) ? "EUR":"ETH"), + currencyCode: this.getCurrencyCode(data.currency), amount: data.amount, paymentMethod: data.payment_method, isExplicit: data.is_explicit, @@ -41,10 +58,29 @@ export default class OrderPage extends Component { }); } + // Gets currency code (3 letters) from numeric (e.g., 1 -> USD) + // Improve this function so currencies are read from json + getCurrencyCode(val){ + return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH") + } + // Fix to use proper react props handleClickBackButton=()=>{ window.history.back(); } + + handleClickTakeOrderButton=()=>{ + console.log(this.state) + const requestOptions = { + method: 'POST', + headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + body: JSON.stringify({}), + }; + fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) + .then((response) => response.json()) + .then((data) => (console.log(data) & this.getOrderDetails(data.id))); + } + render (){ return ( @@ -53,7 +89,7 @@ export default class OrderPage extends Component { BTC {this.state.type ? " Sell " : " Buy "} Order - + + + + : ""} @@ -98,14 +140,16 @@ export default class OrderPage extends Component { + + - {this.state.isParticipant ? "" : } + {this.state.isParticipant ? "" : } - + ); diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index bdab24d0..0332043b 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -74,7 +74,7 @@ export default class UserGenPage extends Component { this.setState({ token: this.genBase62Token(32), }) - this.getGeneratedUser(); + this.reload_for_csrf_to_work(); } handleChangeToken=(e)=>{ diff --git a/frontend/static/assets/misc/unknown_avatar.png b/frontend/static/assets/misc/unknown_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..9e19b6a9857296d12ca124efe8c1f61aa0fa5431 GIT binary patch literal 5347 zcmaKwXE+;P*v9R>E5xc*wRe?JwDzVX#2&GC#A>bDE2zCGs%lG7yRDH>TBAj&m?2b2 z5o%Q1xBvJ1`+hjrxv%G3_qopbJ?G20(=E*O8R@v`NJvN+4GnZGNk~Yg{u?wD|0sJ0i13(>mQ28+RAu4`4jgo$ev|h}haIL)Y$FKN{n*70YwzYwCEaKemEqc4H zu=9M`t2#!?Sb&9wf-L_2|1~iQuy=Guvaf9E45(4plyD+|vYV9QXG$L&3#g2*w10Hv19L`O+dojh;`&ey!}-KcGgZjH_#EgfgSeElYwu9i~oh%Ycl`~V%;wpuX*_f7@V zA~h(uadd;@j_|Jenrm8T_#Gnn{+q5K3x7fCU1H3i0s$*NcP>sidwE)4^b?`XMiu(c zed;nGNGJivljLarW^j(ic4C;nSE>yJIn5ihTT6GC?DsfelH+o`G$(}3g5Of;esGO@ zkxoU`2Kg`&+!i86Vm;1SGI9Hr`JLttmW_vl$vlpV3#5ddIu+|_PE)#sad^L^;L+Rf z#GqHr(u)eDln|;8{}Nt9`ZItq*@f>F{09KA8)D*Ga^1aIu5jfAq3yqAm90kGZI4 zr+z-XRGliNJ5`;KlEP@dERoNjy2qY=7{+cru;-iT!X!OsYyb8*j7{_WW$Oq7Lg~uV z*Y2yxA&27S6R!-0h15~54*-!c`b3Ujk#`wxZZkPqN%<9dVkS5Jb5#FPTKc#ARy)?570mon;BN~Wl78|0=-iTE>i5d2dxuQ> zzdJO~4nTDl-dU466(1enoL(!d@|}XvGqRmKOe;@VQybNPs@v&r&4Z&Je*cfVOGwRp zr^DSv)(W4JR;Dhg9&WL%e&3c@hoEODBM`g7#)_`J$4M-+L+ML;XIS>$_bQYs9F_Z; z4x#>w2@vo2st}#4$GZ*IEXM!T>YLuTq?EkT9@)VNvW^{r>A*NGTsA3Y9Lrxi-pE4! zMR*Jujd-NoCTLP?n-sDJ1iC{TBd1;dBYp1W?}>yNk`9*Gr-pyBgK>wP~(q zb(>#f&~o|vr{OZ};k&WbKtJVnj-g6>RP62^Pb1-`?z;46-U`LV$hu$Lu^+tx8sDgS zwou0eS0J8t&Ka;rQ2dtuaGKRRa$WFI7ojgFmW7CMZ}|bQ@FvjAvtEyAVn%L<4SEwy zQWg@{ue{M;jZ3#TEC9`|vZ_3>;wnq(ItVXh7}=ML_t1i`1KODV5&nOEIwRaat(wIX zDYU4&QO`8^q8E^2bg^iBi@st?c2L3Z;v{^wkn{VdzKSj3Yn zTc(ykpBO!(kFmqtt?d);9T{QZ@4h{g8v0cAwQL1m7&gRMdO?5|DNXD5g#IDrnmQ~& zrCi{hTqy~KS1%Em{mpz^MOOkEIJW|Vg`Jo-uipiUhkk;7yeu_Gb~80({zEQV-hMF# zrC+u@4WEXRVt4aTe*GI6V&Th?Q(mC@HQ=}evO@LjV%81 z4A5HXh~hp5@dN$jZ-qsu#F%eFi8J)_ncx0B0m&jt&3m6hMRzVq#=o@O#VCu}urQ>3 zVj4A5{!{v5VNXm|jh`Xk)~y$X@2J^3P|1+`xv5bXw2PIDpRH8-?j7sVfeN>v8XV~I zq`Z9ICz=9%a&K;_wQ`e&ARmr?oN%V=+LPc)Eu%DV9i=h|Rgn6EgyiG^?c$tAm}=Yo zfGqEJumSEVN_=NJ&dOg}e(Fs}%kI`c9p$yjDjXK4;{L8SsVFMLBl(0BLpf@6&!#}= zYkxqi1I}r2nYlFoKqG^^Vj`ptc}8iM-lke$H~>7OFcK%YSb!IXp=Xza4;EUK_C(5j z<7+r%x2<7FB}WX$xBPd?JANrkzM=(r^v+*_k`u-s_dbp|h$vTgPj=b?=gJ|;-|PF# zB^hXssK{SjOM6y@p8F(|a-3Xb?b6rv0N&Hqgb&M24BXS$4NAWUc-GK;a@k!hYwjYX zsH4kRV<|T|5Tz}3KWfgxnBBffN#ppSb(if$M#|M;7#!`or1VJkkjm7t|!0oQ$S59+qSb1RWj6Rd%hdW#H1O*P1h&jE7Ft^)a;r2SL8M> zf;;EHDgHC7U`40k%?wv^%gIs|tF}yC$pk8}Vo@*x834BtbTb`)UGhFk=`dm`BUb|C zwi8IxGuBIbQ8jvK^6g&hvNeZ2&Xte`Du_;ju>|~{!tro>+G)bMh4E?Ol5eas6W&uH znUomPlbg(!AR1n%mb*#<*jk0!LaBN23-Ey5FUj2M9&qDe;xE5P0S6j)I5~fxJA2bz zz4}Q7P5dLl(jBs1uIU}NeNCyl9G@2F7V|y$sT0O#dAO+Ty8VyV=U_q3c|mu`mPZ(P zUdzuS3n?Fj7_pX2Hwa?kfcj5=`5qc*F}$n-F*N;CY|b?%;$Sl_T(a6Lcy*vwh^8$& zU{>B@PFoVsU?`ebV7Tcf9sT?<$NMG^+oEI>hj0}>Ff^-ej{J%5lX4~C>JXnzs*q>0 z1TAOqs_RaWF_`~R;ew^@wQ$@$EExFlO24zco_j+!%!zhubQL)MVWKc3bi^k0QM40U z_ujzlOse{%gvll#)u&l*6q`6_kn<-_`-TSIP5;`-mo1qtDprpw&8lXKc;!(v;P=yq`fKU7`x}f z;{s4V(sS3>5bhv97*2-&Zj`vZt2XT1%t3xI8HAG}Xk?=lVJoR83e@Wxwv(>O52vXQ zNbGT|z5MEhL}zbYgG3aY*~TD+L2tV7VM*W(MJ~tVH{_i~k}Ackwdz18_hsn|H!ZU} zQ^&G+j;m4YL@E4neNr?=qU}D7Au8Fe3zv_r+DQ6s8=Cfq_>7C+04n&LA^$+iIn=+= z-^36qX$*L~k&fB{pgGD<9=Dm4v@Ra&;W>1FgS3`Q3TCb1r3c5TtppDQ+KrEHi2P`} zP5@e21IB!!L8O{98q*`CHNcF5i7jc`A3*!*r<>#mhyvBE>z^hfYnGxIUuo!vO`2G? z@ev=cw_Dx*2~e^SCS%^-lZWW`h4(iY*68gf#(y#SpBf)a*bDxI+$lViFZIJ&5ZktI zU-MPwa(q62InI`fHvu0#!oLHRN>|)}M8|_OAv^Q~6NHlmrmq63> z+=4GBe2&D1sHVKTv0>Gvgtye-4oPWN{Q2{Uv`Z%VvLY_{T_93zze7GPLS>NAR&UAI z`-!kaAT3j81%E+<(-)D-<@WF6W}W6B$s& z*|w#h>lyN>{%cj%Ko^(NKy{E@(gB(17r)GJ2bgc%SQgbT7DVDM4@vhx@?gBxY|Xox zFnSLAqua3!tEXu}k187MeM+={r&L4W;>NQ~v~T2F$h=cBLFQ9)8->@BuZ9NfD1$Y6 zd_X$hns2wm{NK-4rC%l*xe2pxg%M`M#|TEJjE|IfU$KT(IS!^GK-}Yu$S5d-k_Gt% zgR+l$PhWz0UA%HYAu`iJR#8V*J395(a;oVT@rz$W2Owp$lA`C{__uz|p=E{{hc~$R z86HnSM2O7oXKLjO-N`)-^_Fw&zb}+!UM?ibM#TNF_1^>8C+E$xm|w5{iCgS03WDF8 z>9kkmdjQzupQ7`SPj74zpVp=IL?mjjdLG+%P7|@@5)(V$tF9)fBaByiOM;H>R^$XW zNplO=T?ud*S((n>9ExPD-Y&-0UT*v-yGR&clmfil;hvGqmbN&+hs2I7M>3rg?;_D+ z4#|@888|uGoK41Q9P`)Rr*pl9$v1ufNWKO$t)FZl3Y2VHD(bN_G1#dg?D%gf;@%Z_ z^~cZXZT+iLyLPG5xxam^f};#D_35*0l;v@+0iM@&JWWQcPw7BX+K z@d4sc0X=VRlWC8%$Yx+5=j{{rw|ex40TA{Lz)jgZ7dgM=+562?yMAy$1tVU4^}baVxDZQ*|6Nj9_i_3J(_y%**pvd%)8 zQ2*u}S~^*N3w|ypd3h^rm8+HupP^H=qk#AM`uQE=h6a^o$%OT|o3!MS$*_)eyDBZL z)l1g+7HkC)0C6OsSaB-W_2f65*(RUOKN>p{Ak~G@7^KDc?BF6b8I$eOikUt{(D;MeFqX({_@8g6oyMN*DCVq z`m@4cHAjY^Y5Dfe9MJ*)<^=X>RzZdjuL@S;JGeMASTHCy*eX-fIHH@zbq&RIGb{?d zzthNxNJ5>gCt`V76G$@MXUlT0KZO$qXPHjySceVT-1U{oU8#Psu_CvvPlFZN7e`@2 z3As{`8YdZUNqtowWI)yBP>5`dzE!Ls8*tM8+ZxW)U;3OjYiHeY@J3dCujMVd0@>jq zubAa>%?yeZJe0rh;-|#KN1^x{j2*IWRdG=S`=1kVuw8{}E68-lgh=-f6vMJ|;eMD9 zVd{Rb7W?PxtI$B4OR>Mw{D|YHB7dm}*Ir|cgc*KF=m`ot{xDGK_WRHm?NCCJR^eph$t5FRSjMqI%mAjx?HMXuMLIVIZJv~P9iWLPsoRg3J?VcsJ#xkWD?xmqFo|b!aqpNk|NJ&2-wdToV2V*x4mB literal 0 HcmV?d00001 From 369d9e52a7055696bc3c5f5fb0b6c3a60ae2ffa3 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 4 Jan 2022 16:23:52 -0800 Subject: [PATCH 03/17] Fix bug CSRF token! --- frontend/src/components/OrderPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index b3df2543..d53ca945 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -73,7 +73,7 @@ export default class OrderPage extends Component { console.log(this.state) const requestOptions = { method: 'POST', - headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, body: JSON.stringify({}), }; fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) From ed3605cca6c2edd14eec3c39b88347fd5f5057fc Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 4 Jan 2022 18:03:03 -0800 Subject: [PATCH 04/17] Rework minor ui and bugs --- api/models.py | 6 +-- api/views.py | 27 +++++++--- frontend/src/components/OrderPage.js | 73 +++++++++++++++++++--------- 3 files changed, 73 insertions(+), 33 deletions(-) diff --git a/api/models.py b/api/models.py index 10131d1d..15eaf3ef 100644 --- a/api/models.py +++ b/api/models.py @@ -44,8 +44,8 @@ class Order(models.Model): UPI = 15, 'Updated invoice' DIS = 16, 'In dispute' MLD = 17, 'Maker lost dispute' - # TLD = 18, 'Taker lost dispute' - # EXP = 19, 'Expired' + TLD = 18, 'Taker lost dispute' + EXP = 19, 'Expired' # order info status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=int(Status.WFB)) @@ -77,7 +77,7 @@ class Order(models.Model): # buyer payment LN invoice has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid invoice = models.CharField(max_length=300, unique=False, null=True, default=None) - + class Profile(models.Model): user = models.OneToOneField(User,on_delete=models.CASCADE) diff --git a/api/views.py b/api/views.py index 33555171..a5fc6ef4 100644 --- a/api/views.py +++ b/api/views.py @@ -1,4 +1,4 @@ -from rest_framework import status +from rest_framework import status, serializers from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.views import APIView from rest_framework import viewsets @@ -101,19 +101,30 @@ class OrderView(viewsets.ViewSet): data = ListOrderSerializer(order).data nickname = request.user.username - #To do fix: data['status_message'] = Order.Status.get(order.status).label - data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value. + # Add booleans if user is maker, taker, partipant, buyer or seller + data['is_maker'] = str(order.maker) == nickname + data['is_taker'] = str(order.taker) == nickname + data['is_participant'] = data['is_maker'] or data['is_taker'] + data['is_buyer'] = (data['is_maker'] and order.type == int(Order.Types.BUY)) or (data['is_taker'] and order.type == int(Order.Types.SELL)) + data['is_seller'] = (data['is_maker'] and order.type == int(Order.Types.SELL)) or (data['is_taker'] and order.type == int(Order.Types.BUY)) - # Check if requester is participant in the order and add boolean to response - data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname) + # If not a participant and order is not public, forbid. + if not data['is_participant'] and order.status != int(Order.Status.PUB): + return Response({'bad_request':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) + + # return nicks too data['maker_nick'] = str(order.maker) data['taker_nick'] = str(order.taker) + + #To do fix: data['status_message'] = Order.Status.get(order.status).label + # Needs to serialize the order.status into the message. + data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value. if data['is_participant']: return Response(data, status=status.HTTP_200_OK) else: - # Non participants should not see the status or who is the taker - for key in ('status','status_message','taker','taker_nick'): + # Non participants should not see the status, who is the taker, etc + for key in ('status','status_message','taker','taker_nick','is_maker','is_taker','is_buyer','is_seller'): del data[key] return Response(data, status=status.HTTP_200_OK) @@ -139,6 +150,8 @@ class OrderView(viewsets.ViewSet): order.taker = self.request.user order.status = int(Order.Status.TAK) + + #TODO REPLY WITH HODL INVOICE data = ListOrderSerializer(order).data # An invoice came in! update it diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index d53ca945..afda6b6e 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -2,6 +2,17 @@ import React, { Component } from "react"; import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core" import { Link } from 'react-router-dom' +function msToTime(duration) { + var seconds = Math.floor((duration / 1000) % 60), + minutes = Math.floor((duration / (1000 * 60)) % 60), + hours = Math.floor((duration / (1000 * 60 * 60)) % 24); + + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + + return hours + "h " + minutes + "m " + seconds + "s"; +} + function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { @@ -54,6 +65,9 @@ export default class OrderPage extends Component { makerNick: data.maker_nick, takerId: data.taker, takerNick: data.taker_nick, + isBuyer:data.buyer, + isSeller:data.seller, + expiresAt:data.expires_at, }); }); } @@ -90,16 +104,43 @@ export default class OrderPage extends Component { - + - + + + {this.state.isParticipant ? + <> + {this.state.takerNick!='None' ? + <> + + + + + + + + : + <> + + + + + + } + + :"" + } + @@ -116,31 +157,17 @@ export default class OrderPage extends Component { } - {this.state.isParticipant ? - <> - - - - - { this.state.takerNick!='None' ? - <> - - - - - - : ""} - - :"" - } + + + + + + - + From 635420c9ddecb7fb0d224b0b357a278e98e56dce Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Wed, 5 Jan 2022 02:30:38 -0800 Subject: [PATCH 05/17] Add HTLC model and placeholder functions --- api/admin.py | 10 ++- api/lightning.py | 41 +++++++++++ api/models.py | 79 ++++++++++++++++----- api/views.py | 26 +++---- frontend/src/components/TradePipelineBox.js | 0 5 files changed, 121 insertions(+), 35 deletions(-) create mode 100644 api/lightning.py create mode 100644 frontend/src/components/TradePipelineBox.js diff --git a/api/admin.py b/api/admin.py index 15bebbaf..aedb441b 100644 --- a/api/admin.py +++ b/api/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.db import models from django.contrib.auth.models import Group, User from django.contrib.auth.admin import UserAdmin -from .models import Order, Profile +from .models import Order, LNPayment, Profile admin.site.unregister(Group) admin.site.unregister(User) @@ -24,7 +24,13 @@ class EUserAdmin(UserAdmin): @admin.register(Order) class OrderAdmin(admin.ModelAdmin): - list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'invoice') + list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'buyer_invoice','maker_bond','taker_bond','trade_escrow') + list_display_links = ['id'] + pass + +@admin.register(LNPayment) +class LNPaymentAdmin(admin.ModelAdmin): + list_display = ('id','concept','status','amount','type','invoice','secret','expires_at','sender','receiver') list_display_links = ['id'] pass diff --git a/api/lightning.py b/api/lightning.py new file mode 100644 index 00000000..50d60529 --- /dev/null +++ b/api/lightning.py @@ -0,0 +1,41 @@ +import random +import string + +####### +# Placeholder functions +# Should work with LND (maybe c-lightning in the future) + +class LNNode(): + ''' + Place holder functions to interact with Lightning Node + ''' + + def gen_hodl_invoice(): + '''Generates hodl invoice to publish an order''' + return ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) + + def validate_hodl_invoice_locked(): + '''Generates hodl invoice to publish an order''' + return True + + def validate_ln_invoice(invoice): + '''Checks if a LN invoice is valid''' + return True + + def pay_buyer_invoice(invoice): + '''Sends sats to buyer''' + return True + + def charge_hodl_htlcs(invoice): + '''Charges a LN hodl invoice''' + return True + + def free_hodl_htlcs(invoice): + '''Returns sats''' + return True + + + + + + diff --git a/api/models.py b/api/models.py index 15eaf3ef..778113c8 100644 --- a/api/models.py +++ b/api/models.py @@ -11,8 +11,50 @@ from pathlib import Path ############################# # TODO # Load hparams from .env file -min_satoshis_trade = 10*1000 -max_satoshis_trade = 500*1000 + +MIN_TRADE = 10*1000 #In sats +MAX_TRADE = 500*1000 +FEE = 0.002 # Trade fee in % +BOND_SIZE = 0.01 # Bond in % + + +class LNPayment(models.Model): + + class Types(models.IntegerChoices): + NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hodl) + HODL = 1, 'Hodl invoice' + + class Concepts(models.IntegerChoices): + MAKEBOND = 0, 'Maker bond' + TAKEBOND = 1, 'Taker-buyer bond' + TRESCROW = 2, 'Trade escrow' + PAYBUYER = 3, 'Payment to buyer' + + class Status(models.IntegerChoices): + INVGEN = 0, 'Hodl invoice was generated' + LOCKED = 1, 'Hodl invoice has HTLCs locked' + CHRGED = 2, 'Hodl invoice was charged' + RETNED = 3, 'Hodl invoice was returned' + MISSNG = 4, 'Buyer invoice is missing' + IVALID = 5, 'Buyer invoice is valid' + INPAID = 6, 'Buyer invoice was paid' + INFAIL = 7, 'Buyer invoice routing failed' + + # payment use case + type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL) + concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND) + status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) + + # payment details + invoice = models.CharField(max_length=300, unique=False, null=True, default=None) + secret = models.CharField(max_length=300, unique=False, null=True, default=None) + expires_at = models.DateTimeField() + amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) + + # payment relationals + sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None) + receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None) + class Order(models.Model): @@ -25,7 +67,7 @@ class Order(models.Model): EUR = 2, 'EUR' ETH = 3, 'ETH' - class Status(models.TextChoices): + class Status(models.IntegerChoices): WFB = 0, 'Waiting for bond' PUB = 1, 'Published in order book' DEL = 2, 'Deleted from order book' @@ -48,36 +90,37 @@ class Order(models.Model): EXP = 19, 'Expired' # order info - status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=int(Status.WFB)) + status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() # order details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) currency = models.PositiveSmallIntegerField(choices=Currencies.choices, null=False) - amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)]) + amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) payment_method = models.CharField(max_length=30, null=False, default="Not specified") - premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)]) - satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(min_satoshis_trade), MaxValueValidator(max_satoshis_trade)]) - is_explicit = models.BooleanField(default=False, null=False) # pricing method. A explicit amount of sats, or a relative premium above/below market. + # order pricing method. A explicit amount of sats, or a relative premium above/below market. + is_explicit = models.BooleanField(default=False, null=False) + # marked to marked + premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)]) + t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) + # explicit + satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) + # order participants maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a taker can only take one order # order collateral - has_maker_bond = models.BooleanField(default=False, null=False) - has_taker_bond = models.BooleanField(default=False, null=False) - has_trade_collat = models.BooleanField(default=False, null=False) - - maker_bond_secret = models.CharField(max_length=300, unique=False, null=True, default=None) - taker_bond_secret = models.CharField(max_length=300, unique=False, null=True, default=None) - trade_collat_secret = models.CharField(max_length=300, unique=False, null=True, default=None) + maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None) + taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None) + trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None) # buyer payment LN invoice - has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid - invoice = models.CharField(max_length=300, unique=False, null=True, default=None) - + buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None) + + class Profile(models.Model): user = models.OneToOneField(User,on_delete=models.CASCADE) diff --git a/api/views.py b/api/views.py index a5fc6ef4..2adc38a7 100644 --- a/api/views.py +++ b/api/views.py @@ -8,7 +8,8 @@ from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer -from .models import Order +from .models import Order, LNPayment +from .lightning import LNNode from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -39,11 +40,6 @@ def validate_already_maker_or_taker(request): return True, None -def validate_ln_invoice(invoice): - '''Checks if a LN invoice is valid''' - #TODO - return True - # Create your views here. class OrderMakerView(CreateAPIView): @@ -68,7 +64,7 @@ class OrderMakerView(CreateAPIView): # Creates a new order in db order = Order( type=otype, - status=int(Order.Status.PUB), # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) + status=Order.Status.PUB, # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) currency=currency, amount=amount, payment_method=payment_method, @@ -105,11 +101,11 @@ class OrderView(viewsets.ViewSet): data['is_maker'] = str(order.maker) == nickname data['is_taker'] = str(order.taker) == nickname data['is_participant'] = data['is_maker'] or data['is_taker'] - data['is_buyer'] = (data['is_maker'] and order.type == int(Order.Types.BUY)) or (data['is_taker'] and order.type == int(Order.Types.SELL)) - data['is_seller'] = (data['is_maker'] and order.type == int(Order.Types.SELL)) or (data['is_taker'] and order.type == int(Order.Types.BUY)) + data['is_buyer'] = (data['is_maker'] and order.type == Order.Types.BUY) or (data['is_taker'] and order.type == Order.Types.SELL) + data['is_seller'] = (data['is_maker'] and order.type == Order.Types.SELL) or (data['is_taker'] and order.type == Order.Types.BUY) # If not a participant and order is not public, forbid. - if not data['is_participant'] and order.status != int(Order.Status.PUB): + if not data['is_participant'] and order.status != Order.Status.PUB: return Response({'bad_request':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) # return nicks too @@ -142,28 +138,28 @@ class OrderView(viewsets.ViewSet): invoice = serializer.data.get('invoice') # If this is an empty POST request (no invoice), it must be taker request! - if not invoice and order.status == int(Order.Status.PUB): + if not invoice and order.status == Order.Status.PUB: valid, response = validate_already_maker_or_taker(request) if not valid: return response order.taker = self.request.user - order.status = int(Order.Status.TAK) + order.status = Order.Status.TAK #TODO REPLY WITH HODL INVOICE data = ListOrderSerializer(order).data # An invoice came in! update it elif invoice: - if validate_ln_invoice(invoice): + if LNNode.validate_ln_invoice(invoice): order.invoice = invoice #TODO Validate if request comes from PARTICIPANT AND BUYER #If the order status was Payment Failed. Move foward to invoice Updated. - if order.status == int(Order.Status.FAI): - order.status = int(Order.Status.UPI) + if order.status == Order.Status.FAI: + order.status = Order.Status.UPI else: return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}) diff --git a/frontend/src/components/TradePipelineBox.js b/frontend/src/components/TradePipelineBox.js new file mode 100644 index 00000000..e69de29b From 7d4cd868b0b681b45b4f820192ad5c2389485b1d Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Wed, 5 Jan 2022 03:20:08 -0800 Subject: [PATCH 06/17] Work on admin panel relationals --- api/admin.py | 28 ++++++++++++++-------------- setup.md | 3 +++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/api/admin.py b/api/admin.py index aedb441b..1d0bd415 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from django.db import models +from django_admin_relation_links import AdminChangeLinksMixin from django.contrib.auth.models import Group, User from django.contrib.auth.admin import UserAdmin from .models import Order, LNPayment, Profile @@ -23,20 +23,20 @@ class EUserAdmin(UserAdmin): return obj.profile.avatar_tag() @admin.register(Order) -class OrderAdmin(admin.ModelAdmin): - list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'buyer_invoice','maker_bond','taker_bond','trade_escrow') - list_display_links = ['id'] - pass +class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): + list_display = ('id','type','maker_link','taker_link','status','amount','currency','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') + list_display_links = ('id','type') + change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow') @admin.register(LNPayment) -class LNPaymentAdmin(admin.ModelAdmin): - list_display = ('id','concept','status','amount','type','invoice','secret','expires_at','sender','receiver') - list_display_links = ['id'] - pass +class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): + list_display = ('id','concept','status','amount','type','invoice','secret','expires_at','sender_link','receiver_link') + list_display_links = ('id','concept') + change_links = ('sender','receiver') @admin.register(Profile) -class UserProfileAdmin(admin.ModelAdmin): - list_display = ('avatar_tag','user','id','total_ratings','avg_rating','num_disputes','lost_disputes') - list_display_links =['user'] - readonly_fields = ['avatar_tag'] - pass \ No newline at end of file +class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): + list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes') + list_display_links = ('avatar_tag','id') + change_links =['user'] + readonly_fields = ['avatar_tag'] \ No newline at end of file diff --git a/setup.md b/setup.md index f3b0af5d..c9c82cbf 100644 --- a/setup.md +++ b/setup.md @@ -28,6 +28,9 @@ source /usr/local/bin/virtualenvwrapper.sh ### Install Django and Restframework `pip3 install django djangorestframework` +## Install Django admin relational links +`pip install django-admin-relation-links` + *Django 4.0 at the time of writting* ### Launch the local development node From bd1601d59f3273f25a48f7d2c512733da772372b Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Wed, 5 Jan 2022 04:18:54 -0800 Subject: [PATCH 07/17] Bug fix, order status now as message, HTLCs relationals working --- api/models.py | 35 +++++++++++++++------------- api/serializers.py | 2 +- api/views.py | 15 +++++------- frontend/src/components/OrderPage.js | 11 ++++----- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/api/models.py b/api/models.py index 778113c8..452e57e3 100644 --- a/api/models.py +++ b/api/models.py @@ -46,15 +46,18 @@ class LNPayment(models.Model): status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) # payment details - invoice = models.CharField(max_length=300, unique=False, null=True, default=None) - secret = models.CharField(max_length=300, unique=False, null=True, default=None) + invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + secret = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) expires_at = models.DateTimeField() - amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) + amount = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) # payment relationals sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None) receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None) + def __str__(self): + # Make relational back to ORDER + return (f'HTLC {self.id}: {self.Concepts(self.concept).label}') class Order(models.Model): @@ -97,28 +100,28 @@ class Order(models.Model): # order details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) currency = models.PositiveSmallIntegerField(choices=Currencies.choices, null=False) - amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) - payment_method = models.CharField(max_length=30, null=False, default="Not specified") + amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)]) + payment_method = models.CharField(max_length=30, null=False, default="not specified", blank=True) # order pricing method. A explicit amount of sats, or a relative premium above/below market. is_explicit = models.BooleanField(default=False, null=False) # marked to marked - premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)]) - t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) + premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True) + t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # explicit - satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) + satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # order participants maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order - taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a taker can only take one order + taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order # order collateral - maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None) - taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None) - trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None) + maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) + taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) + trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True) # buyer payment LN invoice - buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None) + buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) class Profile(models.Model): @@ -127,15 +130,15 @@ class Profile(models.Model): # Ratings stored as a comma separated integer list total_ratings = models.PositiveIntegerField(null=False, default=0) - latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list]) # Will only store latest ratings - avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)]) + latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings + avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True) # Disputes num_disputes = models.PositiveIntegerField(null=False, default=0) lost_disputes = models.PositiveIntegerField(null=False, default=0) # RoboHash - avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar') + avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True) @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): diff --git a/api/serializers.py b/api/serializers.py index 722a7f4a..c88b14b8 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -14,4 +14,4 @@ class MakeOrderSerializer(serializers.ModelSerializer): class UpdateOrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ('id','invoice') \ No newline at end of file + fields = ('id','buyer_invoice') \ No newline at end of file diff --git a/api/views.py b/api/views.py index 2adc38a7..04fc6d42 100644 --- a/api/views.py +++ b/api/views.py @@ -112,9 +112,7 @@ class OrderView(viewsets.ViewSet): data['maker_nick'] = str(order.maker) data['taker_nick'] = str(order.taker) - #To do fix: data['status_message'] = Order.Status.get(order.status).label - # Needs to serialize the order.status into the message. - data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value. + data['status_message'] = Order.Status(order.status).label if data['is_participant']: return Response(data, status=status.HTTP_200_OK) @@ -125,9 +123,9 @@ class OrderView(viewsets.ViewSet): return Response(data, status=status.HTTP_200_OK) return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) - return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) + def take_or_update(self, request, format=None): order_id = request.GET.get(self.lookup_url_kwarg) @@ -135,7 +133,7 @@ class OrderView(viewsets.ViewSet): order = Order.objects.get(id=order_id) if serializer.is_valid(): - invoice = serializer.data.get('invoice') + invoice = serializer.data.get('buyer_invoice') # If this is an empty POST request (no invoice), it must be taker request! if not invoice and order.status == Order.Status.PUB: @@ -196,8 +194,7 @@ class UserView(APIView): value, counts = np.unique(list(token), return_counts=True) shannon_entropy = entropy(counts, base=62) bits_entropy = log2(len(value)**len(token)) - - # Start preparing payload + # 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 @@ -208,11 +205,11 @@ class UserView(APIView): # Hashes the token, only 1 iteration. Maybe more is better. hash = hashlib.sha256(str.encode(token)).hexdigest() - # generate nickname + # Generate nickname nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] context['nickname'] = nickname - # generate avatar + # Generate avatar rh = Robohash(hash) rh.assemble(roboset='set1', bgset='any')# for backgrounds ON diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index afda6b6e..6deef6bf 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -130,13 +130,12 @@ export default class OrderPage extends Component { : - <> - - - - - + "" } + + + + :"" } From 5640b11e6f31d6b2488a71a7ce780b96e242213a Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Wed, 5 Jan 2022 05:39:58 -0800 Subject: [PATCH 08/17] Handle empty books --- frontend/src/components/BookPage.js | 138 ++++++++++++++++------------ 1 file changed, 81 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 055b114a..93cb63d8 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -1,5 +1,5 @@ import React, { Component } from "react"; -import { Paper, Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, Link, RouterLink, ListItemAvatar} from "@material-ui/core" +import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, Link, RouterLink, ListItemAvatar} from "@material-ui/core" export default class BookPage extends Component { constructor(props) { @@ -19,7 +19,10 @@ export default class BookPage extends Component { fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type) .then((response) => response.json()) .then((data) => //console.log(data)); - this.setState({orders: data})); + this.setState({ + orders: data, + not_found: data.not_found, + })); } handleCardClick=(e)=>{ @@ -53,6 +56,63 @@ export default class BookPage extends Component { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } + bookCards=()=>{ + return (this.state.orders.map((order) => + + + + this.handleCardClick(order.id)}> + + + + + + + + + + {order.maker_nick} + + + + + {/* CARD PARAGRAPH CONTENT */} + + + â—‘{order.type == 0 ? Buys : Sells } + {parseFloat(parseFloat(order.amount).toFixed(4))} + {" " +this.getCurrencyCode(order.currency)} worth of bitcoin + + + + â—‘ Payment via {order.payment_method} + + + + â—‘ Priced {order.is_explicit ? + " explicitly at " + this.pn(order.satoshis) + " Sats" : ( + " at " + + parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market" + )} + + + + â—‘ {" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC (Binance API) + + + + + + + + + + )); + } + render() { return ( @@ -102,66 +162,30 @@ export default class BookPage extends Component { - {this.state.orders.map((order) => - - - - {/* To fix! does not pass order.id to handleCardCLick. Instead passes the clicked */} - this.handleCardClick(order.id)}> - - - - - - - - - - {order.maker_nick} - - - - - {/* CARD PARAGRAPH CONTENT */} - - - â—‘{order.type == 0 ? Buys : Sells } - {parseFloat(parseFloat(order.amount).toFixed(4))} - {" " +this.getCurrencyCode(order.currency)} worth of bitcoin - - - - â—‘ Payment via {order.payment_method} - - - - â—‘ Priced {order.is_explicit ? - " explicitly at " + this.pn(order.satoshis) + " Sats" : ( - " at " + - parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market" - )} - - - - â—‘ {" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC (Binance API) - - - - - - - - - - )} + { this.state.not_found ? "" : You are {this.state.type == 0 ? " selling " : " buying "} BTC for {this.state.currencyCode} + } + + { this.state.not_found ? + ( + + + No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode} + + + Be the first one to create an order + + + + + + ) + : this.bookCards() + } + Be the first one to create an order - - - - ) : this.bookCards() } diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 0332043b..ef7d9c98 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -58,7 +58,7 @@ export default class UserGenPage extends Component { delGeneratedUser() { const requestOptions = { method: 'DELETE', - headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, }; fetch("/api/usergen", requestOptions) .then((response) => response.json()) From 5505476ea4b6376ee6465ca05c54dbb971323f0a Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 05:55:47 -0800 Subject: [PATCH 10/17] Add logics for Invoice update/creation and maker_hodl_invoice --- api/lightning.py | 10 +++++-- api/models.py | 70 ++++++++++++++++++++++++++++++++++++++------ api/views.py | 75 ++++++++++++++++++++++++++---------------------- 3 files changed, 111 insertions(+), 44 deletions(-) diff --git a/api/lightning.py b/api/lightning.py index de480279..6f2b9eae 100644 --- a/api/lightning.py +++ b/api/lightning.py @@ -1,3 +1,4 @@ +from datetime import timedelta from django.utils import timezone import random @@ -12,9 +13,14 @@ class LNNode(): Place holder functions to interact with Lightning Node ''' - def gen_hodl_invoice(): + def gen_hodl_invoice(num_satoshis, description): '''Generates hodl invoice to publish an order''' - return ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) + # TODO + invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX + payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX + expires_at = timezone.now() + timedelta(hours=8) ##FIX + + return invoice, payment_hash, expires_at def validate_hodl_invoice_locked(): '''Generates hodl invoice to publish an order''' diff --git a/api/models.py b/api/models.py index b5cb5135..44a2f7ab 100644 --- a/api/models.py +++ b/api/models.py @@ -5,6 +5,9 @@ from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from django.utils.html import mark_safe +from datetime import timedelta +from django.utils import timezone + from pathlib import Path from .lightning import LNNode @@ -75,10 +78,10 @@ class Order(models.Model): ETH = 3, 'ETH' class Status(models.IntegerChoices): - WFB = 0, 'Waiting for bond' - PUB = 1, 'Published in order book' - DEL = 2, 'Deleted from order book' - TAK = 3, 'Taken' + WFB = 0, 'Waiting for maker bond' + PUB = 1, 'Public' + DEL = 2, 'Deleted' + TAK = 3, 'Waiting for taker bond' # only needed when taker is a buyer UCA = 4, 'Unilaterally cancelled' RET = 5, 'Returned to order book' # Probably same as 1 in most cases. WF2 = 6, 'Waiting for trade collateral and buyer invoice' @@ -109,11 +112,14 @@ class Order(models.Model): # order pricing method. A explicit amount of sats, or a relative premium above/below market. is_explicit = models.BooleanField(default=False, null=False) - # marked to marked + # marked to market premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True) - t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # explicit satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) + # how many sats at creation and at last check (relevant for marked to market) + t0_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # sats at creation + last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max... + # order participants maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order @@ -172,6 +178,8 @@ class Profile(models.Model): class Logics(): + # escrow_user = User.objects.get(username=ESCROW_USERNAME) + def validate_already_maker_or_taker(user): '''Checks if the user is already partipant of an order''' queryset = Order.objects.filter(maker=user) @@ -197,16 +205,30 @@ class Logics(): is_taker = order.taker == user return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY) + def satoshis_now(order): + ''' checks trade amount in sats ''' + # TODO + # order.last_satoshis = + # order.save() + + return 50000 + + def order_expires(order): + order.status = Order.Status.EXP + order.maker = None + order.taker = None + order.save() + @classmethod def update_invoice(cls, order, user, invoice): is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) # only user is the buyer and a valid LN invoice if cls.is_buyer(order, user) and is_valid_invoice: - order.buyer_invoice, created = LNPayment.objects.update_or_create( - receiver= user, + order.buyer_invoice, _ = LNPayment.objects.update_or_create( concept = LNPayment.Concepts.PAYBUYER, type = LNPayment.Types.NORM, sender = User.objects.get(username=ESCROW_USERNAME), + receiver= user, # if there is a LNPayment matching these above, it updates that with defaults below. defaults={ 'invoice' : invoice, @@ -224,3 +246,35 @@ class Logics(): return True return False + + @classmethod + def gen_maker_hodl_invoice(cls, order, user): + + # Do not and delete if order is more than 5 minutes old + if order.expires_at < timezone.now(): + cls.order_expires(order) + return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + + if order.maker_bond: + return order.maker_bond.invoice + + bond_amount = cls.satoshis_now(order) + description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_amount, description=description) + + order.maker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.MAKEBOND, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = bond_amount, + description = description, + payment_hash = payment_hash, + expires_at = expires_at, + ) + + order.save() + return invoice + diff --git a/api/views.py b/api/views.py index 1a7eaadd..0ea88ee4 100644 --- a/api/views.py +++ b/api/views.py @@ -22,7 +22,7 @@ from datetime import timedelta from django.utils import timezone # .env -expiration_time = 8 +EXPIRATION_MAKE = 5 # minutes avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) @@ -51,14 +51,14 @@ class OrderMakerView(CreateAPIView): # Creates a new order in db order = Order( type=otype, - status=Order.Status.PUB, # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) + status=Order.Status.WFB, currency=currency, amount=amount, payment_method=payment_method, premium=premium, satoshis=satoshis, is_explicit=is_explicit, - expires_at= timezone.now()+timedelta(hours=expiration_time), + expires_at= timezone.now()+timedelta(minutes=EXPIRATION_MAKE), maker=request.user) order.save() @@ -75,42 +75,49 @@ class OrderView(viewsets.ViewSet): def get(self, request, format=None): order_id = request.GET.get(self.lookup_url_kwarg) - if order_id != None: - order = Order.objects.filter(id=order_id) + if order_id == 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 : - order = order[0] - data = ListOrderSerializer(order).data - nickname = request.user.username + # check if exactly one order is found in the db + if len(order) == 1 : + order = order[0] - # Add booleans if user is maker, taker, partipant, buyer or seller - data['is_maker'] = str(order.maker) == nickname - data['is_taker'] = str(order.taker) == nickname - data['is_participant'] = data['is_maker'] or data['is_taker'] - data['is_buyer'] = (data['is_maker'] and order.type == Order.Types.BUY) or (data['is_taker'] and order.type == Order.Types.SELL) - data['is_seller'] = (data['is_maker'] and order.type == Order.Types.SELL) or (data['is_taker'] and order.type == Order.Types.BUY) - - # 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':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) + # If order expired + if order.status == Order.Status.EXP: + return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST) - # return nicks too - data['maker_nick'] = str(order.maker) - data['taker_nick'] = str(order.taker) - - data['status_message'] = Order.Status(order.status).label + data = ListOrderSerializer(order).data - if data['is_participant']: - return Response(data, status=status.HTTP_200_OK) - else: - # Non participants should not see the status, who is the taker, etc - for key in ('status','status_message','taker','taker_nick','is_maker','is_taker','is_buyer','is_seller'): - del data[key] - return Response(data, status=status.HTTP_200_OK) + # 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'] + + # 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':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) + + # non participants can view some details, but only if PUB + elif not data['is_participant'] and order.status != Order.Status.PUB: + return Response(data, status=status.HTTP_200_OK) - return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) - return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) + # For participants add position side, nicks and status as message + 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 + + # If status is 'waiting for maker bond', reply with a hodl invoice too. + if order.status == Order.Status.WFB and data['is_maker']: + data['hodl_invoice'] = Logics.gen_maker_hodl_invoice(order, request.user) + + return Response(data, status=status.HTTP_200_OK) + + return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) + def take_or_update(self, request, format=None): From 805b12de652d930fbac449ccb7cc6aaae03e130e Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 08:20:04 -0800 Subject: [PATCH 11/17] Add preliminary pricing for t0 and maker bond. Add reverse on_delete Cascade Orders -> Htlcs --- api/admin.py | 2 +- api/models.py | 41 +++++++++++++++++++++++++++++++---------- api/views.py | 11 +++++++++-- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/api/admin.py b/api/admin.py index f496caa8..5e8f2aca 100644 --- a/api/admin.py +++ b/api/admin.py @@ -24,7 +24,7 @@ class EUserAdmin(UserAdmin): @admin.register(Order) class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','type','maker_link','taker_link','status','amount','currency','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') + list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') list_display_links = ('id','type') change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow') diff --git a/api/models.py b/api/models.py index 44a2f7ab..ad6de46e 100644 --- a/api/models.py +++ b/api/models.py @@ -9,6 +9,7 @@ from datetime import timedelta from django.utils import timezone from pathlib import Path +import requests from .lightning import LNNode @@ -21,6 +22,8 @@ MAX_TRADE = 500*1000 FEE = 0.002 # Trade fee in % BOND_SIZE = 0.01 # Bond in % ESCROW_USERNAME = 'admin' +MARKET_PRICE_API = 'https://blockchain.info/ticker' + class LNPayment(models.Model): @@ -133,6 +136,16 @@ class Order(models.Model): # buyer payment LN invoice buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) +@receiver(pre_delete, sender=Order) +def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): + to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow) + + for htlc in to_delete: + try: + htlc.delete() + except: + pass + class Profile(models.Model): user = models.OneToOneField(User,on_delete=models.CASCADE) @@ -207,11 +220,17 @@ class Logics(): def satoshis_now(order): ''' checks trade amount in sats ''' - # TODO - # order.last_satoshis = - # order.save() + if order.is_explicit: + satoshis_now = order.satoshis + else: + market_prices = requests.get(MARKET_PRICE_API).json() + print(market_prices) + exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) + print(exchange_rate) + satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 + print(satoshis_now) - return 50000 + return satoshis_now def order_expires(order): order.status = Order.Status.EXP @@ -256,11 +275,12 @@ class Logics(): return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} if order.maker_bond: - return order.maker_bond.invoice - - bond_amount = cls.satoshis_now(order) + return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + + order.satoshis_now = cls.satoshis_now(order) + bond_satoshis = order.satoshis_now * BOND_SIZE description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_amount, description=description) + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) order.maker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.MAKEBOND, @@ -269,12 +289,13 @@ class Logics(): receiver = User.objects.get(username=ESCROW_USERNAME), invoice = invoice, status = LNPayment.Status.INVGEN, - num_satoshis = bond_amount, + num_satoshis = bond_satoshis, description = description, payment_hash = payment_hash, expires_at = expires_at, ) order.save() - return invoice + + return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} diff --git a/api/views.py b/api/views.py index 0ea88ee4..a583f52e 100644 --- a/api/views.py +++ b/api/views.py @@ -58,8 +58,11 @@ class OrderMakerView(CreateAPIView): premium=premium, satoshis=satoshis, is_explicit=is_explicit, - expires_at= timezone.now()+timedelta(minutes=EXPIRATION_MAKE), + expires_at=timezone.now()+timedelta(minutes=EXPIRATION_MAKE), maker=request.user) + + order.t0_satoshis=Logics.satoshis_now(order) # TODO reate Order class method when new instance is created! + order.last_satoshis=Logics.satoshis_now(order) order.save() if not serializer.is_valid(): @@ -112,7 +115,11 @@ class OrderView(viewsets.ViewSet): # If status is 'waiting for maker bond', reply with a hodl invoice too. if order.status == Order.Status.WFB and data['is_maker']: - data['hodl_invoice'] = Logics.gen_maker_hodl_invoice(order, request.user) + valid, context = Logics.gen_maker_hodl_invoice(order, request.user) + if valid: + data = {**data, **context} + else: + Response(context, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_200_OK) From a1771ae5eafdf77a9074a03abf6649b18a183115 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 08:54:37 -0800 Subject: [PATCH 12/17] Add environmental variables to .env --- .env-sample | 17 ++++++ api/logics.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++ api/models.py | 128 ++--------------------------------------- api/views.py | 9 +-- setup.md | 5 +- 5 files changed, 186 insertions(+), 127 deletions(-) create mode 100644 .env-sample create mode 100644 api/logics.py diff --git a/.env-sample b/.env-sample new file mode 100644 index 00000000..2e88f3ec --- /dev/null +++ b/.env-sample @@ -0,0 +1,17 @@ +# Market price public API +MARKET_PRICE_API = 'https://blockchain.info/ticker' + +# Trade fee in percentage % +FEE = 0.002 +# Bond size in percentage % +BOND_SIZE = 0.01 + +# Trade limits in satoshis +MIN_TRADE = 10000 +MAX_TRADE = 500000 + +# Expiration time in minutes +EXPIRATION_MAKE = 5 + +# Username for HTLCs escrows +ESCROW_USERNAME = 'admin' \ No newline at end of file diff --git a/api/logics.py b/api/logics.py new file mode 100644 index 00000000..77c87399 --- /dev/null +++ b/api/logics.py @@ -0,0 +1,154 @@ +from datetime import timedelta +from django.utils import timezone +import requests +from .lightning import LNNode + +from .models import Order, LNPayment, User +from decouple import config + +FEE = float(config('FEE')) +BOND_SIZE = float(config('BOND_SIZE')) +MARKET_PRICE_API = config('MARKET_PRICE_API') +ESCROW_USERNAME = config('ESCROW_USERNAME') + + +class Logics(): + + # escrow_user = User.objects.get(username=ESCROW_USERNAME) + + def validate_already_maker_or_taker(user): + '''Checks if the user is already partipant of an order''' + queryset = Order.objects.filter(maker=user) + if queryset.exists(): + return False, {'Bad Request':'You are already maker of an order'} + queryset = Order.objects.filter(taker=user) + if queryset.exists(): + return False, {'Bad Request':'You are already taker of an order'} + return True, None + + def take(order, user): + order.taker = user + order.status = Order.Status.TAK + order.save() + + def is_buyer(order, user): + is_maker = order.maker == user + is_taker = order.taker == user + return (is_maker and order.type == Order.Types.BUY) or (is_taker and order.type == Order.Types.SELL) + + def is_seller(order, user): + is_maker = order.maker == user + is_taker = order.taker == user + return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY) + + def satoshis_now(order): + ''' checks trade amount in sats ''' + if order.is_explicit: + satoshis_now = order.satoshis + else: + market_prices = requests.get(MARKET_PRICE_API).json() + print(market_prices) + exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) + print(exchange_rate) + satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 + print(satoshis_now) + + return satoshis_now + + def order_expires(order): + order.status = Order.Status.EXP + order.maker = None + order.taker = None + order.save() + + @classmethod + def update_invoice(cls, order, user, invoice): + is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) + # only user is the buyer and a valid LN invoice + if cls.is_buyer(order, user) and is_valid_invoice: + order.buyer_invoice, _ = LNPayment.objects.update_or_create( + concept = LNPayment.Concepts.PAYBUYER, + type = LNPayment.Types.NORM, + sender = User.objects.get(username=ESCROW_USERNAME), + receiver= user, + # if there is a LNPayment matching these above, it updates that with defaults below. + defaults={ + 'invoice' : invoice, + 'status' : LNPayment.Status.VALIDI, + 'num_satoshis' : num_satoshis, + 'description' : description, + 'payment_hash' : payment_hash, + 'expires_at' : expires_at} + ) + + #If the order status was Payment Failed. Move foward to invoice Updated. + if order.status == Order.Status.FAI: + order.status = Order.Status.UPI + order.save() + return True + + return False + + @classmethod + def gen_maker_hodl_invoice(cls, order, user): + + # Do not and delete if order is more than 5 minutes old + if order.expires_at < timezone.now(): + cls.order_expires(order) + return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + + if order.maker_bond: + return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + + order.satoshis_now = cls.satoshis_now(order) + bond_satoshis = order.satoshis_now * BOND_SIZE + description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) + + order.maker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.MAKEBOND, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = bond_satoshis, + description = description, + payment_hash = payment_hash, + expires_at = expires_at, + ) + + order.save() + return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} + + @classmethod + def gen_taker_buyer_hodl_invoice(cls, order, user): + + # Do not and delete if order is more than 5 minutes old + if order.expires_at < timezone.now(): + cls.order_expires(order) + return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + + if order.maker_bond: + return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + + order.satoshis_now = cls.satoshis_now(order) + bond_satoshis = order.satoshis_now * BOND_SIZE + description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) + + order.maker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.MAKEBOND, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = bond_satoshis, + description = description, + payment_hash = payment_hash, + expires_at = expires_at, + ) + + order.save() + return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} \ No newline at end of file diff --git a/api/models.py b/api/models.py index ad6de46e..655189cc 100644 --- a/api/models.py +++ b/api/models.py @@ -5,24 +5,18 @@ from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from django.utils.html import mark_safe -from datetime import timedelta -from django.utils import timezone - +from decouple import config from pathlib import Path -import requests - -from .lightning import LNNode ############################# # TODO # Load hparams from .env file -MIN_TRADE = 10*1000 #In sats -MAX_TRADE = 500*1000 -FEE = 0.002 # Trade fee in % -BOND_SIZE = 0.01 # Bond in % -ESCROW_USERNAME = 'admin' -MARKET_PRICE_API = 'https://blockchain.info/ticker' +MIN_TRADE = int(config('MIN_TRADE')) +MAX_TRADE = int(config('MAX_TRADE')) +FEE = float(config('FEE')) +BOND_SIZE = float(config('BOND_SIZE')) + class LNPayment(models.Model): @@ -189,113 +183,3 @@ class Profile(models.Model): def avatar_tag(self): return mark_safe('' % self.get_avatar()) -class Logics(): - - # escrow_user = User.objects.get(username=ESCROW_USERNAME) - - def validate_already_maker_or_taker(user): - '''Checks if the user is already partipant of an order''' - queryset = Order.objects.filter(maker=user) - if queryset.exists(): - return False, {'Bad Request':'You are already maker of an order'} - queryset = Order.objects.filter(taker=user) - if queryset.exists(): - return False, {'Bad Request':'You are already taker of an order'} - return True, None - - def take(order, user): - order.taker = user - order.status = Order.Status.TAK - order.save() - - def is_buyer(order, user): - is_maker = order.maker == user - is_taker = order.taker == user - return (is_maker and order.type == Order.Types.BUY) or (is_taker and order.type == Order.Types.SELL) - - def is_seller(order, user): - is_maker = order.maker == user - is_taker = order.taker == user - return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY) - - def satoshis_now(order): - ''' checks trade amount in sats ''' - if order.is_explicit: - satoshis_now = order.satoshis - else: - market_prices = requests.get(MARKET_PRICE_API).json() - print(market_prices) - exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) - print(exchange_rate) - satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 - print(satoshis_now) - - return satoshis_now - - def order_expires(order): - order.status = Order.Status.EXP - order.maker = None - order.taker = None - order.save() - - @classmethod - def update_invoice(cls, order, user, invoice): - is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) - # only user is the buyer and a valid LN invoice - if cls.is_buyer(order, user) and is_valid_invoice: - order.buyer_invoice, _ = LNPayment.objects.update_or_create( - concept = LNPayment.Concepts.PAYBUYER, - type = LNPayment.Types.NORM, - sender = User.objects.get(username=ESCROW_USERNAME), - receiver= user, - # if there is a LNPayment matching these above, it updates that with defaults below. - defaults={ - 'invoice' : invoice, - 'status' : LNPayment.Status.VALIDI, - 'num_satoshis' : num_satoshis, - 'description' : description, - 'payment_hash' : payment_hash, - 'expires_at' : expires_at} - ) - - #If the order status was Payment Failed. Move foward to invoice Updated. - if order.status == Order.Status.FAI: - order.status = Order.Status.UPI - order.save() - return True - - return False - - @classmethod - def gen_maker_hodl_invoice(cls, order, user): - - # Do not and delete if order is more than 5 minutes old - if order.expires_at < timezone.now(): - cls.order_expires(order) - return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} - - if order.maker_bond: - return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} - - order.satoshis_now = cls.satoshis_now(order) - bond_satoshis = order.satoshis_now * BOND_SIZE - description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) - - order.maker_bond = LNPayment.objects.create( - concept = LNPayment.Concepts.MAKEBOND, - type = LNPayment.Types.HODL, - sender = user, - receiver = User.objects.get(username=ESCROW_USERNAME), - invoice = invoice, - status = LNPayment.Status.INVGEN, - num_satoshis = bond_satoshis, - description = description, - payment_hash = payment_hash, - expires_at = expires_at, - ) - - order.save() - - return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} - diff --git a/api/views.py b/api/views.py index a583f52e..68a09041 100644 --- a/api/views.py +++ b/api/views.py @@ -8,8 +8,8 @@ from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateInvoiceSerializer -from .models import Order, LNPayment, Logics -from .lightning import LNNode +from .models import Order +from .logics import Logics from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -21,8 +21,9 @@ from pathlib import Path from datetime import timedelta from django.utils import timezone -# .env -EXPIRATION_MAKE = 5 # minutes +from decouple import config + +EXPIRATION_MAKE = config('EXPIRATION_MAKE') avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) diff --git a/setup.md b/setup.md index c9c82cbf..c1cbf307 100644 --- a/setup.md +++ b/setup.md @@ -4,7 +4,10 @@ `sudo apt install python3 python3 pip` ### Install virtual environments -`pip install virtualenvwrapper` +``` +pip install virtualenvwrapper +pip install python-decouple +``` ### Add to .bashrc From 34e05465c2919b6e7b5a1435da9f77bd1d6f5451 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 12:33:40 -0800 Subject: [PATCH 13/17] Add more logics bareframes --- api/admin.py | 2 +- api/lightning.py | 2 +- api/logics.py | 83 ++++++++++----- api/models.py | 36 ++++--- api/serializers.py | 13 +-- api/urls.py | 2 +- api/views.py | 146 ++++++++++++++++----------- frontend/src/components/OrderPage.js | 6 +- 8 files changed, 173 insertions(+), 117 deletions(-) diff --git a/api/admin.py b/api/admin.py index 5e8f2aca..b45ca85b 100644 --- a/api/admin.py +++ b/api/admin.py @@ -30,7 +30,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','concept','status','num_satoshis','type','invoice','preimage','expires_at','sender_link','receiver_link') + list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link') list_display_links = ('id','concept') change_links = ('sender','receiver') diff --git a/api/lightning.py b/api/lightning.py index 6f2b9eae..cf66a12f 100644 --- a/api/lightning.py +++ b/api/lightning.py @@ -13,7 +13,7 @@ class LNNode(): Place holder functions to interact with Lightning Node ''' - def gen_hodl_invoice(num_satoshis, description): + def gen_hodl_invoice(num_satoshis, description, expiry): '''Generates hodl invoice to publish an order''' # TODO invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX diff --git a/api/logics.py b/api/logics.py index 77c87399..5fcdfa95 100644 --- a/api/logics.py +++ b/api/logics.py @@ -11,6 +11,12 @@ BOND_SIZE = float(config('BOND_SIZE')) MARKET_PRICE_API = config('MARKET_PRICE_API') ESCROW_USERNAME = config('ESCROW_USERNAME') +EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) +EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE')) +EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE')) + +BOND_EXPIRY = int(config('BOND_EXPIRY')) +ESCROW_EXPIRY = int(config('ESCROW_EXPIRY')) class Logics(): @@ -46,12 +52,10 @@ class Logics(): if order.is_explicit: satoshis_now = order.satoshis else: + # TODO Add fallback Public APIs and error handling market_prices = requests.get(MARKET_PRICE_API).json() - print(market_prices) exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) - print(exchange_rate) satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 - print(satoshis_now) return satoshis_now @@ -85,25 +89,57 @@ class Logics(): if order.status == Order.Status.FAI: order.status = Order.Status.UPI order.save() - return True + return True, None + + return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'} + + @classmethod + def cancel_order(cls, order, user, state): + + # 1) When maker cancels before bond + '''The order never shows up on the book and status + changes to cancelled. That's it.''' + + # 2) When maker cancels after bond + '''The order dissapears from book and goes to cancelled. + Maker is charged a small amount of sats, to prevent DDOS + on the LN node and order book''' + + # 3) When taker cancels before bond + ''' The order goes back to the book as public. + LNPayment "order.taker_bond" is deleted() ''' + + # 4) When taker or maker cancel after bond + '''The order goes into cancelled status if maker cancels. + The order goes into the public book if taker cancels. + In both cases there is a small fee.''' + + # 5) When trade collateral has been posted + '''Always goes to cancelled status. Collaboration is needed. + When a user asks for cancel, 'order.is_pending_cancel' goes True. + When the second user asks for cancel. Order is totally cancelled. + Has a small cost for both parties to prevent node DDOS.''' + pass - return False @classmethod def gen_maker_hodl_invoice(cls, order, user): - # Do not and delete if order is more than 5 minutes old + # Do not gen and delete if order is more than 5 minutes old if order.expires_at < timezone.now(): cls.order_expires(order) return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + # Return the previous invoice if there was one if order.maker_bond: return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} order.satoshis_now = cls.satoshis_now(order) bond_satoshis = order.satoshis_now * BOND_SIZE - description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) + description = f'RoboSats - Maker bond for order ID {order.id}. These sats will return to you if you do not cheat!' + + # Gen HODL Invoice + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.maker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.MAKEBOND, @@ -115,30 +151,32 @@ class Logics(): num_satoshis = bond_satoshis, description = description, payment_hash = payment_hash, - expires_at = expires_at, - ) + expires_at = expires_at) order.save() return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} @classmethod - def gen_taker_buyer_hodl_invoice(cls, order, user): + def gen_takerbuyer_hodl_invoice(cls, order, user): - # Do not and delete if order is more than 5 minutes old - if order.expires_at < timezone.now(): - cls.order_expires(order) - return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + # Do not gen and cancel if a taker invoice is there and older than 2 minutes + if order.taker_bond.created_at < (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)): + cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond + return False, {'Invoice expired':'You did not confirm taking the order in time.'} - if order.maker_bond: - return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + # Return the previous invoice if there was one + if order.taker_bond: + return True, {'invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} order.satoshis_now = cls.satoshis_now(order) bond_satoshis = order.satoshis_now * BOND_SIZE - description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) + description = f'RoboSats - Taker bond for order ID {order.id}. These sats will return to you if you do not cheat!' + + # Gen HODL Invoice + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) - order.maker_bond = LNPayment.objects.create( - concept = LNPayment.Concepts.MAKEBOND, + order.taker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.TAKEBOND, type = LNPayment.Types.HODL, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), @@ -147,8 +185,7 @@ class Logics(): num_satoshis = bond_satoshis, description = description, payment_hash = payment_hash, - expires_at = expires_at, - ) + expires_at = expires_at) order.save() return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} \ No newline at end of file diff --git a/api/models.py b/api/models.py index 655189cc..2c2ad4f7 100644 --- a/api/models.py +++ b/api/models.py @@ -49,7 +49,6 @@ class LNPayment(models.Model): # payment info invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) payment_hash = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) - preimage = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) description = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() @@ -79,22 +78,21 @@ class Order(models.Model): PUB = 1, 'Public' DEL = 2, 'Deleted' TAK = 3, 'Waiting for taker bond' # only needed when taker is a buyer - UCA = 4, 'Unilaterally cancelled' - RET = 5, 'Returned to order book' # Probably same as 1 in most cases. - WF2 = 6, 'Waiting for trade collateral and buyer invoice' - WTC = 7, 'Waiting only for trade collateral' - WBI = 8, 'Waiting only for buyer invoice' - EXF = 9, 'Exchanging fiat / In chat' - CCA = 10, 'Collaboratively cancelled' - FSE = 11, 'Fiat sent' - FCO = 12, 'Fiat confirmed' - SUC = 13, 'Sucessfully settled' - FAI = 14, 'Failed lightning network routing' - UPI = 15, 'Updated invoice' - DIS = 16, 'In dispute' - MLD = 17, 'Maker lost dispute' - TLD = 18, 'Taker lost dispute' - EXP = 19, 'Expired' + UCA = 4, 'Cancelled' + WF2 = 5, 'Waiting for trade collateral and buyer invoice' + WTC = 6, 'Waiting only for seller trade collateral' + WBI = 7, 'Waiting only for buyer invoice' + EXF = 8, 'Sending fiat - In chatroom' + CCA = 9, 'Collaboratively cancelled' + FSE = 10, 'Fiat sent - In chatroom' + FCO = 11, 'Fiat confirmed' + SUC = 12, 'Sucessfully settled' + FAI = 13, 'Failed lightning network routing' + UPI = 14, 'Updated invoice' + DIS = 15, 'In dispute' + MLD = 16, 'Maker lost dispute' + TLD = 17, 'Taker lost dispute' + EXP = 18, 'Expired' # order info status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB) @@ -117,11 +115,11 @@ class Order(models.Model): t0_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # sats at creation last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max... - # order participants maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order - + is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled. + # order collateral maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) diff --git a/api/serializers.py b/api/serializers.py index 7298b77d..d77b1e17 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -11,12 +11,7 @@ class MakeOrderSerializer(serializers.ModelSerializer): model = Order fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis') -class UpdateOrderSerializer(serializers.ModelSerializer): - class Meta: - model = Order - fields = ('id','buyer_invoice') - -class UpdateInvoiceSerializer(serializers.ModelSerializer): - class Meta: - model = LNPayment - fields = ['invoice'] \ No newline at end of file +class UpdateOrderSerializer(serializers.Serializer): + invoice = serializers.CharField(max_length=300, allow_null=True, allow_blank=True, default=None) + action = serializers.ChoiceField(choices=('take','dispute','cancel','confirm','rate'), allow_null=False) + rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None) \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index eae708dd..b10893c8 100644 --- a/api/urls.py +++ b/api/urls.py @@ -3,7 +3,7 @@ from .views import OrderMakerView, OrderView, UserView, BookView urlpatterns = [ path('make/', OrderMakerView.as_view()), - path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})), + path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})), path('usergen/', UserView.as_view()), path('book/', BookView.as_view()), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 68a09041..1b8900a1 100644 --- a/api/views.py +++ b/api/views.py @@ -7,9 +7,9 @@ from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User -from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateInvoiceSerializer +from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from .models import Order -from .logics import Logics +from .logics import EXP_MAKER_BOND_INVOICE, Logics from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -23,7 +23,7 @@ from django.utils import timezone from decouple import config -EXPIRATION_MAKE = config('EXPIRATION_MAKE') +EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) @@ -36,44 +36,39 @@ class OrderMakerView(CreateAPIView): def post(self,request): serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - otype = 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') + if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST) - valid, context = Logics.validate_already_maker_or_taker(request.user) - if not valid: - return Response(context, status=status.HTTP_409_CONFLICT) + 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') - # Creates a new order in db - order = Order( - type=otype, - status=Order.Status.WFB, - currency=currency, - amount=amount, - payment_method=payment_method, - premium=premium, - satoshis=satoshis, - is_explicit=is_explicit, - expires_at=timezone.now()+timedelta(minutes=EXPIRATION_MAKE), - maker=request.user) + valid, context = Logics.validate_already_maker_or_taker(request.user) + if not valid: return Response(context, status=status.HTTP_409_CONFLICT) - order.t0_satoshis=Logics.satoshis_now(order) # TODO reate Order class method when new instance is created! - order.last_satoshis=Logics.satoshis_now(order) - order.save() + # Creates a new order + order = Order( + type=type, + currency=currency, + amount=amount, + payment_method=payment_method, + premium=premium, + satoshis=satoshis, + is_explicit=is_explicit, + expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), + maker=request.user) - if not serializer.is_valid(): - return Response(status=status.HTTP_400_BAD_REQUEST) - + order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order) # TODO move to Order class method when new instance is created! + + order.save() return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED) class OrderView(viewsets.ViewSet): - serializer_class = UpdateInvoiceSerializer + serializer_class = UpdateOrderSerializer lookup_url_kwarg = 'order_id' def get(self, request, format=None): @@ -88,7 +83,7 @@ class OrderView(viewsets.ViewSet): if len(order) == 1 : order = order[0] - # If order expired + # 1) If order expired if order.status == Order.Status.EXP: return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST) @@ -99,11 +94,11 @@ class OrderView(viewsets.ViewSet): data['is_taker'] = order.taker == request.user data['is_participant'] = data['is_maker'] or data['is_taker'] - # If not a participant and order is not public, forbid. + # 2) 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':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) - # non participants can view some details, but only if PUB + # 3) Non participants can view details (but only if PUB) elif not data['is_participant'] and order.status != Order.Status.PUB: return Response(data, status=status.HTTP_200_OK) @@ -114,45 +109,73 @@ class OrderView(viewsets.ViewSet): data['taker_nick'] = str(order.taker) data['status_message'] = Order.Status(order.status).label - # If status is 'waiting for maker bond', reply with a hodl invoice too. + # 4) If status is 'waiting for maker bond', reply with a MAKER HODL invoice. if order.status == Order.Status.WFB and data['is_maker']: valid, context = Logics.gen_maker_hodl_invoice(order, request.user) - if valid: - data = {**data, **context} - else: - Response(context, status=status.HTTP_400_BAD_REQUEST) + data = {**data, **context} if valid else Response(context, status.HTTP_400_BAD_REQUEST) + + # 5) If status is 'Public' and user is taker/buyer, reply with a TAKER HODL invoice. + elif order.status == Order.Status.PUB and data['is_taker'] and data['is_buyer']: + valid, context = Logics.gen_takerbuyer_hodl_invoice(order, request.user) + data = {**data, **context} if valid else Response(context, status.HTTP_400_BAD_REQUEST) + + # 6) If status is 'Public' and user is taker/seller, reply with a ESCROW HODL invoice. + elif order.status == Order.Status.PUB and data['is_taker'] and data['is_seller']: + valid, context = Logics.gen_seller_hodl_invoice(order, request.user) + data = {**data, **context} if valid else Response(context, status.HTTP_400_BAD_REQUEST) + + # 7) If status is 'WF2/WTC' and user is maker/seller, reply with an ESCROW HODL invoice. + elif (order.status == Order.Status.WF2 or order.status == Order.Status.WF2) and data['is_maker'] and data['is_seller']: + valid, context = Logics.gen_seller_hodl_invoice(order, request.user) + data = {**data, **context} if valid else Response(context, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_200_OK) - return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) - - - def take_or_update(self, request, format=None): + def take_update_confirm_dispute_cancel(self, request, format=None): order_id = request.GET.get(self.lookup_url_kwarg) - serializer = UpdateInvoiceSerializer(data=request.data) + 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) - if serializer.is_valid(): - invoice = serializer.data.get('invoice') + # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update' (invoice) 6)'rate' (counterparty) + action = serializer.data.get('action') + invoice = serializer.data.get('invoice') + rating = serializer.data.get('rating') + # 1) If action is take, it is be 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) + + Logics.take(order, request.user) + else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST) + + # 2) If action is update (invoice) + elif action == 'update' and invoice: + updated, context = Logics.update_invoice(order,request.user,invoice) + if not updated: return Response(context,status.HTTP_400_BAD_REQUEST) - # If this is an empty POST request (no invoice), it must be taker request! - if not invoice and 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) + # 3) If action is cancel + elif action == 'cancel': + pass - Logics.take(order, request.user) + # 4) If action is confirm + elif action == 'confirm': + pass - # An invoice came in! update it - elif invoice: - print(invoice) - updated = Logics.update_invoice(order=order,user=request.user,invoice=invoice) - if not updated: - return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}) - - # Something else is going on. Probably not allowed. + # 5) If action is dispute + elif action == 'dispute': + pass + + # 6) If action is dispute + elif action == 'rate' and rating: + pass + + # If nothing... something else is going on. Probably not allowed! else: return Response({'bad_request':'Not allowed'}) @@ -264,6 +287,7 @@ class BookView(ListAPIView): user = User.objects.filter(id=data['maker']) if len(user) == 1: data['maker_nick'] = user[0].username + # Non participants should not see the status or who is the taker for key in ('status','taker'): del data[key] diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 6deef6bf..5d3d9d44 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -87,8 +87,10 @@ export default class OrderPage extends Component { console.log(this.state) const requestOptions = { method: 'POST', - headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, - body: JSON.stringify({}), + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, + body: JSON.stringify({ + 'action':'take', + }), }; fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) .then((response) => response.json()) From c0d6236dbb5a9fcd6f94fab33d0e8a4366555f67 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 13:36:22 -0800 Subject: [PATCH 14/17] Maker and taker bonds OK --- api/logics.py | 29 +++++++++++++++++++--------- api/serializers.py | 4 ++-- api/views.py | 48 ++++++++++++++++++++++++++++++---------------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/api/logics.py b/api/logics.py index 5fcdfa95..e3ed98c5 100644 --- a/api/logics.py +++ b/api/logics.py @@ -11,6 +11,9 @@ BOND_SIZE = float(config('BOND_SIZE')) MARKET_PRICE_API = config('MARKET_PRICE_API') ESCROW_USERNAME = config('ESCROW_USERNAME') +MIN_TRADE = int(config('MIN_TRADE')) +MAX_TRADE = int(config('MAX_TRADE')) + EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE')) EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE')) @@ -32,6 +35,14 @@ class Logics(): return False, {'Bad Request':'You are already taker of an order'} return True, None + def validate_order_size(order): + '''Checks if order is withing limits at t0''' + if order.t0_satoshis > MAX_TRADE: + return False, {'Bad_request': 'Your order is too big. It is worth {order.t0_satoshis} now, max is {MAX_TRADE}'} + if order.t0_satoshis < MIN_TRADE: + return False, {'Bad_request': 'Your order is too small. It is worth {order.t0_satoshis} now, min is {MIN_TRADE}'} + return True, None + def take(order, user): order.taker = user order.status = Order.Status.TAK @@ -135,7 +146,7 @@ class Logics(): return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} order.satoshis_now = cls.satoshis_now(order) - bond_satoshis = order.satoshis_now * BOND_SIZE + bond_satoshis = int(order.satoshis_now * BOND_SIZE) description = f'RoboSats - Maker bond for order ID {order.id}. These sats will return to you if you do not cheat!' # Gen HODL Invoice @@ -160,16 +171,16 @@ class Logics(): def gen_takerbuyer_hodl_invoice(cls, order, user): # Do not gen and cancel if a taker invoice is there and older than 2 minutes - if order.taker_bond.created_at < (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)): - cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond - return False, {'Invoice expired':'You did not confirm taking the order in time.'} - - # Return the previous invoice if there was one if order.taker_bond: - return True, {'invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} + if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)): + cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond + return False, {'Invoice expired':'You did not confirm taking the order in time.'} + else: + # Return the previous invoice if there was one + return True, {'invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} order.satoshis_now = cls.satoshis_now(order) - bond_satoshis = order.satoshis_now * BOND_SIZE + bond_satoshis = int(order.satoshis_now * BOND_SIZE) description = f'RoboSats - Taker bond for order ID {order.id}. These sats will return to you if you do not cheat!' # Gen HODL Invoice @@ -188,4 +199,4 @@ class Logics(): expires_at = expires_at) order.save() - return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} \ No newline at end of file + return True, {'invoice':invoice,'bond_satoshis': bond_satoshis} \ No newline at end of file diff --git a/api/serializers.py b/api/serializers.py index d77b1e17..a819db91 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Order, LNPayment +from .models import Order class ListOrderSerializer(serializers.ModelSerializer): class Meta: @@ -13,5 +13,5 @@ class MakeOrderSerializer(serializers.ModelSerializer): class UpdateOrderSerializer(serializers.Serializer): invoice = serializers.CharField(max_length=300, allow_null=True, allow_blank=True, default=None) - action = serializers.ChoiceField(choices=('take','dispute','cancel','confirm','rate'), allow_null=False) + action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False) rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None) \ No newline at end of file diff --git a/api/views.py b/api/views.py index 1b8900a1..39ec55d1 100644 --- a/api/views.py +++ b/api/views.py @@ -1,7 +1,6 @@ -from rest_framework import status, serializers +from rest_framework import status, viewsets from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.views import APIView -from rest_framework import viewsets from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout @@ -9,7 +8,7 @@ from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from .models import Order -from .logics import EXP_MAKER_BOND_INVOICE, Logics +from .logics import Logics from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -20,7 +19,6 @@ import hashlib from pathlib import Path from datetime import timedelta from django.utils import timezone - from decouple import config EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) @@ -47,7 +45,7 @@ class OrderMakerView(CreateAPIView): is_explicit = serializer.data.get('is_explicit') valid, context = Logics.validate_already_maker_or_taker(request.user) - if not valid: return Response(context, status=status.HTTP_409_CONFLICT) + if not valid: return Response(context, status.HTTP_409_CONFLICT) # Creates a new order order = Order( @@ -58,10 +56,14 @@ class OrderMakerView(CreateAPIView): premium=premium, satoshis=satoshis, is_explicit=is_explicit, - expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), + expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), # TODO Move to class method maker=request.user) + + # TODO move to Order class method when new instance is created! + order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order) - order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order) # TODO move to Order class method when new instance is created! + valid, context = Logics.validate_order_size(order) + if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) order.save() return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED) @@ -112,25 +114,37 @@ class OrderView(viewsets.ViewSet): # 4) If status is 'waiting for maker bond', reply with a MAKER HODL invoice. if order.status == Order.Status.WFB and data['is_maker']: valid, context = Logics.gen_maker_hodl_invoice(order, request.user) - data = {**data, **context} if valid else Response(context, status.HTTP_400_BAD_REQUEST) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) - # 5) If status is 'Public' and user is taker/buyer, reply with a TAKER HODL invoice. - elif order.status == Order.Status.PUB and data['is_taker'] and data['is_buyer']: + # 5) If status is 'Taken' and user is taker/buyer, reply with a TAKER HODL invoice. + elif order.status == Order.Status.TAK and data['is_taker'] and data['is_buyer']: valid, context = Logics.gen_takerbuyer_hodl_invoice(order, request.user) - data = {**data, **context} if valid else Response(context, status.HTTP_400_BAD_REQUEST) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) # 6) If status is 'Public' and user is taker/seller, reply with a ESCROW HODL invoice. elif order.status == Order.Status.PUB and data['is_taker'] and data['is_seller']: valid, context = Logics.gen_seller_hodl_invoice(order, request.user) - data = {**data, **context} if valid else Response(context, status.HTTP_400_BAD_REQUEST) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) # 7) If status is 'WF2/WTC' and user is maker/seller, reply with an ESCROW HODL invoice. elif (order.status == Order.Status.WF2 or order.status == Order.Status.WF2) and data['is_maker'] and data['is_seller']: valid, context = Logics.gen_seller_hodl_invoice(order, request.user) - data = {**data, **context} if valid else Response(context, status=status.HTTP_400_BAD_REQUEST) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) - return Response(data, status=status.HTTP_200_OK) - return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) + return Response(data, status.HTTP_200_OK) + return Response({'Order Not Found':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND) def take_update_confirm_dispute_cancel(self, request, format=None): order_id = request.GET.get(self.lookup_url_kwarg) @@ -140,7 +154,7 @@ class OrderView(viewsets.ViewSet): order = Order.objects.get(id=order_id) - # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update' (invoice) 6)'rate' (counterparty) + # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' 6)'rate' (counterparty) action = serializer.data.get('action') invoice = serializer.data.get('invoice') rating = serializer.data.get('rating') @@ -232,7 +246,7 @@ class UserView(APIView): with open(image_path, "wb") as f: rh.img.save(f, format="png") - # Create new credentials and logsin if nickname is new + # Create new credentials and log in if nickname is new if len(User.objects.filter(username=nickname)) == 0: User.objects.create_user(username=nickname, password=token, is_staff=False) user = authenticate(request, username=nickname, password=token) From 31b19ce18c8a3ce30139063b2737400849de9776 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 14:39:59 -0800 Subject: [PATCH 15/17] Work on order cancel --- api/logics.py | 106 +++++++++++++++++++++++++++++++------------------- api/models.py | 8 ++-- api/views.py | 43 ++++++++++---------- 3 files changed, 92 insertions(+), 65 deletions(-) diff --git a/api/logics.py b/api/logics.py index e3ed98c5..340d9f7e 100644 --- a/api/logics.py +++ b/api/logics.py @@ -80,57 +80,81 @@ class Logics(): def update_invoice(cls, order, user, invoice): is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) # only user is the buyer and a valid LN invoice - if cls.is_buyer(order, user) and is_valid_invoice: - order.buyer_invoice, _ = LNPayment.objects.update_or_create( - concept = LNPayment.Concepts.PAYBUYER, - type = LNPayment.Types.NORM, - sender = User.objects.get(username=ESCROW_USERNAME), - receiver= user, - # if there is a LNPayment matching these above, it updates that with defaults below. - defaults={ - 'invoice' : invoice, - 'status' : LNPayment.Status.VALIDI, - 'num_satoshis' : num_satoshis, - 'description' : description, - 'payment_hash' : payment_hash, - 'expires_at' : expires_at} - ) + if not (cls.is_buyer(order, user) or is_valid_invoice): + return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'} - #If the order status was Payment Failed. Move foward to invoice Updated. - if order.status == Order.Status.FAI: - order.status = Order.Status.UPI - order.save() - return True, None + order.buyer_invoice, _ = LNPayment.objects.update_or_create( + concept = LNPayment.Concepts.PAYBUYER, + type = LNPayment.Types.NORM, + sender = User.objects.get(username=ESCROW_USERNAME), + receiver= user, + # if there is a LNPayment matching these above, it updates that one with defaults below. + defaults={ + 'invoice' : invoice, + 'status' : LNPayment.Status.VALIDI, + 'num_satoshis' : num_satoshis, + 'description' : description, + 'payment_hash' : payment_hash, + 'expires_at' : expires_at} + ) - return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'} + # If the order status is 'Waiting for invoice'. Move forward to 'waiting for invoice' + if order.status == Order.Status.WFE: order.status = Order.Status.CHA + + # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' or to 'chat' + if order.status == Order.Status.WF2: + print(order.trade_escrow) + if order.trade_escrow: + if order.trade_escrow.status == LNPayment.Status.LOCKED: + order.status = Order.Status.CHA + else: + order.status = Order.Status.WFE + + # If the order status was Payment Failed. Move forward to invoice Updated. + if order.status == Order.Status.FAI: + order.status = Order.Status.UPI + + order.save() + return True, None + + @classmethod def cancel_order(cls, order, user, state): # 1) When maker cancels before bond - '''The order never shows up on the book and status - changes to cancelled. That's it.''' + '''The order never shows up on the book and order + status becomes "cancelled". That's it.''' + if order.status == Order.Status.WFB and order.maker == user: + order.maker = None + order.status = Order.Status.UCA + order.save() + return True, None - # 2) When maker cancels after bond - '''The order dissapears from book and goes to cancelled. - Maker is charged a small amount of sats, to prevent DDOS - on the LN node and order book''' - # 3) When taker cancels before bond - ''' The order goes back to the book as public. - LNPayment "order.taker_bond" is deleted() ''' + # 2) When maker cancels after bond + '''The order dissapears from book and goes to cancelled. + Maker is charged a small amount of sats, to prevent DDOS + on the LN node and order book''' - # 4) When taker or maker cancel after bond - '''The order goes into cancelled status if maker cancels. - The order goes into the public book if taker cancels. - In both cases there is a small fee.''' + # 3) When taker cancels before bond + ''' The order goes back to the book as public. + LNPayment "order.taker_bond" is deleted() ''' + + # 4) When taker or maker cancel after bond + '''The order goes into cancelled status if maker cancels. + The order goes into the public book if taker cancels. + In both cases there is a small fee.''' + + # 5) When trade collateral has been posted + '''Always goes to cancelled status. Collaboration is needed. + When a user asks for cancel, 'order.is_pending_cancel' goes True. + When the second user asks for cancel. Order is totally cancelled. + Has a small cost for both parties to prevent node DDOS.''' + + else: + return False, {'bad_request':'You cannot cancel this order'} - # 5) When trade collateral has been posted - '''Always goes to cancelled status. Collaboration is needed. - When a user asks for cancel, 'order.is_pending_cancel' goes True. - When the second user asks for cancel. Order is totally cancelled. - Has a small cost for both parties to prevent node DDOS.''' - pass @classmethod @@ -168,7 +192,7 @@ class Logics(): return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} @classmethod - def gen_takerbuyer_hodl_invoice(cls, order, user): + def gen_taker_hodl_invoice(cls, order, user): # Do not gen and cancel if a taker invoice is there and older than 2 minutes if order.taker_bond: diff --git a/api/models.py b/api/models.py index 2c2ad4f7..b9a5ae66 100644 --- a/api/models.py +++ b/api/models.py @@ -77,12 +77,12 @@ class Order(models.Model): WFB = 0, 'Waiting for maker bond' PUB = 1, 'Public' DEL = 2, 'Deleted' - TAK = 3, 'Waiting for taker bond' # only needed when taker is a buyer + TAK = 3, 'Waiting for taker bond' UCA = 4, 'Cancelled' WF2 = 5, 'Waiting for trade collateral and buyer invoice' - WTC = 6, 'Waiting only for seller trade collateral' - WBI = 7, 'Waiting only for buyer invoice' - EXF = 8, 'Sending fiat - In chatroom' + WFE = 6, 'Waiting only for seller trade collateral' + WFI = 7, 'Waiting only for buyer invoice' + CHA = 8, 'Sending fiat - In chatroom' CCA = 9, 'Collaboratively cancelled' FSE = 10, 'Fiat sent - In chatroom' FCO = 11, 'Fiat confirmed' diff --git a/api/views.py b/api/views.py index 39ec55d1..ea200022 100644 --- a/api/views.py +++ b/api/views.py @@ -89,6 +89,12 @@ class OrderView(viewsets.ViewSet): if order.status == Order.Status.EXP: return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST) + # 2) If order 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 # Add booleans if user is maker, taker, partipant, buyer or seller @@ -96,11 +102,11 @@ class OrderView(viewsets.ViewSet): data['is_taker'] = order.taker == request.user data['is_participant'] = data['is_maker'] or data['is_taker'] - # 2) If not a participant and order is not public, forbid. + # 3) 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':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) - # 3) Non participants can view details (but only if PUB) + # 4) Non participants can view details (but only if PUB) elif not data['is_participant'] and order.status != Order.Status.PUB: return Response(data, status=status.HTTP_200_OK) @@ -111,7 +117,7 @@ class OrderView(viewsets.ViewSet): data['taker_nick'] = str(order.taker) data['status_message'] = Order.Status(order.status).label - # 4) If status is 'waiting for maker bond', reply with a MAKER HODL invoice. + # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice. if order.status == Order.Status.WFB and data['is_maker']: valid, context = Logics.gen_maker_hodl_invoice(order, request.user) if valid: @@ -119,24 +125,16 @@ class OrderView(viewsets.ViewSet): else: return Response(context, status.HTTP_400_BAD_REQUEST) - # 5) If status is 'Taken' and user is taker/buyer, reply with a TAKER HODL invoice. - elif order.status == Order.Status.TAK and data['is_taker'] and data['is_buyer']: - valid, context = Logics.gen_takerbuyer_hodl_invoice(order, request.user) - if valid: - data = {**data, **context} - else: - return Response(context, status.HTTP_400_BAD_REQUEST) - - # 6) If status is 'Public' and user is taker/seller, reply with a ESCROW HODL invoice. - elif order.status == Order.Status.PUB and data['is_taker'] and data['is_seller']: - valid, context = Logics.gen_seller_hodl_invoice(order, request.user) + # 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER HODL invoice. + elif order.status == Order.Status.TAK and data['is_taker']: + valid, context = Logics.gen_taker_hodl_invoice(order, request.user) if valid: data = {**data, **context} else: return Response(context, status.HTTP_400_BAD_REQUEST) - # 7) If status is 'WF2/WTC' and user is maker/seller, reply with an ESCROW HODL invoice. - elif (order.status == Order.Status.WF2 or order.status == Order.Status.WF2) and data['is_maker'] and data['is_seller']: + # 7) If status is 'WF2'or'WTC' and user is Seller, reply with an ESCROW HODL invoice. + elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE) and data['is_seller']: valid, context = Logics.gen_seller_hodl_invoice(order, request.user) if valid: data = {**data, **context} @@ -147,6 +145,10 @@ class OrderView(viewsets.ViewSet): return Response({'Order Not Found':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND) def take_update_confirm_dispute_cancel(self, request, format=None): + ''' + Here take place all of the user 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) @@ -169,13 +171,14 @@ class OrderView(viewsets.ViewSet): else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST) # 2) If action is update (invoice) - elif action == 'update' and invoice: - updated, context = Logics.update_invoice(order,request.user,invoice) - if not updated: return Response(context,status.HTTP_400_BAD_REQUEST) + elif 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) # 3) If action is cancel elif action == 'cancel': - pass + valid, context = Logics.cancel_order(order,request.user,invoice) + if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) # 4) If action is confirm elif action == 'confirm': From 6a1a906beafa7b58c26c443f028cf4f16d654230 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 15:33:55 -0800 Subject: [PATCH 16/17] Cosmetic --- api/logics.py | 5 ++--- api/models.py | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/logics.py b/api/logics.py index 340d9f7e..f8ebab04 100644 --- a/api/logics.py +++ b/api/logics.py @@ -131,7 +131,6 @@ class Logics(): order.save() return True, None - # 2) When maker cancels after bond '''The order dissapears from book and goes to cancelled. Maker is charged a small amount of sats, to prevent DDOS @@ -171,7 +170,7 @@ class Logics(): order.satoshis_now = cls.satoshis_now(order) bond_satoshis = int(order.satoshis_now * BOND_SIZE) - description = f'RoboSats - Maker bond for order ID {order.id}. These sats will return to you if you do not cheat!' + description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.' # Gen HODL Invoice invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) @@ -205,7 +204,7 @@ class Logics(): order.satoshis_now = cls.satoshis_now(order) bond_satoshis = int(order.satoshis_now * BOND_SIZE) - description = f'RoboSats - Taker bond for order ID {order.id}. These sats will return to you if you do not cheat!' + description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.' # Gen HODL Invoice invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) diff --git a/api/models.py b/api/models.py index b9a5ae66..1524ef8d 100644 --- a/api/models.py +++ b/api/models.py @@ -128,6 +128,10 @@ class Order(models.Model): # buyer payment LN invoice buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) + def __str__(self): + # Make relational back to ORDER + return (f'Order {self.id}: {self.Types(self.type).label} {"{:,}".format(self.t0_satoshis)} Sats for {self.Currencies(self.currency).label}') + @receiver(pre_delete, sender=Order) def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow) From 8a55383761c6b71d13119e503966ac634834fec1 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 7 Jan 2022 03:31:33 -0800 Subject: [PATCH 17/17] Add more api logics The workflow is actually more complex than I though. In fact the whole scope of the project greatly surpass my expectation of "weekend project". Want to lay down something functional even if buggy and ugly, I'm a bad coder but this will work out! --- api/logics.py | 125 +++++++++++++++++------ api/models.py | 41 ++++---- api/views.py | 144 ++++++++++++++++----------- frontend/src/components/MakerPage.js | 12 ++- frontend/src/components/OrderPage.js | 1 + 5 files changed, 213 insertions(+), 110 deletions(-) diff --git a/api/logics.py b/api/logics.py index f8ebab04..a0f6c62a 100644 --- a/api/logics.py +++ b/api/logics.py @@ -29,18 +29,18 @@ class Logics(): '''Checks if the user is already partipant of an order''' queryset = Order.objects.filter(maker=user) if queryset.exists(): - return False, {'Bad Request':'You are already maker of an order'} + return False, {'bad_request':'You are already maker of an order'} queryset = Order.objects.filter(taker=user) if queryset.exists(): - return False, {'Bad Request':'You are already taker of an order'} + return False, {'bad_request':'You are already taker of an order'} return True, None def validate_order_size(order): '''Checks if order is withing limits at t0''' if order.t0_satoshis > MAX_TRADE: - return False, {'Bad_request': 'Your order is too big. It is worth {order.t0_satoshis} now, max is {MAX_TRADE}'} + return False, {'bad_request': f'Your order is too big. It is worth {order.t0_satoshis} now. But maximum is {MAX_TRADE}'} if order.t0_satoshis < MIN_TRADE: - return False, {'Bad_request': 'Your order is too small. It is worth {order.t0_satoshis} now, min is {MIN_TRADE}'} + return False, {'bad_request': f'Your order is too small. It is worth {order.t0_satoshis} now. But minimum is {MIN_TRADE}'} return True, None def take(order, user): @@ -64,11 +64,12 @@ class Logics(): satoshis_now = order.satoshis else: # TODO Add fallback Public APIs and error handling + # Think about polling price data in a different way (e.g. store locally every t seconds) market_prices = requests.get(MARKET_PRICE_API).json() exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 - return satoshis_now + return int(satoshis_now) def order_expires(order): order.status = Order.Status.EXP @@ -76,6 +77,16 @@ class Logics(): order.taker = None order.save() + @classmethod + def buyer_invoice_amount(cls, order, user): + ''' Computes buyer invoice amount. Uses order.last_satoshis, + that is the final trade amount set at Taker Bond time''' + + if cls.is_buyer(order, user): + invoice_amount = int(order.last_satoshis * (1-FEE)) # Trading FEE is charged here. + + return True, {'invoice_amount': invoice_amount} + @classmethod def update_invoice(cls, order, user, invoice): is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) @@ -117,12 +128,26 @@ class Logics(): order.save() return True, None - + @classmethod + def rate_counterparty(cls, order, user, rating): + # if maker, rates taker + if order.maker == user: + order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1 + last_ratings = list(order.taker.profile.last_ratings).append(rating) + order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings) + # if taker, rates maker + if order.taker == user: + order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1 + last_ratings = list(order.maker.profile.last_ratings).append(rating) + order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings) + + order.save() + return True, None @classmethod def cancel_order(cls, order, user, state): - - # 1) When maker cancels before bond + + # 1) When maker cancels before bond '''The order never shows up on the book and order status becomes "cancelled". That's it.''' if order.status == Order.Status.WFB and order.maker == user: @@ -140,12 +165,12 @@ class Logics(): ''' The order goes back to the book as public. LNPayment "order.taker_bond" is deleted() ''' - # 4) When taker or maker cancel after bond + # 4) When taker or maker cancel after bond (before escrow) '''The order goes into cancelled status if maker cancels. The order goes into the public book if taker cancels. In both cases there is a small fee.''' - # 5) When trade collateral has been posted + # 5) When trade collateral has been posted (after escrow) '''Always goes to cancelled status. Collaboration is needed. When a user asks for cancel, 'order.is_pending_cancel' goes True. When the second user asks for cancel. Order is totally cancelled. @@ -154,22 +179,23 @@ class Logics(): else: return False, {'bad_request':'You cannot cancel this order'} - - @classmethod def gen_maker_hodl_invoice(cls, order, user): - # Do not gen and delete if order is more than 5 minutes old + # Do not gen and cancel if order is more than 5 minutes old if order.expires_at < timezone.now(): cls.order_expires(order) - return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'} - # Return the previous invoice if there was one + # Return the previous invoice if there was one and is still unpaid if order.maker_bond: - return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + if order.maker_bond.status == LNPayment.Status.INVGEN: + return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + else: + return False, None - order.satoshis_now = cls.satoshis_now(order) - bond_satoshis = int(order.satoshis_now * BOND_SIZE) + order.last_satoshis = cls.satoshis_now(order) + bond_satoshis = int(order.last_satoshis * BOND_SIZE) description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.' # Gen HODL Invoice @@ -188,22 +214,27 @@ class Logics(): expires_at = expires_at) order.save() - return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} + return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis} @classmethod def gen_taker_hodl_invoice(cls, order, user): - # Do not gen and cancel if a taker invoice is there and older than 2 minutes + # Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still if order.taker_bond: - if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)): - cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond - return False, {'Invoice expired':'You did not confirm taking the order in time.'} + # Check if status is INVGEN and still not expired + if order.taker_bond.status == LNPayment.Status.INVGEN: + if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)): + cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond + return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'} + # Return the previous invoice there was with INVGEN status + else: + return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} + # Invoice exists, but was already locked or settled else: - # Return the previous invoice if there was one - return True, {'invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} + return False, None - order.satoshis_now = cls.satoshis_now(order) - bond_satoshis = int(order.satoshis_now * BOND_SIZE) + order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE + bond_satoshis = int(order.last_satoshis * BOND_SIZE) description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.' # Gen HODL Invoice @@ -222,4 +253,42 @@ class Logics(): expires_at = expires_at) order.save() - return True, {'invoice':invoice,'bond_satoshis': bond_satoshis} \ No newline at end of file + return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis} + + @classmethod + def gen_escrow_hodl_invoice(cls, order, user): + + # Do not generate and cancel if an invoice is there and older than X minutes and unpaid still + if order.trade_escrow: + # Check if status is INVGEN and still not expired + if order.taker_bond.status == LNPayment.Status.INVGEN: + if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TRADE_ESCR_INVOICE)): # Expired + cls.cancel_order(order, user, 4) # State 4, cancel order before trade escrow locked + return False, {'bad_request':'Invoice expired. You did not lock the trade escrow in time.'} + # Return the previous invoice there was with INVGEN status + else: + return True, {'escrow_invoice':order.trade_escrow.invoice,'escrow_satoshis':order.trade_escrow.num_satoshis} + # Invoice exists, but was already locked or settled + else: + return False, None # Does not return any context of a healthy locked escrow + + escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis) + description = f'RoboSats - Escrow amount for {str(order)} - This escrow will be released to the buyer once you confirm you received the fiat.' + + # Gen HODL Invoice + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600) + + order.taker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.TRESCROW, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = escrow_satoshis, + description = description, + payment_hash = payment_hash, + expires_at = expires_at) + + order.save() + return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis} \ No newline at end of file diff --git a/api/models.py b/api/models.py index 1524ef8d..accc8e18 100644 --- a/api/models.py +++ b/api/models.py @@ -27,18 +27,18 @@ class LNPayment(models.Model): class Concepts(models.IntegerChoices): MAKEBOND = 0, 'Maker bond' - TAKEBOND = 1, 'Taker-buyer bond' + TAKEBOND = 1, 'Taker bond' TRESCROW = 2, 'Trade escrow' PAYBUYER = 3, 'Payment to buyer' class Status(models.IntegerChoices): - INVGEN = 0, 'Hodl invoice was generated' - LOCKED = 1, 'Hodl invoice has HTLCs locked' - SETLED = 2, 'Invoice settled' - RETNED = 3, 'Hodl invoice was returned' - MISSNG = 4, 'Buyer invoice is missing' - VALIDI = 5, 'Buyer invoice is valid' - INFAIL = 6, 'Buyer invoice routing failed' + INVGEN = 0, 'Generated' + LOCKED = 1, 'Locked' + SETLED = 2, 'Settled' + RETNED = 3, 'Returned' + MISSNG = 4, 'Missing' + VALIDI = 5, 'Valid' + INFAIL = 6, 'Failed routing' # payment use details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL) @@ -59,8 +59,7 @@ class LNPayment(models.Model): receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None) def __str__(self): - # Make relational back to ORDER - return (f'HTLC {self.id}: {self.Concepts(self.concept).label}') + return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}') class Order(models.Model): @@ -74,16 +73,16 @@ class Order(models.Model): ETH = 3, 'ETH' class Status(models.IntegerChoices): - WFB = 0, 'Waiting for maker bond' - PUB = 1, 'Public' - DEL = 2, 'Deleted' - TAK = 3, 'Waiting for taker bond' - UCA = 4, 'Cancelled' - WF2 = 5, 'Waiting for trade collateral and buyer invoice' - WFE = 6, 'Waiting only for seller trade collateral' - WFI = 7, 'Waiting only for buyer invoice' - CHA = 8, 'Sending fiat - In chatroom' - CCA = 9, 'Collaboratively cancelled' + WFB = 0, 'Waiting for maker bond' + PUB = 1, 'Public' + DEL = 2, 'Deleted' + TAK = 3, 'Waiting for taker bond' + UCA = 4, 'Cancelled' + WF2 = 5, 'Waiting for trade collateral and buyer invoice' + WFE = 6, 'Waiting only for seller trade collateral' + WFI = 7, 'Waiting only for buyer invoice' + CHA = 8, 'Sending fiat - In chatroom' + CCA = 9, 'Collaboratively cancelled' FSE = 10, 'Fiat sent - In chatroom' FCO = 11, 'Fiat confirmed' SUC = 12, 'Sucessfully settled' @@ -130,7 +129,7 @@ class Order(models.Model): def __str__(self): # Make relational back to ORDER - return (f'Order {self.id}: {self.Types(self.type).label} {"{:,}".format(self.t0_satoshis)} Sats for {self.Currencies(self.currency).label}') + return (f'Order {self.id}: {self.Types(self.type).label} BTC for {self.amount} {self.Currencies(self.currency).label}') @receiver(pre_delete, sender=Order) def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): diff --git a/api/views.py b/api/views.py index ea200022..c1cf3897 100644 --- a/api/views.py +++ b/api/views.py @@ -7,7 +7,7 @@ from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer -from .models import Order +from .models import LNPayment, Order from .logics import Logics from .nick_generator.nick_generator import NickGenerator @@ -74,75 +74,100 @@ class OrderView(viewsets.ViewSet): lookup_url_kwarg = 'order_id' 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 order_id == None: - return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) + 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 : - order = order[0] + if len(order) != 1 : + return Response({'bad_request':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND) + + # This is our order. + order = order[0] - # 1) If order expired - if order.status == Order.Status.EXP: - return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST) + # 1) If order expired + if order.status == Order.Status.EXP: + return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST) - # 2) If order 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) + # 2) If order 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 = ListOrderSerializer(order).data - # 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) 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':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) - - # 4) Non participants can view details (but only if PUB) - elif not data['is_participant'] and order.status != Order.Status.PUB: - return Response(data, status=status.HTTP_200_OK) + # 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) 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':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN) + + # 4) Non participants can view details (but only if PUB) + elif not data['is_participant'] and order.status != Order.Status.PUB: + return Response(data, status=status.HTTP_200_OK) - # For participants add position side, nicks and status as message - 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 + # For participants add position side, nicks and status as message + 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 - # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice. - if order.status == Order.Status.WFB and data['is_maker']: - valid, context = Logics.gen_maker_hodl_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 HODL invoice. - elif order.status == Order.Status.TAK and data['is_taker']: - valid, context = Logics.gen_taker_hodl_invoice(order, request.user) - if valid: - data = {**data, **context} - else: - return Response(context, status.HTTP_400_BAD_REQUEST) - - # 7) If status is 'WF2'or'WTC' and user is Seller, reply with an ESCROW HODL invoice. - elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE) and data['is_seller']: - valid, context = Logics.gen_seller_hodl_invoice(order, request.user) - if valid: - data = {**data, **context} - else: - return Response(context, status.HTTP_400_BAD_REQUEST) + # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice. + if order.status == Order.Status.WFB and data['is_maker']: + valid, context = Logics.gen_maker_hodl_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 HODL invoice. + elif order.status == Order.Status.TAK and data['is_taker']: + valid, context = Logics.gen_taker_hodl_invoice(order, request.user) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) + + # 7) If status is 'WF2'or'WTC' + elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE): - return Response(data, status.HTTP_200_OK) - return Response({'Order Not Found':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND) + # If the two bonds are locked + if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED: + + # 7.a) And if user is Seller, reply with an ESCROW HODL invoice. + if data['is_seller']: + valid, context = Logics.gen_escrow_hodl_invoice(order, request.user) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) + + # 7.b) If user is Buyer, reply with an AMOUNT so he can send the buyer invoice. + elif data['is_buyer']: + valid, context = Logics.buyer_invoice_amount(order, request.user) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) + + # 8) If status is 'CHA'or '' or '' and all HTLCS are in LOCKED + elif order.status == Order.Status.CHA: # TODO Add the other status + if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED: + # add whether a collaborative cancel is pending + data['pending_cancel'] = order.is_pending_cancel + + return Response(data, status.HTTP_200_OK) def take_update_confirm_dispute_cancel(self, request, format=None): ''' @@ -177,7 +202,7 @@ class OrderView(viewsets.ViewSet): # 3) If action is cancel elif action == 'cancel': - valid, context = Logics.cancel_order(order,request.user,invoice) + valid, context = Logics.cancel_order(order,request.user) if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) # 4) If action is confirm @@ -190,11 +215,12 @@ class OrderView(viewsets.ViewSet): # 6) If action is dispute elif action == 'rate' and rating: - pass + valid, context = Logics.rate_counterparty(order,request.user, rating) + if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) # If nothing... something else is going on. Probably not allowed! else: - return Response({'bad_request':'Not allowed'}) + return Response({'bad_request':'The Robotic Satoshis working in the warehouse did not understand you'}) return self.get(request) diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index fa344cbf..a98e5049 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" +import { Paper, Alert, AlertTitle, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" import { Link } from 'react-router-dom' function getCookie(name) { @@ -104,7 +104,8 @@ export default class MakerPage extends Component { }; fetch("/api/make/",requestOptions) .then((response) => response.json()) - .then((data) => (console.log(data) & this.props.history.push('/order/' + data.id))); + .then((data) => (this.setState({badRequest:data.bad_request}) + & (data.id ? this.props.history.push('/order/' + data.id) :""))); } render() { @@ -242,6 +243,13 @@ export default class MakerPage extends Component { + + + {this.state.badRequest ? + + {this.state.badRequest}
+
+ : ""}
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode} diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 5d3d9d44..86ed50ec 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -68,6 +68,7 @@ export default class OrderPage extends Component { isBuyer:data.buyer, isSeller:data.seller, expiresAt:data.expires_at, + badRequest:data.bad_request, }); }); }