mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-14 11:26:24 +00:00
merge with branch logics-second-iteration
This commit is contained in:
commit
ec823586c1
17
.env-sample
Normal file
17
.env-sample
Normal 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'
|
16
api/admin.py
16
api/admin.py
@ -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']
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
111
api/logics.py
111
api/logics.py
@ -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
|
||||||
|
@ -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}'
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
12
api/utils.py
Normal 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
|
88
api/views.py
88
api/views.py
@ -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")
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user