merge with branch logics-second-iteration

This commit is contained in:
LowEntropyFace 2022-01-08 20:18:11 -05:00
commit ec823586c1
9 changed files with 286 additions and 88 deletions

17
.env-sample Normal file
View File

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

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin from django_admin_relation_links import AdminChangeLinksMixin
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from .models import Order, LNPayment, Profile from .models import Order, LNPayment, Profile, MarketTick
admin.site.unregister(Group) admin.site.unregister(Group)
admin.site.unregister(User) admin.site.unregister(User)
@ -17,22 +17,24 @@ class ProfileInline(admin.StackedInline):
@admin.register(User) @admin.register(User)
class EUserAdmin(UserAdmin): class EUserAdmin(UserAdmin):
inlines = [ProfileInline] inlines = [ProfileInline]
list_display = ('avatar_tag',) + UserAdmin.list_display list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
list_display_links = ['username'] list_display_links = ('id','username')
def avatar_tag(self, obj): def avatar_tag(self, obj):
return obj.profile.avatar_tag() return obj.profile.avatar_tag()
@admin.register(Order) @admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
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 = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display_links = ('id','type') list_display_links = ('id','type')
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow') change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow')
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
@admin.register(LNPayment) @admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('id','concept','status','num_satoshis','type','invoice','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') list_display_links = ('id','concept')
change_links = ('sender','receiver') change_links = ('sender','receiver')
list_filter = ('type','concept','status')
@admin.register(Profile) @admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@ -40,3 +42,9 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display_links = ('avatar_tag','id') list_display_links = ('avatar_tag','id')
change_links =['user'] change_links =['user']
readonly_fields = ['avatar_tag'] readonly_fields = ['avatar_tag']
@admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin):
list_display = ('timestamp','price','volume','premium','currency','fee')
readonly_fields = ('timestamp','price','volume','premium','currency','fee')
list_filter = ['currency']

View File

@ -22,32 +22,36 @@ class LNNode():
return invoice, payment_hash, expires_at return invoice, payment_hash, expires_at
def validate_hodl_invoice_locked(): def validate_hodl_invoice_locked(payment_hash):
'''Generates hodl invoice to publish an order''' '''Generates hodl invoice to publish an order'''
return True return True
def validate_ln_invoice(invoice): # num_satoshis def validate_ln_invoice(invoice, num_satoshis): # num_satoshis
'''Checks if the submited LN invoice is as expected''' '''Checks if the submited LN invoice is as expected'''
valid = True valid = True
num_satoshis = 50000 # TODO decrypt and confirm sats are as expected context = None
description = 'Placeholder desc' # TODO decrypt from LN invoice description = 'Placeholder desc' # TODO decrypt from LN invoice
payment_hash = '567126' # TODO decrypt payment_hash = '567&*GIHU126' # TODO decrypt
expires_at = timezone.now() # TODO decrypt expires_at = timezone.now() # TODO decrypt
return valid, num_satoshis, description, payment_hash, expires_at return valid, context, description, payment_hash, expires_at
def pay_buyer_invoice(invoice): def pay_invoice(invoice):
'''Sends sats to buyer''' '''Sends sats to buyer, or cancelinvoices'''
return True return True
def charge_hodl_htlcs(invoice): def settle_hodl_htlcs(payment_hash):
'''Charges a LN hodl invoice''' '''Charges a LN hodl invoice'''
return True return True
def free_hodl_htlcs(invoice): def return_hodl_htlcs(payment_hash):
'''Returns sats''' '''Returns sats'''
return True return True
def double_check_htlc_is_settled(payment_hash):
''' Just as it sounds. Better safe than sorry!'''
return True

View File

