Add more logics bareframes

This commit is contained in:
Reckless_Satoshi 2022-01-06 12:33:40 -08:00
parent a1771ae5ea
commit 34e05465c2
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
8 changed files with 173 additions and 117 deletions

View File

@ -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')

View File

@ -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

View File

@ -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}

View File

@ -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)

View File

@ -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']
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)

View File

@ -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()),
]

View File

@ -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]

View File

@ -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())