@ -1,10 +1,10 @@
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
import requests
from .lightning import LNNode from .lightning import LNNode
from .models import Order, LNPayment, User from .models import Order, LNPayment, MarketTick, User
from decouple import config from decouple import config
from .utils import get_exchange_rate
FEE = float(config('FEE')) FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE')) BOND_SIZE = float(config('BOND_SIZE'))
@ -23,8 +23,6 @@ ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
class Logics(): class Logics():
# escrow_user = User.objects.get(username=ESCROW_USERNAME)
def validate_already_maker_or_taker(user): def validate_already_maker_or_taker(user):
'''Checks if the user is already partipant of an order''' '''Checks if the user is already partipant of an order'''
queryset = Order.objects.filter(maker=user) queryset = Order.objects.filter(maker=user)
@ -63,11 +61,9 @@ class Logics():
if order.is_explicit: if order.is_explicit:
satoshis_now = order.satoshis satoshis_now = order.satoshis
else: else:
# TODO Add fallback Public APIs and error handling exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
# Think about polling price data in a different way (e.g. store locally every t seconds) premium_rate = exchange_rate * (1+float(order.premium)/100)
market_prices = requests.get(MARKET_PRICE_API).json() satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000
exchange_rate = float(market_prices[Order.currency_dict[str(order.currency)]]['last'])
satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000
return int(satoshis_now) return int(satoshis_now)
@ -89,10 +85,19 @@ class Logics():
@classmethod @classmethod
def update_invoice(cls, order, user, invoice): 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 # only the buyer can post a buyer invoice
if not (cls.is_buyer(order, user) or is_valid_invoice): if not cls.is_buyer(order, user):
return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'} return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
if not order.taker_bond:
return False, {'bad_request':'Wait for your order to be taken.'}
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED):
return False, {'bad_request':'You cannot a invoice while bonds are not posted.'}
num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount']
valid, context, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice, num_satoshis)
if not valid:
return False, context
order.buyer_invoice, _ = LNPayment.objects.update_or_create( order.buyer_invoice, _ = LNPayment.objects.update_or_create(
concept = LNPayment.Concepts.PAYBUYER, concept = LNPayment.Concepts.PAYBUYER,
@ -130,22 +135,29 @@ class Logics():
@classmethod @classmethod
def rate_counterparty(cls, order, user, rating): def rate_counterparty(cls, order, user, rating):
# if maker, rates taker
if order.maker == user: # If the trade is finished
order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1 if order.status > Order.Status.PAY:
last_ratings = list(order.taker.profile.last_ratings).append(rating)
order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings) # if maker, rates taker
# if taker, rates maker if order.maker == user:
if order.taker == user: order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1
order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1 last_ratings = list(order.taker.profile.last_ratings).append(rating)
last_ratings = list(order.maker.profile.last_ratings).append(rating) order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
order.maker.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)
else:
return False, {'bad_request':'You cannot rate your counterparty yet.'}
order.save() order.save()
return True, None return True, None
@classmethod @classmethod
def cancel_order(cls, order, user, state): def cancel_order(cls, order, user, state=None):
# 1) When maker cancels before bond # 1) When maker cancels before bond
'''The order never shows up on the book and order '''The order never shows up on the book and order
@ -292,3 +304,54 @@ class Logics():
order.save() order.save()
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis} return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}
def settle_escrow(order):
''' Settles the trade escrow HTLC'''
# TODO ERROR HANDLING
valid = LNNode.settle_hodl_htlcs(order.trade_escrow.payment_hash)
return valid
def pay_buyer_invoice(order):
''' Settles the trade escrow HTLC'''
# TODO ERROR HANDLING
valid = LNNode.pay_invoice(order.buyer_invoice.payment_hash)
return valid
@classmethod
def confirm_fiat(cls, order, user):
''' If Order is in the CHAT states:
If user is buyer: mark FIAT SENT and settle escrow!
If User is the seller and FIAT is SENT: Pay buyer invoice!'''
if order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Alternatively, if all collateral is locked? test out
# If buyer, settle escrow and mark fiat sent
if cls.is_buyer(order, user):
if cls.settle_escrow(order): # KEY LINE - SETTLES THE TRADE ESCROW !!
order.trade_escrow.status = LNPayment.Status.SETLED
order.status = Order.Status.FSE
order.is_fiat_sent = True
# If seller and fiat sent, pay buyer invoice
elif cls.is_seller(order, user):
if not order.is_fiat_sent:
return False, {'bad_request':'You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer.'}
# Double check the escrow is settled.
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
# Make sure the trade escrow is at least as big as the buyer invoice
if order.trade_escrow.num_satoshis <= order.buyer_invoice.num_satoshis:
return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'}
# Double check the trade escrow is settled
elif cls.pay_buyer_invoice(order): # KEY LINE - PAYS THE BUYER !!
order.status = Order.Status.PAY
order.buyer_invoice.status = LNPayment.Status.PAYING
else:
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
order.save()
return True, None

View File

@ -7,19 +7,14 @@ from django.utils.html import mark_safe
from decouple import config from decouple import config
from pathlib import Path from pathlib import Path
from .utils import get_exchange_rate
import json import json
#############################
# TODO
# Load hparams from .env file
MIN_TRADE = int(config('MIN_TRADE')) MIN_TRADE = int(config('MIN_TRADE'))
MAX_TRADE = int(config('MAX_TRADE')) MAX_TRADE = int(config('MAX_TRADE'))
FEE = float(config('FEE')) FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE')) BOND_SIZE = float(config('BOND_SIZE'))
class LNPayment(models.Model): class LNPayment(models.Model):
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
@ -39,7 +34,8 @@ class LNPayment(models.Model):
RETNED = 3, 'Returned' RETNED = 3, 'Returned'
MISSNG = 4, 'Missing' MISSNG = 4, 'Missing'
VALIDI = 5, 'Valid' VALIDI = 5, 'Valid'
INFAIL = 6, 'Failed routing' PAYING = 6, 'Paying ongoing'
FAILRO = 7, 'Failed routing'
# payment use details # payment use details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL) type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL)
@ -74,24 +70,24 @@ class Order(models.Model):
DEL = 2, 'Deleted' DEL = 2, 'Deleted'
TAK = 3, 'Waiting for taker bond' TAK = 3, 'Waiting for taker bond'
UCA = 4, 'Cancelled' UCA = 4, 'Cancelled'
WF2 = 5, 'Waiting for trade collateral and buyer invoice' EXP = 5, 'Expired'
WFE = 6, 'Waiting only for seller trade collateral' WF2 = 6, 'Waiting for trade collateral and buyer invoice'
WFI = 7, 'Waiting only for buyer invoice' WFE = 7, 'Waiting only for seller trade collateral'
CHA = 8, 'Sending fiat - In chatroom' WFI = 8, 'Waiting only for buyer invoice'
CCA = 9, 'Collaboratively cancelled' CHA = 9, 'Sending fiat - In chatroom'
FSE = 10, 'Fiat sent - In chatroom' FSE = 10, 'Fiat sent - In chatroom'
FCO = 11, 'Fiat confirmed' DIS = 11, 'In dispute'
SUC = 12, 'Sucessfully settled' CCA = 12, 'Collaboratively cancelled'
FAI = 13, 'Failed lightning network routing' PAY = 13, 'Sending satoshis to buyer'
UPI = 14, 'Updated invoice' SUC = 14, 'Sucessfully settled'
DIS = 15, 'In dispute' FAI = 15, 'Failed lightning network routing'
MLD = 16, 'Maker lost dispute' MLD = 16, 'Maker lost dispute'
TLD = 17, 'Taker lost dispute' TLD = 17, 'Taker lost dispute'
EXP = 18, 'Expired'
currency_dict = json.load(open('./api/currencies.json')) currency_dict = json.load(open('./api/currencies.json'))
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())] currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
print(currency_choices) print(currency_choices)
# order info # order info
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@ -101,7 +97,7 @@ class Order(models.Model):
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False) currency = models.PositiveSmallIntegerField(choices=currency_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(0.00001)])
payment_method = models.CharField(max_length=30, null=False, default="not specified", blank=True) payment_method = models.CharField(max_length=50, null=False, default="not specified", blank=True)
# order 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) is_explicit = models.BooleanField(default=False, null=False)
@ -117,8 +113,11 @@ class Order(models.Model):
maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order 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 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. is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
is_disputed = models.BooleanField(default=False, null=False)
is_fiat_sent = models.BooleanField(default=False, null=False)
# order collateral # HTLCs
# Order collateral
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) 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) 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) trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
@ -126,9 +125,13 @@ class Order(models.Model):
# buyer payment LN invoice # buyer payment LN invoice
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
# cancel LN invoice // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
def __str__(self): def __str__(self):
# Make relational back to ORDER # Make relational back to ORDER
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {self.amount} {self.currency_dict[str(self.currency)]}') return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}')
@receiver(pre_delete, sender=Order) @receiver(pre_delete, sender=Order)
def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs):
@ -183,3 +186,48 @@ class Profile(models.Model):
def avatar_tag(self): def avatar_tag(self):
return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar()) return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
class MarketTick(models.Model):
'''
Records tick by tick Non-KYC Bitcoin price.
Data to be aggregated and offered via public API.
It is checked against current cex prices for nice
insight on the historical premium of Non-KYC BTC
Price is set when both taker bond is locked. Both
maker and taker are commited with bonds (contract
is finished and cancellation has a cost)
'''
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
currency = models.PositiveSmallIntegerField(choices=Order.currency_choices, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
fee = models.DecimalField(max_digits=4, decimal_places=4, default=FEE, validators=[MinValueValidator(0), MaxValueValidator(1)])
def log_a_tick(order):
'''
Adds a new tick
'''
if not order.taker_bond:
return None
elif order.taker_bond.status == LNPayment.Status.LOCKED:
volume = order.last_satoshis / 100000000
price = float(order.amount) / volume # Amount Fiat / Amount BTC
premium = 100 * (price / get_exchange_rate(Order.currency_dict[str(order.currency)]) - 1)
tick = MarketTick.objects.create(
price=price,
volume=volume,
premium=premium,
currency=order.currency)
tick.save()
def __str__(self):
return f'Tick: {self.id}'

View File

@ -1,10 +1,11 @@
from django.urls import path from django.urls import path
from .views import OrderMakerView, OrderView, UserView, BookView, get_currencies_json from .views import MakerView, OrderView, UserView, BookView, InfoView, get_currencies_json
urlpatterns = [ urlpatterns = [
path('make/', OrderMakerView.as_view()), path('make/', MakerView.as_view()),
path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})), path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
path('usergen/', UserView.as_view()), path('usergen/', UserView.as_view()),
path('book/', BookView.as_view()), path('book/', BookView.as_view()),
path('info/', InfoView.as_view()),
path('currencies/', get_currencies_json), path('currencies/', get_currencies_json),
] ]

12
api/utils.py Normal file
View File

@ -0,0 +1,12 @@
from decouple import config
import requests
def get_exchange_rate(currency):
# 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(config('MARKET_PRICE_API')).json()
exchange_rate = float(market_prices[currency]['last'])
return exchange_rate

View File

@ -25,13 +25,14 @@ import json
from django.http import HttpResponse from django.http import HttpResponse
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
FEE = float(config('FEE'))
avatar_path = Path('frontend/static/assets/avatars') avatar_path = Path('frontend/static/assets/avatars')
avatar_path.mkdir(parents=True, exist_ok=True) avatar_path.mkdir(parents=True, exist_ok=True)
# Create your views here. # Create your views here.
class OrderMakerView(CreateAPIView): class MakerView(CreateAPIView):
serializer_class = MakeOrderSerializer serializer_class = MakeOrderSerializer
def post(self,request): def post(self,request):
@ -125,6 +126,18 @@ class OrderView(viewsets.ViewSet):
data['maker_nick'] = str(order.maker) data['maker_nick'] = str(order.maker)
data['taker_nick'] = str(order.taker) data['taker_nick'] = str(order.taker)
data['status_message'] = Order.Status(order.status).label data['status_message'] = Order.Status(order.status).label
data['is_fiat_sent'] = order.is_fiat_sent
data['is_disputed'] = order.is_disputed
# If both bonds are locked, participants can see the trade in sats is also final.
if order.taker_bond:
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
# Seller sees the amount he pays
if data['is_seller']:
data['trade_satoshis'] = order.last_satoshis
# Buyer sees the amount he receives
elif data['is_buyer']:
data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount']
# 5) If status is 'waiting for maker bond' and user is MAKER, 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']: if order.status == Order.Status.WFB and data['is_maker']:
@ -170,6 +183,10 @@ class OrderView(viewsets.ViewSet):
# add whether a collaborative cancel is pending # add whether a collaborative cancel is pending
data['pending_cancel'] = order.is_pending_cancel data['pending_cancel'] = order.is_pending_cancel
# 9) if buyer confirmed FIAT SENT
elif order.status == Order.Status.FSE:
data['buyer_confirmed']
return Response(data, status.HTTP_200_OK) return Response(data, status.HTTP_200_OK)
def take_update_confirm_dispute_cancel(self, request, format=None): def take_update_confirm_dispute_cancel(self, request, format=None):
@ -198,32 +215,42 @@ class OrderView(viewsets.ViewSet):
Logics.take(order, request.user) Logics.take(order, request.user)
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST) else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
# 2) If action is update (invoice) # Any other action is only allowed if the user is a participant
elif action == 'update_invoice' and invoice: if not (order.maker == request.user or order.taker == request.user):
return Response({'bad_request':'You are not a participant in this order'}, status.HTTP_403_FORBIDDEN)
# 2) If action is 'update invoice'
if action == 'update_invoice' and invoice:
valid, context = Logics.update_invoice(order,request.user,invoice) valid, context = Logics.update_invoice(order,request.user,invoice)
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 3) If action is cancel # 3) If action is cancel
elif action == 'cancel': elif action == 'cancel':
valid, context = Logics.cancel_order(order,request.user) valid, context = Logics.cancel_order(order,request.user)
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 4) If action is confirm # 4) If action is confirm
elif action == 'confirm': elif action == 'confirm':
pass valid, context = Logics.confirm_fiat(order,request.user)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 5) If action is dispute # 5) If action is dispute
elif action == 'dispute': elif action == 'dispute':
pass valid, context = Logics.open_dispute(order,request.user, rating)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is dispute # 6) If action is rate
elif action == 'rate' and rating: elif action == 'rate' and rating:
valid, context = Logics.rate_counterparty(order,request.user, rating) valid, context = Logics.rate_counterparty(order,request.user, rating)
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# If nothing... something else is going on. Probably not allowed! # If nothing of the above... something else is going on. Probably not allowed!
else: else:
return Response({'bad_request':'The Robotic Satoshis working in the warehouse did not understand you'}) return Response(
{'bad_request':
'The Robotic Satoshis working in the warehouse did not understand you. ' +
'Please, fill a Bug Issue in Github https://github.com/Reckless-Satoshi/robosats/issues'},
status.HTTP_501_NOT_IMPLEMENTED)
return self.get(request) return self.get(request)
@ -298,23 +325,27 @@ class UserView(APIView):
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
context['found'] = 'Bad luck, this nickname is taken' context['found'] = 'Bad luck, this nickname is taken'
context['bad_request'] = 'Enter a different token' context['bad_request'] = 'Enter a different token'
return Response(context, status=status.HTTP_403_FORBIDDEN) return Response(context, status.HTTP_403_FORBIDDEN)
def delete(self,request): def delete(self,request):
user = User.objects.get(id = request.user.id) ''' Pressing "give me another" deletes the logged in user '''
user = request.user
if not user:
return Response(status.HTTP_403_FORBIDDEN)
# TO DO. Pressing "give me another" deletes the logged in user # Only delete if user life is shorter than 30 minutes. Helps deleting users by mistake
# However it might be a long time recovered user if user.date_joined < (timezone.now() - timedelta(minutes=30)):
# Only delete if user live is < 5 minutes return Response(status.HTTP_400_BAD_REQUEST)
# TODO check if user exists AND it is not a maker or taker! # Check if it is not a maker or taker!
if user is not None: if not Logics.validate_already_maker_or_taker(user):
logout(request) return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
user.delete()
logout(request)
user.delete()
return Response({'user_deleted':'User deleted permanently'}, 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(ListAPIView): class BookView(ListAPIView):
serializer_class = ListOrderSerializer serializer_class = ListOrderSerializer
@ -342,6 +373,19 @@ class BookView(ListAPIView):
return Response(book_data, status=status.HTTP_200_OK) return Response(book_data, status=status.HTTP_200_OK)
class InfoView(ListAPIView):
def get(self):
context = {}
context['num_public_buy_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB))
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB))
context['last_day_avg_btc_premium'] = None # Todo
context['num_active_robots'] = None
context['total_volume'] = None
return Response(context, status.HTTP_200_OK)
def get_currencies_json(request): def get_currencies_json(request):
currency_dict = json.load(open('./api/currencies.json')) currency_dict = json.load(open('./api/currencies.json'))
return HttpResponse(json.dumps(currency_dict),content_type="application/json") return HttpResponse(json.dumps(currency_dict),content_type="application/json")

View File

@ -193,7 +193,8 @@ export default class MakerPage extends Component {
type="text" type="text"
require={true} require={true}
inputProps={{ inputProps={{
style: {textAlign:"center"} style: {textAlign:"center"},
maxLength: 50
}} }}
onChange={this.handlePaymentMethodChange} onChange={this.handlePaymentMethodChange}
/> />