diff --git a/.env-sample b/.env-sample
index 810c588e..c6428c8f 100644
--- a/.env-sample
+++ b/.env-sample
@@ -55,8 +55,10 @@ FEE = 0.002
# Shall incentivize order making
MAKER_FEE_SPLIT=0.125
-# Default bond size as fraction
-BOND_SIZE = 0.01
+# Bond size as percentage (%)
+DEFAULT_BOND_SIZE = 1
+MIN_BOND_SIZE = 1
+MAX_BOND_SIZE = 15
# Time out penalty for canceling takers in SECONDS
PENALTY_TIMEOUT = 60
@@ -69,6 +71,7 @@ MAX_PUBLIC_ORDERS = 100
# Trade limits in satoshis
MIN_TRADE = 20000
MAX_TRADE = 800000
+MAX_TRADE_BONDLESS_TAKER = 50000
# Expiration (CLTV_expiry) time for HODL invoices in HOURS // 7 min/block assumed
BOND_EXPIRY = 54
@@ -79,7 +82,10 @@ EXP_MAKER_BOND_INVOICE = 300
EXP_TAKER_BOND_INVOICE = 200
# Time a order is public in the book HOURS
-PUBLIC_ORDER_DURATION = 6
+DEFAULT_PUBLIC_ORDER_DURATION = 24
+MAX_PUBLIC_ORDER_DURATION = 24
+MIN_PUBLIC_ORDER_DURATION = 0.166
+
# Time to provide a valid invoice and the trade escrow MINUTES
INVOICE_AND_ESCROW_DURATION = 30
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
diff --git a/.gitignore b/.gitignore
index 91952b07..e7dd4ef0 100755
--- a/.gitignore
+++ b/.gitignore
@@ -651,4 +651,5 @@ api/lightning/invoices*
api/lightning/router*
api/lightning/googleapis*
frontend/static/admin*
-frontend/static/rest_framework*
\ No newline at end of file
+frontend/static/rest_framework*
+frontend/static/import_export*
\ No newline at end of file
diff --git a/api/admin.py b/api/admin.py
index 15fa6929..0b45bd9a 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -13,21 +13,25 @@ class ProfileInline(admin.StackedInline):
can_delete = False
fields = ("avatar_tag", )
readonly_fields = ["avatar_tag"]
-
+ show_change_link = True
# extended users with avatars
@admin.register(User)
-class EUserAdmin(UserAdmin):
+class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
inlines = [ProfileInline]
list_display = (
"avatar_tag",
"id",
+ "profile_link",
"username",
"last_login",
"date_joined",
"is_staff",
)
list_display_links = ("id", "username")
+ change_links = (
+ "profile",
+ )
ordering = ("-id", )
def avatar_tag(self, obj):
@@ -42,7 +46,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"maker_link",
"taker_link",
"status",
- "amount",
+ "amt",
"currency_link",
"t0_satoshis",
"is_disputed",
@@ -65,7 +69,13 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"trade_escrow",
)
list_filter = ("is_disputed", "is_fiat_sent", "type", "currency", "status")
- search_fields = ["id","amount"]
+ search_fields = ["id","amount","min_amount","max_amount"]
+
+ def amt(self, obj):
+ if obj.has_range and obj.amount == None:
+ return str(float(obj.min_amount))+"-"+ str(float(obj.max_amount))
+ else:
+ return float(obj.amount)
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@@ -74,6 +84,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"concept",
"status",
"num_satoshis",
+ "fee",
"type",
"expires_at",
"expiry_height",
@@ -95,7 +106,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
)
list_filter = ("type", "concept", "status")
ordering = ("-expires_at", )
- search_fields = ["payment_hash","num_satoshis"]
+ search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"]
@admin.register(Profile)
@@ -116,9 +127,11 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"num_disputes",
"lost_disputes",
)
+ list_editable = ["pending_rewards", "earned_rewards"]
list_display_links = ("avatar_tag", "id")
change_links = ["user"]
readonly_fields = ["avatar_tag"]
+ search_fields = ["user__username","id"]
@admin.register(Currency)
@@ -128,7 +141,6 @@ class CurrencieAdmin(admin.ModelAdmin):
readonly_fields = ("currency", "exchange_rate", "timestamp")
ordering = ("id", )
-
@admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin):
list_display = ("timestamp", "price", "volume", "premium", "currency",
diff --git a/api/lightning/node.py b/api/lightning/node.py
index 28ad6847..a9bec2e4 100644
--- a/api/lightning/node.py
+++ b/api/lightning/node.py
@@ -261,6 +261,7 @@ class LNNode:
if response.status == 2: # STATUS 'SUCCEEDED'
lnpayment.status = LNPayment.Status.SUCCED
+ lnpayment.fee = float(response.fee_msat)/1000
lnpayment.save()
return True, None
diff --git a/api/logics.py b/api/logics.py
index 10bd5d78..c0dbb5b4 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -14,7 +14,6 @@ import time
FEE = float(config("FEE"))
MAKER_FEE_SPLIT = float(config("MAKER_FEE_SPLIT"))
-BOND_SIZE = float(config("BOND_SIZE"))
ESCROW_USERNAME = config("ESCROW_USERNAME")
PENALTY_TIMEOUT = int(config("PENALTY_TIMEOUT"))
@@ -27,7 +26,6 @@ EXP_TAKER_BOND_INVOICE = int(config("EXP_TAKER_BOND_INVOICE"))
BOND_EXPIRY = int(config("BOND_EXPIRY"))
ESCROW_EXPIRY = int(config("ESCROW_EXPIRY"))
-PUBLIC_ORDER_DURATION = int(config("PUBLIC_ORDER_DURATION"))
INVOICE_AND_ESCROW_DURATION = int(config("INVOICE_AND_ESCROW_DURATION"))
FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION"))
@@ -89,24 +87,65 @@ class Logics:
return True, None, None
- def validate_order_size(order):
- """Validates if order is withing limits in satoshis at t0"""
- if order.t0_satoshis > MAX_TRADE:
+ @classmethod
+ def validate_order_size(cls, order):
+ """Validates if order size in Sats is within limits at t0"""
+ if not order.has_range:
+ if order.t0_satoshis > MAX_TRADE:
+ return False, {
+ "bad_request":
+ "Your order is too big. It is worth " +
+ "{:,}".format(order.t0_satoshis) +
+ " Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
+ " Sats"
+ }
+ if order.t0_satoshis < MIN_TRADE:
+ return False, {
+ "bad_request":
+ "Your order is too small. It is worth " +
+ "{:,}".format(order.t0_satoshis) +
+ " Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
+ " Sats"
+ }
+ elif order.has_range:
+ min_sats = cls.calc_sats(order.min_amount, order.currency.exchange_rate, order.premium)
+ max_sats = cls.calc_sats(order.max_amount, order.currency.exchange_rate, order.premium)
+ if min_sats > max_sats/1.5:
+ return False, {
+ "bad_request":
+ "Maximum range amount must be at least 50 percent higher than the minimum amount"
+ }
+ elif max_sats > MAX_TRADE:
+ return False, {
+ "bad_request":
+ "Your order maximum amount is too big. It is worth " +
+ "{:,}".format(int(max_sats)) +
+ " Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
+ " Sats"
+ }
+ elif min_sats < MIN_TRADE:
+ return False, {
+ "bad_request":
+ "Your order minimum amount is too small. It is worth " +
+ "{:,}".format(int(min_sats)) +
+ " Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
+ " Sats"
+ }
+ elif min_sats < max_sats/5:
+ return False, {
+ "bad_request":
+ f"Your order amount range is too large. Max amount can only be 5 times bigger than min amount"
+ }
+
+ return True, None
+
+ def validate_amount_within_range(order, amount):
+ if amount > float(order.max_amount) or amount < float(order.min_amount):
return False, {
"bad_request":
- "Your order is too big. It is worth " +
- "{:,}".format(order.t0_satoshis) +
- " Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
- " Sats"
- }
- if order.t0_satoshis < MIN_TRADE:
- return False, {
- "bad_request":
- "Your order is too small. It is worth " +
- "{:,}".format(order.t0_satoshis) +
- " Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
- " Sats"
+ "The amount specified is outside the range specified by the maker"
}
+
return True, None
def user_activity_status(last_seen):
@@ -118,7 +157,7 @@ class Logics:
return "Inactive"
@classmethod
- def take(cls, order, user):
+ def take(cls, order, user, amount=None):
is_penalized, time_out = cls.is_penalized(user)
if is_penalized:
return False, {
@@ -126,10 +165,12 @@ class Logics:
f"You need to wait {time_out} seconds to take an order",
}
else:
+ if order.has_range:
+ order.amount= amount
order.taker = user
order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.TAK])
+ seconds=order.t_to_expire(Order.Status.TAK))
order.save()
# send_message.delay(order.id,'order_taken') # Too spammy
return True, None
@@ -146,15 +187,19 @@ class Logics:
return (is_maker and order.type == Order.Types.SELL) or (
is_taker and order.type == Order.Types.BUY)
- def satoshis_now(order):
+ def calc_sats(amount, exchange_rate, premium):
+ exchange_rate = float(exchange_rate)
+ premium_rate = exchange_rate * (1 + float(premium) / 100)
+ return (float(amount) /premium_rate) * 100 * 1000 * 1000
+
+ @classmethod
+ def satoshis_now(cls, order):
"""checks trade amount in sats"""
if order.is_explicit:
satoshis_now = order.satoshis
else:
- exchange_rate = float(order.currency.exchange_rate)
- premium_rate = exchange_rate * (1 + float(order.premium) / 100)
- satoshis_now = (float(order.amount) /
- premium_rate) * 100 * 1000 * 1000
+ amount = order.amount if order.amount != None else order.max_amount
+ satoshis_now = cls.calc_sats(amount, order.currency.exchange_rate, order.premium)
return int(satoshis_now)
@@ -165,8 +210,8 @@ class Logics:
premium = order.premium
price = exchange_rate * (1 + float(premium) / 100)
else:
- order_rate = float(
- order.amount) / (float(order.satoshis) / 100000000)
+ amount = order.amount if not order.has_range else order.max_amount
+ order_rate = float(amount) / (float(order.satoshis) / 100000000)
premium = order_rate / exchange_rate - 1
premium = int(premium * 10000) / 100 # 2 decimals left
price = order_rate
@@ -336,7 +381,7 @@ class Logics:
order.is_disputed = True
order.status = Order.Status.DIS
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.DIS])
+ seconds=order.t_to_expire(Order.Status.DIS))
order.save()
# User could be None if a dispute is open automatically due to weird expiration.
@@ -380,7 +425,7 @@ class Logics:
if order.maker_statement not in [None,""] and order.taker_statement not in [None,""]:
order.status = Order.Status.WFR
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.WFR])
+ seconds=order.t_to_expire(Order.Status.WFR))
order.save()
return True, None
@@ -442,6 +487,12 @@ class Logics:
"bad_request":
"You cannot submit a invoice while bonds are not locked."
}
+ if order.status == Order.Status.FAI:
+ if order.payout.status != LNPayment.Status.EXPIRE:
+ return False, {
+ "bad_request":
+ "You cannot submit an invoice only after expiration or 3 failed attempts"
+ }
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
@@ -472,7 +523,7 @@ class Logics:
if order.status == Order.Status.WFI:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.CHA])
+ seconds=order.t_to_expire(Order.Status.CHA))
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
@@ -483,7 +534,7 @@ class Logics:
elif order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.CHA])
+ seconds=order.t_to_expire(Order.Status.CHA))
else:
order.status = Order.Status.WFE
@@ -661,7 +712,9 @@ class Logics:
def publish_order(order):
order.status = Order.Status.PUB
order.expires_at = order.created_at + timedelta(
- seconds=Order.t_to_expire[Order.Status.PUB])
+ seconds=order.t_to_expire(Order.Status.PUB))
+ if order.has_range:
+ order.amount = None
order.save()
# send_message.delay(order.id,'order_published') # too spammy
return
@@ -699,7 +752,7 @@ class Logics:
# If there was no maker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
- bond_satoshis = int(order.last_satoshis * BOND_SIZE)
+ bond_satoshis = int(order.last_satoshis * order.bond_size/100)
description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."
@@ -708,7 +761,7 @@ class Logics:
hold_payment = LNNode.gen_hold_invoice(
bond_satoshis,
description,
- invoice_expiry=Order.t_to_expire[Order.Status.WFB],
+ invoice_expiry=order.t_to_expire(Order.Status.WFB),
cltv_expiry_secs=BOND_EXPIRY * 3600,
)
except Exception as e:
@@ -759,7 +812,7 @@ class Logics:
# With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.WF2])
+ seconds=order.t_to_expire(Order.Status.WF2))
order.status = Order.Status.WF2
order.save()
@@ -809,7 +862,7 @@ class Logics:
# If there was no taker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
- bond_satoshis = int(order.last_satoshis * BOND_SIZE)
+ bond_satoshis = int(order.last_satoshis * order.bond_size/100)
pos_text = "Buying" if cls.is_buyer(order, user) else "Selling"
description = (
f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
@@ -822,7 +875,7 @@ class Logics:
hold_payment = LNNode.gen_hold_invoice(
bond_satoshis,
description,
- invoice_expiry=Order.t_to_expire[Order.Status.TAK],
+ invoice_expiry=order.t_to_expire(Order.Status.TAK),
cltv_expiry_secs=BOND_EXPIRY * 3600,
)
@@ -850,7 +903,7 @@ class Logics:
)
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.TAK])
+ seconds=order.t_to_expire(Order.Status.TAK))
order.save()
return True, {
"bond_invoice": hold_payment["invoice"],
@@ -866,7 +919,7 @@ class Logics:
elif order.status == Order.Status.WFE:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.CHA])
+ seconds=order.t_to_expire(Order.Status.CHA))
order.save()
@classmethod
@@ -909,7 +962,7 @@ class Logics:
hold_payment = LNNode.gen_hold_invoice(
escrow_satoshis,
description,
- invoice_expiry=Order.t_to_expire[Order.Status.WF2],
+ invoice_expiry=order.t_to_expire(Order.Status.WF2),
cltv_expiry_secs=ESCROW_EXPIRY * 3600,
)
diff --git a/api/management/commands/telegram_watcher.py b/api/management/commands/telegram_watcher.py
index 3c5e4483..e4afe6a3 100644
--- a/api/management/commands/telegram_watcher.py
+++ b/api/management/commands/telegram_watcher.py
@@ -45,11 +45,19 @@ class Command(BaseCommand):
except:
print(f'No profile with token {token}')
continue
- profile.telegram_chat_id = result['message']['from']['id']
- profile.telegram_lang_code = result['message']['from']['language_code']
- self.telegram.welcome(profile.user)
- profile.telegram_enabled = True
- profile.save()
+
+ attempts = 5
+ while attempts >= 0:
+ try:
+ profile.telegram_chat_id = result['message']['from']['id']
+ profile.telegram_lang_code = result['message']['from']['language_code']
+ self.telegram.welcome(profile.user)
+ profile.telegram_enabled = True
+ profile.save()
+ break
+ except:
+ time.sleep(5)
+ attempts = attempts - 1
offset = response['result'][-1]['update_id']
diff --git a/api/models.py b/api/models.py
index 7fc1c130..d7094ca2 100644
--- a/api/models.py
+++ b/api/models.py
@@ -5,6 +5,7 @@ from django.core.validators import (
MinValueValidator,
validate_comma_separated_integer_list,
)
+from django.utils import timezone
from django.db.models.signals import post_save, pre_delete
from django.template.defaultfilters import truncatechars
from django.dispatch import receiver
@@ -19,7 +20,7 @@ import json
MIN_TRADE = int(config("MIN_TRADE"))
MAX_TRADE = int(config("MAX_TRADE"))
FEE = float(config("FEE"))
-BOND_SIZE = float(config("BOND_SIZE"))
+DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
class Currency(models.Model):
@@ -38,7 +39,7 @@ class Currency(models.Model):
null=True,
validators=[MinValueValidator(0)],
)
- timestamp = models.DateTimeField(auto_now_add=True)
+ timestamp = models.DateTimeField(default=timezone.now)
def __str__(self):
# returns currency label ( 3 letters code)
@@ -105,9 +106,11 @@ class LNPayment(models.Model):
default=None,
blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[
- MinValueValidator(MIN_TRADE * BOND_SIZE),
- MaxValueValidator(MAX_TRADE * (1 + BOND_SIZE + FEE)),
+ MinValueValidator(100),
+ MaxValueValidator(MAX_TRADE * (1 + DEFAULT_BOND_SIZE + FEE)),
])
+ # Fee in sats with mSats decimals fee_msat
+ fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False)
created_at = models.DateTimeField()
expires_at = models.DateTimeField()
cltv_expiry = models.PositiveSmallIntegerField(null=True,
@@ -181,7 +184,7 @@ class Order(models.Model):
status = models.PositiveSmallIntegerField(choices=Status.choices,
null=False,
default=Status.WFB)
- created_at = models.DateTimeField(auto_now_add=True)
+ created_at = models.DateTimeField(default=timezone.now)
expires_at = models.DateTimeField()
# order details
@@ -189,14 +192,15 @@ class Order(models.Model):
currency = models.ForeignKey(Currency,
null=True,
on_delete=models.SET_NULL)
- amount = models.DecimalField(max_digits=16,
- decimal_places=8,
- validators=[MinValueValidator(0.00000001)])
+ amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
+ has_range = models.BooleanField(default=False, null=False, blank=False)
+ min_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
+ max_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
payment_method = models.CharField(max_length=35,
null=False,
default="not specified",
blank=True)
-
+ bondless_taker = models.BooleanField(default=False, null=False, blank=False)
# 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 market
@@ -218,6 +222,29 @@ class Order(models.Model):
],
blank=True,
)
+ # optionally makers can choose the public order duration length (seconds)
+ public_duration = models.PositiveBigIntegerField(
+ default=60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1,
+ null=False,
+ validators=[
+ MinValueValidator(60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))), # Min is 10 minutes
+ MaxValueValidator(60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))), # Max is 24 Hours
+ ],
+ blank=False,
+ )
+ # optionally makers can choose the fidelity bond size of the maker and taker (%)
+ bond_size = models.DecimalField(
+ max_digits=4,
+ decimal_places=2,
+ default=DEFAULT_BOND_SIZE,
+ null=False,
+ validators=[
+ MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 %
+ MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 %
+ ],
+ blank=False,
+ )
+
# how many sats at creation and at last check (relevant for marked to market)
t0_satoshis = models.PositiveBigIntegerField(
null=True,
@@ -311,30 +338,38 @@ class Order(models.Model):
maker_platform_rated = models.BooleanField(default=False, null=False)
taker_platform_rated = models.BooleanField(default=False, null=False)
- t_to_expire = {
- 0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
- 1: 60 * 60 * int(config("PUBLIC_ORDER_DURATION")), # 'Public'
- 2: 0, # 'Deleted'
- 3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
- 4: 0, # 'Cancelled'
- 5: 0, # 'Expired'
- 6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting for trade collateral and buyer invoice'
- 7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for seller trade collateral'
- 8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for buyer invoice'
- 9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
- 10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom'
- 11: 1 * 24 * 60 * 60, # 'In dispute'
- 12: 0, # 'Collaboratively cancelled'
- 13: 24 * 60 * 60, # 'Sending satoshis to buyer'
- 14: 24 * 60 * 60, # 'Sucessful trade'
- 15: 24 * 60 * 60, # 'Failed lightning network routing'
- 16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
- 17: 24 * 60 * 60, # 'Maker lost dispute'
- 18: 24 * 60 * 60, # 'Taker lost dispute'
- }
-
def __str__(self):
- return f"Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}"
+ if self.has_range and self.amount == None:
+ amt = str(float(self.min_amount))+"-"+ str(float(self.max_amount))
+ else:
+ amt = float(self.amount)
+ return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
+
+ def t_to_expire(self, status):
+
+ t_to_expire = {
+ 0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
+ 1: self.public_duration, # 'Public'
+ 2: 0, # 'Deleted'
+ 3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
+ 4: 0, # 'Cancelled'
+ 5: 0, # 'Expired'
+ 6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting for trade collateral and buyer invoice'
+ 7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for seller trade collateral'
+ 8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for buyer invoice'
+ 9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
+ 10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom'
+ 11: 1 * 24 * 60 * 60, # 'In dispute'
+ 12: 0, # 'Collaboratively cancelled'
+ 13: 24 * 60 * 60, # 'Sending satoshis to buyer'
+ 14: 24 * 60 * 60, # 'Sucessful trade'
+ 15: 24 * 60 * 60, # 'Failed lightning network routing'
+ 16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
+ 17: 24 * 60 * 60, # 'Maker lost dispute'
+ 18: 24 * 60 * 60, # 'Taker lost dispute'
+ }
+
+ return t_to_expire[status]
@receiver(pre_delete, sender=Order)
@@ -393,7 +428,7 @@ class Profile(models.Model):
null=False
)
telegram_lang_code = models.CharField(
- max_length=4,
+ max_length=10,
null=True,
blank=True
)
@@ -529,7 +564,7 @@ class MarketTick(models.Model):
currency = models.ForeignKey(Currency,
null=True,
on_delete=models.SET_NULL)
- timestamp = models.DateTimeField(auto_now_add=True)
+ timestamp = models.DateTimeField(default=timezone.now)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
fee = models.DecimalField(
diff --git a/api/serializers.py b/api/serializers.py
index f1467646..7933c67c 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -14,10 +14,14 @@ class ListOrderSerializer(serializers.ModelSerializer):
"type",
"currency",
"amount",
+ "has_range",
+ "min_amount",
+ "max_amount",
"payment_method",
"is_explicit",
"premium",
"satoshis",
+ "bondless_taker",
"maker",
"taker",
)
@@ -31,13 +35,18 @@ class MakeOrderSerializer(serializers.ModelSerializer):
"type",
"currency",
"amount",
+ "has_range",
+ "min_amount",
+ "max_amount",
"payment_method",
"is_explicit",
"premium",
"satoshis",
+ "public_duration",
+ "bond_size",
+ "bondless_taker",
)
-
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000,
allow_null=True,
@@ -66,6 +75,7 @@ class UpdateOrderSerializer(serializers.Serializer):
allow_blank=True,
default=None,
)
+ amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None)
class ClaimRewardSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000,
diff --git a/api/tasks.py b/api/tasks.py
index 596fb71b..6d9e0d79 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -111,7 +111,7 @@ def follow_send_payment(lnpayment):
lnpayment.save()
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.FAI])
+ seconds=order.t_to_expire(Order.Status.FAI))
order.save()
context = {
"routing_failed":
@@ -123,10 +123,11 @@ def follow_send_payment(lnpayment):
if response.status == 2: # Status 2 'SUCCEEDED'
print("SUCCEEDED")
lnpayment.status = LNPayment.Status.SUCCED
+ lnpayment.fee = float(response.fee_msat)/1000
lnpayment.save()
order.status = Order.Status.SUC
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.SUC])
+ seconds=order.t_to_expire(Order.Status.SUC))
order.save()
return True, None
@@ -138,7 +139,7 @@ def follow_send_payment(lnpayment):
lnpayment.save()
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(
- seconds=Order.t_to_expire[Order.Status.FAI])
+ seconds=order.t_to_expire(Order.Status.FAI))
order.save()
context = {"routing_failed": "The payout invoice has expired"}
return False, context
@@ -191,6 +192,9 @@ def send_message(order_id, message):
from api.messages import Telegram
telegram = Telegram()
+ if message == 'welcome':
+ telegram.welcome(order)
+
if message == 'order_taken':
telegram.order_taken(order)
diff --git a/api/urls.py b/api/urls.py
index e402b1c0..67c7e188 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -1,5 +1,5 @@
from django.urls import path
-from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView
+from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView, LimitView
urlpatterns = [
path("make/", MakerView.as_view()),
@@ -13,5 +13,6 @@ urlpatterns = [
# path('robot/') # Profile Info
path("info/", InfoView.as_view()),
path("price/", PriceView.as_view()),
+ path("limits/", LimitView.as_view()),
path("reward/", RewardView.as_view()),
]
diff --git a/api/utils.py b/api/utils.py
index d918ee2d..f1109d1c 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -101,11 +101,13 @@ def compute_premium_percentile(order):
if len(queryset) <= 1:
return 0.5
- order_rate = float(order.last_satoshis) / float(order.amount)
+ amount = order.amount if not order.has_range else order.max_amount
+ order_rate = float(order.last_satoshis) / float(amount)
rates = []
for similar_order in queryset:
+ similar_order_amount = similar_order.amount if not similar_order.has_range else similar_order.max_amount
rates.append(
- float(similar_order.last_satoshis) / float(similar_order.amount))
+ float(similar_order.last_satoshis) / float(similar_order_amount))
rates = np.array(rates)
return round(np.sum(rates < order_rate) / len(rates), 2)
diff --git a/api/views.py b/api/views.py
index b032f9c5..406735c8 100644
--- a/api/views.py
+++ b/api/views.py
@@ -30,6 +30,8 @@ from decouple import config
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
RETRY_TIME = int(config("RETRY_TIME"))
+PUBLIC_DURATION = 60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1
+BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
avatar_path = Path(settings.AVATAR_ROOT)
avatar_path.mkdir(parents=True, exist_ok=True)
@@ -64,35 +66,76 @@ class MakerView(CreateAPIView):
},
status.HTTP_400_BAD_REQUEST,
)
+ # Only allow users who are not already engaged in an order
+ valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
+ if not valid:
+ return Response(context, status.HTTP_409_CONFLICT)
type = serializer.data.get("type")
currency = serializer.data.get("currency")
amount = serializer.data.get("amount")
+ has_range = serializer.data.get("has_range")
+ min_amount = serializer.data.get("min_amount")
+ max_amount = serializer.data.get("max_amount")
payment_method = serializer.data.get("payment_method")
premium = serializer.data.get("premium")
satoshis = serializer.data.get("satoshis")
is_explicit = serializer.data.get("is_explicit")
+ public_duration = serializer.data.get("public_duration")
+ bond_size = serializer.data.get("bond_size")
+ bondless_taker = serializer.data.get("bondless_taker")
- valid, context, _ = Logics.validate_already_maker_or_taker(
- request.user)
- if not valid:
- return Response(context, status.HTTP_409_CONFLICT)
+ # Optional params
+ if public_duration == None: public_duration = PUBLIC_DURATION
+ if bond_size == None: bond_size = BOND_SIZE
+ if bondless_taker == None: bondless_taker = False
+ if has_range == None: has_range = False
+
+ # An order can either have an amount or a range (min_amount and max_amount)
+ if has_range:
+ amount = None
+ else:
+ min_amount = None
+ max_amount = None
+
+ # Either amount or min_max has to be specified.
+ if has_range and (min_amount == None or max_amount == None):
+ return Response(
+ {
+ "bad_request":
+ "You must specify min_amount and max_amount for a range order"
+ },
+ status.HTTP_400_BAD_REQUEST,
+ )
+ elif not has_range and amount == None:
+ return Response(
+ {
+ "bad_request":
+ "You must specify an order amount"
+ },
+ status.HTTP_400_BAD_REQUEST,
+ )
# Creates a new order
order = Order(
type=type,
currency=Currency.objects.get(id=currency),
amount=amount,
+ has_range=has_range,
+ min_amount=min_amount,
+ max_amount=max_amount,
payment_method=payment_method,
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at=timezone.now() + timedelta(
- seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
+ seconds=EXP_MAKER_BOND_INVOICE),
maker=request.user,
+ public_duration=public_duration,
+ bond_size=bond_size,
+ bondless_taker=bondless_taker,
)
- # TODO move to Order class method when new instance is created!
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
valid, context = Logics.validate_order_size(order)
@@ -155,7 +198,7 @@ class OrderView(viewsets.ViewSet):
)
data = ListOrderSerializer(order).data
- data["total_secs_exp"] = Order.t_to_expire[order.status]
+ data["total_secs_exp"] = order.t_to_expire(order.status)
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
@@ -347,6 +390,10 @@ class OrderView(viewsets.ViewSet):
"""
order_id = request.GET.get(self.lookup_url_kwarg)
+ import sys
+ sys.stdout.write('AAAAAA')
+ print('BBBBB1')
+
serializer = UpdateOrderSerializer(data=request.data)
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
@@ -367,7 +414,17 @@ class OrderView(viewsets.ViewSet):
request.user)
if not valid:
return Response(context, status=status.HTTP_409_CONFLICT)
- valid, context = Logics.take(order, request.user)
+
+ # For order with amount range, set the amount now.
+ if order.has_range:
+ amount = float(serializer.data.get("amount"))
+ valid, context = Logics.validate_amount_within_range(order, amount)
+ if not valid:
+ return Response(context, status=status.HTTP_400_BAD_REQUEST)
+
+ valid, context = Logics.take(order, request.user, amount)
+ else:
+ valid, context = Logics.take(order, request.user)
if not valid:
return Response(context, status=status.HTTP_403_FORBIDDEN)
@@ -684,11 +741,11 @@ class InfoView(ListAPIView):
context["network"] = config("NETWORK")
context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT"))
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
- context["bond_size"] = float(config("BOND_SIZE"))
+ context["bond_size"] = float(config("DEFAULT_BOND_SIZE"))
if request.user.is_authenticated:
context["nickname"] = request.user.username
- context["referral_link"] = str(config('HOST_NAME'))+'/ref/'+str(request.user.profile.referral_code)
+ context["referral_code"] = str(request.user.profile.referral_code)
context["earned_rewards"] = request.user.profile.earned_rewards
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user)
@@ -748,4 +805,28 @@ class PriceView(CreateAPIView):
except:
payload[code] = None
+ return Response(payload, status.HTTP_200_OK)
+
+class LimitView(ListAPIView):
+
+ def get(self, request):
+
+ # Trade limits as BTC
+ min_trade = float(config('MIN_TRADE')) / 100000000
+ max_trade = float(config('MAX_TRADE')) / 100000000
+ max_bondless_trade = float(config('MAX_TRADE_BONDLESS_TAKER')) / 100000000
+
+ payload = {}
+ queryset = Currency.objects.all().order_by('currency')
+
+ for currency in queryset:
+ code = Currency.currency_dict[str(currency.currency)]
+ exchange_rate = float(currency.exchange_rate)
+ payload[currency.currency] = {
+ 'code': code,
+ 'min_amount': min_trade * exchange_rate,
+ 'max_amount': max_trade * exchange_rate,
+ 'max_bondless_amount': max_bondless_trade * exchange_rate,
+ }
+
return Response(payload, status.HTTP_200_OK)
\ No newline at end of file
diff --git a/chat/admin.py b/chat/admin.py
index ef16dd44..cd3ad86b 100644
--- a/chat/admin.py
+++ b/chat/admin.py
@@ -18,4 +18,4 @@ class ChatRoomAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"room_group_name",
)
change_links = ["order","maker","taker"]
- search_fields = ["id","maker__chat_maker"]
\ No newline at end of file
+ search_fields = ["id"]
\ No newline at end of file
diff --git a/control/__init__.py b/control/__init__.py
new file mode 100755
index 00000000..e69de29b
diff --git a/control/admin.py b/control/admin.py
new file mode 100755
index 00000000..c7416eab
--- /dev/null
+++ b/control/admin.py
@@ -0,0 +1,54 @@
+from django.contrib import admin
+from control.models import AccountingDay, AccountingMonth, Dispute
+from import_export.admin import ImportExportModelAdmin
+
+# Register your models here.
+
+@admin.register(AccountingDay)
+class AccountingDayAdmin(ImportExportModelAdmin):
+
+ list_display = (
+ "day",
+ "contracted",
+ "num_contracts",
+ "net_settled",
+ "net_paid",
+ "net_balance",
+ "inflow",
+ "outflow",
+ "routing_fees",
+ "cashflow",
+ "outstanding_earned_rewards",
+ "outstanding_pending_disputes",
+ "lifetime_rewards_claimed",
+ "outstanding_earned_rewards",
+ "earned_rewards",
+ "disputes",
+ "rewards_claimed",
+ )
+ change_links = ["day"]
+ search_fields = ["day"]
+
+@admin.register(AccountingMonth)
+class AccountingMonthAdmin(ImportExportModelAdmin):
+
+ list_display = (
+ "month",
+ "contracted",
+ "num_contracts",
+ "net_settled",
+ "net_paid",
+ "net_balance",
+ "inflow",
+ "outflow",
+ "routing_fees",
+ "cashflow",
+ "outstanding_earned_rewards",
+ "outstanding_pending_disputes",
+ "lifetime_rewards_claimed",
+ "outstanding_earned_rewards",
+ "pending_disputes",
+ "rewards_claimed",
+ )
+ change_links = ["month"]
+ search_fields = ["month"]
\ No newline at end of file
diff --git a/control/apps.py b/control/apps.py
new file mode 100755
index 00000000..14765b63
--- /dev/null
+++ b/control/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ControlConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'control'
diff --git a/control/models.py b/control/models.py
new file mode 100755
index 00000000..82dd2678
--- /dev/null
+++ b/control/models.py
@@ -0,0 +1,77 @@
+from django.db import models
+from django.utils import timezone
+
+class AccountingDay(models.Model):
+ day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
+
+ # Every field is denominated in Sats with (3 decimals for millisats)
+ # Total volume contracted
+ contracted = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Number of contracts
+ num_contracts = models.BigIntegerField(default=0, null=False, blank=False)
+ # Net volume of trading invoices settled (excludes disputes)
+ net_settled = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Net volume of trading invoices paid (excludes rewards and disputes)
+ net_paid = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Sum of net settled and net paid
+ net_balance = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total volume of invoices settled
+ inflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total volume of invoices paid
+ outflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total cost in routing fees
+ routing_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total inflows minus outflows and routing fees
+ cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance on earned rewards (referral rewards, slashed bonds and solved disputes)
+ outstanding_earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance on pending disputes (not resolved yet)
+ outstanding_pending_disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Rewards claimed lifetime
+ lifetime_rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance change from last day on earned rewards (referral rewards, slashed bonds and solved disputes)
+ earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance change on pending disputes (not resolved yet)
+ disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Rewards claimed on day
+ rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+
+
+
+class AccountingMonth(models.Model):
+ month = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
+
+ # Every field is denominated in Sats with (3 decimals for millisats)
+ # Total volume contracted
+ contracted = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Number of contracts
+ num_contracts = models.BigIntegerField(default=0, null=False, blank=False)
+ # Net volume of trading invoices settled (excludes disputes)
+ net_settled = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Net volume of trading invoices paid (excludes rewards and disputes)
+ net_paid = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Sum of net settled and net paid
+ net_balance = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total volume of invoices settled
+ inflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total volume of invoices paid
+ outflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total cost in routing fees
+ routing_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Total inflows minus outflows and routing fees
+ cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance on earned rewards (referral rewards, slashed bonds and solved disputes)
+ outstanding_earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance on pending disputes (not resolved yet)
+ outstanding_pending_disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Rewards claimed lifetime
+ lifetime_rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance change from last day on earned rewards (referral rewards, slashed bonds and solved disputes)
+ earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Balance change on pending disputes (not resolved yet)
+ pending_disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+ # Rewards claimed on day
+ rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
+
+class Dispute(models.Model):
+ pass
\ No newline at end of file
diff --git a/control/tasks.py b/control/tasks.py
new file mode 100644
index 00000000..616ae1ff
--- /dev/null
+++ b/control/tasks.py
@@ -0,0 +1,112 @@
+from celery import shared_task
+from api.models import Order, LNPayment, Profile, MarketTick
+from control.models import AccountingDay, AccountingMonth
+from django.utils import timezone
+from datetime import timedelta
+from django.db.models import Sum
+from decouple import config
+
+@shared_task(name="do_accounting")
+def do_accounting():
+ '''
+ Does all accounting from the beginning of time
+ '''
+
+ all_payments = LNPayment.objects.all()
+ all_ticks = MarketTick.objects.all()
+ today = timezone.now().date()
+
+ try:
+ last_accounted_day = AccountingDay.objects.latest('day').day.date()
+ accounted_yesterday = AccountingDay.objects.latest('day')
+ except:
+ last_accounted_day = None
+ accounted_yesterday = None
+
+ if last_accounted_day == today:
+ return {'message':'no days to account for'}
+ elif last_accounted_day != None:
+ initial_day = last_accounted_day + timedelta(days=1)
+ elif last_accounted_day == None:
+ initial_day = all_payments.earliest('created_at').created_at.date()
+
+
+ day = initial_day
+ result = {}
+ while day <= today:
+ day_payments = all_payments.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1))
+ day_ticks = all_ticks.filter(timestamp__gte=day,timestamp__lte=day+timedelta(days=1))
+
+ # Coarse accounting based on LNpayment objects
+ contracted = day_ticks.aggregate(Sum('volume'))['volume__sum']
+ num_contracts = day_ticks.count()
+ inflow = day_payments.filter(type=LNPayment.Types.HOLD,status=LNPayment.Status.SETLED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
+ outflow = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
+ routing_fees = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('fee'))['fee__sum']
+ rewards_claimed = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.WITHREWA,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
+
+ contracted = 0 if contracted == None else contracted
+ inflow = 0 if inflow == None else inflow
+ outflow = 0 if outflow == None else outflow
+ routing_fees = 0 if routing_fees == None else routing_fees
+ rewards_claimed = 0 if rewards_claimed == None else rewards_claimed
+
+ accounted_day = AccountingDay.objects.create(
+ day = day,
+ contracted = contracted,
+ num_contracts = num_contracts,
+ inflow = inflow,
+ outflow = outflow,
+ routing_fees = routing_fees,
+ cashflow = inflow - outflow - routing_fees,
+ rewards_claimed = rewards_claimed,
+ )
+
+ # Fine Net Daily accounting based on orders
+ # Only account for orders where everything worked out right
+ payouts = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.PAYBUYER, status=LNPayment.Status.SUCCED)
+ escrows_settled = 0
+ payouts_paid = 0
+ routing_cost = 0
+ for payout in payouts:
+ escrows_settled += payout.order_paid.trade_escrow.num_satoshis
+ payouts_paid += payout.num_satoshis
+ routing_cost += payout.fee
+
+ # account for those orders where bonds were lost
+ # + Settled bonds / bond_split
+ bonds_settled = day_payments.filter(type=LNPayment.Types.HOLD,concept__in=[LNPayment.Concepts.TAKEBOND,LNPayment.Concepts.MAKEBOND], status=LNPayment.Status.SETLED)
+
+ if len(bonds_settled) > 0:
+ collected_slashed_bonds = (bonds_settled.aggregate(Sum('num_satoshis'))['num_satoshis__sum'])* float(config('SLASHED_BOND_REWARD_SPLIT'))
+ else:
+ collected_slashed_bonds = 0
+
+ accounted_day.net_settled = escrows_settled + collected_slashed_bonds
+ accounted_day.net_paid = payouts_paid + routing_cost
+ accounted_day.net_balance = float(accounted_day.net_settled) - float(accounted_day.net_paid)
+
+ # Differential accounting based on change of outstanding states and disputes unreslved
+ if day == today:
+ pending_disputes = Order.objects.filter(status__in=[Order.Status.DIS,Order.Status.WFR])
+ if len(pending_disputes) > 0:
+ outstanding_pending_disputes = 0
+ for order in pending_disputes:
+ outstanding_pending_disputes += order.payout.num_satoshis
+ else:
+ outstanding_pending_disputes = 0
+
+ accounted_day.outstanding_earned_rewards = Profile.objects.all().aggregate(Sum('earned_rewards'))['earned_rewards__sum']
+ accounted_day.outstanding_pending_disputes = outstanding_pending_disputes
+ accounted_day.lifetime_rewards_claimed = Profile.objects.all().aggregate(Sum('claimed_rewards'))['claimed_rewards__sum']
+ if accounted_yesterday != None:
+ accounted_day.earned_rewards = accounted_day.outstanding_earned_rewards - accounted_yesterday.outstanding_earned_rewards
+ accounted_day.disputes = outstanding_pending_disputes - accounted_yesterday.outstanding_earned_rewards
+
+ # Close the loop
+ accounted_day.save()
+ accounted_yesterday = accounted_day
+ result[str(day)]={'contracted':contracted,'inflow':inflow,'outflow':outflow}
+ day = day + timedelta(days=1)
+
+ return result
\ No newline at end of file
diff --git a/control/tests.py b/control/tests.py
new file mode 100755
index 00000000..7ce503c2
--- /dev/null
+++ b/control/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/control/views.py b/control/views.py
new file mode 100755
index 00000000..91ea44a2
--- /dev/null
+++ b/control/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/docker-compose.yml b/docker-compose.yml
index c60bfd32..ba646cb1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -27,6 +27,7 @@ services:
build: ./frontend
container_name: npm-dev
restart: always
+ command: npm run build
volumes:
- ./frontend:/usr/src/frontend
diff --git a/docker/lnd/Dockerfile b/docker/lnd/Dockerfile
index 68a784ee..6c681b0c 100644
--- a/docker/lnd/Dockerfile
+++ b/docker/lnd/Dockerfile
@@ -15,4 +15,5 @@ RUN apk --no-cache --no-progress add shadow=~4 sudo=~1 gettext=~0.21 && \
USER root
COPY entrypoint.sh /root/entrypoint.sh
COPY lnd.conf /tmp/lnd.conf
+
ENTRYPOINT [ "/root/entrypoint.sh" ]
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 5e3b0159..a94b244a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1286,6 +1286,43 @@
"minimist": "^1.2.0"
}
},
+ "@date-io/core": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz",
+ "integrity": "sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg=="
+ },
+ "@date-io/date-fns": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.13.1.tgz",
+ "integrity": "sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==",
+ "requires": {
+ "@date-io/core": "^2.13.1"
+ }
+ },
+ "@date-io/dayjs": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.13.1.tgz",
+ "integrity": "sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==",
+ "requires": {
+ "@date-io/core": "^2.13.1"
+ }
+ },
+ "@date-io/luxon": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz",
+ "integrity": "sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==",
+ "requires": {
+ "@date-io/core": "^2.13.1"
+ }
+ },
+ "@date-io/moment": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.13.1.tgz",
+ "integrity": "sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==",
+ "requires": {
+ "@date-io/core": "^2.13.1"
+ }
+ },
"@discoveryjs/json-ext": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz",
@@ -1589,6 +1626,120 @@
"@babel/runtime": "^7.16.3"
}
},
+ "@mui/lab": {
+ "version": "5.0.0-alpha.73",
+ "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.73.tgz",
+ "integrity": "sha512-10Uj0Atc7gBTXKX4VV38P6RdqTQrJZxcl3HeEcytIO1S3NAGfc7gZ3Hdpnhtj5U8kcRJZZPH9LtrBbMZzxU/1A==",
+ "requires": {
+ "@babel/runtime": "^7.17.2",
+ "@date-io/date-fns": "^2.13.1",
+ "@date-io/dayjs": "^2.13.1",
+ "@date-io/luxon": "^2.13.1",
+ "@date-io/moment": "^2.13.1",
+ "@mui/base": "5.0.0-alpha.72",
+ "@mui/system": "^5.5.1",
+ "@mui/utils": "^5.4.4",
+ "clsx": "^1.1.1",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2",
+ "react-transition-group": "^4.4.2",
+ "rifm": "^0.12.1"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.17.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.7.tgz",
+ "integrity": "sha512-L6rvG9GDxaLgFjg41K+5Yv9OMrU98sWe+Ykmc6FDJW/+vYZMhdOMKkISgzptMaERHvS2Y2lw9MDRm2gHhlQQoA==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@emotion/is-prop-valid": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz",
+ "integrity": "sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==",
+ "requires": {
+ "@emotion/memoize": "^0.7.4"
+ }
+ },
+ "@mui/base": {
+ "version": "5.0.0-alpha.72",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.72.tgz",
+ "integrity": "sha512-WCAooa9eqbsC68LhyKtDBRumH4hV1eRZ0A3SDKFHSwYG9fCOdsFv/H1dIYRJM0rwD45bMnuDiG3Qmx7YsTiptw==",
+ "requires": {
+ "@babel/runtime": "^7.17.2",
+ "@emotion/is-prop-valid": "^1.1.2",
+ "@mui/utils": "^5.4.4",
+ "@popperjs/core": "^2.11.3",
+ "clsx": "^1.1.1",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ }
+ },
+ "@mui/private-theming": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.4.4.tgz",
+ "integrity": "sha512-V/gxttr6736yJoU9q+4xxXsa0K/w9Hn9pg99zsOHt7i/O904w2CX5NHh5WqDXtoUzVcayLF0RB17yr6l79CE+A==",
+ "requires": {
+ "@babel/runtime": "^7.17.2",
+ "@mui/utils": "^5.4.4",
+ "prop-types": "^15.7.2"
+ }
+ },
+ "@mui/styled-engine": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.4.4.tgz",
+ "integrity": "sha512-AKx3rSgB6dmt5f7iP4K18mLFlE5/9EfJe/5EH9Pyqez8J/CPkTgYhJ/Va6qtlrcunzpui+uG/vfuf04yAZekSg==",
+ "requires": {
+ "@babel/runtime": "^7.17.2",
+ "@emotion/cache": "^11.7.1",
+ "prop-types": "^15.7.2"
+ }
+ },
+ "@mui/system": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.5.1.tgz",
+ "integrity": "sha512-2hynI4hN8304hOCT8sc4knJviwUUYJ7XK3mXwQ0nagVGOPnWSOad/nYADm7K0vdlCeUXLIbDbe7oNN3Kaiu2kA==",
+ "requires": {
+ "@babel/runtime": "^7.17.2",
+ "@mui/private-theming": "^5.4.4",
+ "@mui/styled-engine": "^5.4.4",
+ "@mui/types": "^7.1.3",
+ "@mui/utils": "^5.4.4",
+ "clsx": "^1.1.1",
+ "csstype": "^3.0.11",
+ "prop-types": "^15.7.2"
+ }
+ },
+ "@mui/types": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.3.tgz",
+ "integrity": "sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA=="
+ },
+ "@mui/utils": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.4.4.tgz",
+ "integrity": "sha512-hfYIXEuhc2mXMGN5nUPis8beH6uE/zl3uMWJcyHX0/LN/+QxO9zhYuV6l8AsAaphHFyS/fBv0SW3Nid7jw5hKQ==",
+ "requires": {
+ "@babel/runtime": "^7.17.2",
+ "@types/prop-types": "^15.7.4",
+ "@types/react-is": "^16.7.1 || ^17.0.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ }
+ },
+ "@popperjs/core": {
+ "version": "2.11.4",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
+ "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg=="
+ },
+ "csstype": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
+ "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
+ }
+ }
+ },
"@mui/material": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz",
@@ -3306,6 +3457,11 @@
"type": "^1.0.1"
}
},
+ "date-fns": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
+ "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
+ },
"dayjs": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
@@ -6749,6 +6905,11 @@
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
},
+ "rifm": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz",
+ "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg=="
+ },
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index fe1c1a06..d8703c06 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -27,9 +27,11 @@
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@mui/icons-material": "^5.2.5",
+ "@mui/lab": "^5.0.0-alpha.73",
"@mui/material": "^5.2.7",
"@mui/system": "^5.2.6",
"@mui/x-data-grid": "^5.2.2",
+ "date-fns": "^2.28.0",
"material-ui-image": "^3.3.2",
"react-countdown": "^2.3.2",
"react-native": "^0.66.4",
diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js
index 7532249b..4d89def1 100644
--- a/frontend/src/components/App.js
+++ b/frontend/src/components/App.js
@@ -1,8 +1,12 @@
import React, { Component } from "react";
import { render } from "react-dom";
-
import HomePage from "./HomePage";
-import BottomBar from "./BottomBar";
+import { CssBaseline, IconButton} from "@mui/material";
+import { ThemeProvider, createTheme } from '@mui/material/styles';
+import UnsafeAlert from "./UnsafeAlert";
+
+import DarkModeIcon from '@mui/icons-material/DarkMode';
+import LightModeIcon from '@mui/icons-material/LightMode';
export default class App extends Component {
constructor(props) {
@@ -10,6 +14,7 @@ export default class App extends Component {
this.state = {
nickname: null,
token: null,
+ dark: false,
}
}
@@ -17,11 +22,28 @@ export default class App extends Component {
this.setState(newState)
}
+ lightTheme = createTheme({
+ });
+
+ darkTheme = createTheme({
+ palette: {
+ mode: 'dark',
+ background: {
+ default: "#070707"
+ },
+ },
+ });
+
render() {
return (
- <>
- It is a BTC/FIAT peer-to-peer exchange over lightning. RoboSats is an open source project (GitHub).
+ RoboSats is an open source project (GitHub).
It is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies
- matchmaking and minimizes the need for entrust. RoboSats focuses in privacy and speed.
It simplifies
matchmaking and minimizes the need of trust. RoboSats focuses in privacy and speed.
RoboSats is an open source project (GitHub). +
RoboSats is an open source project (GitHub).
@@ -59,10 +59,11 @@ export default class InfoDialog extends Component { received the fiat, then the satoshis are released to Bob. Enjoy your satoshis, Bob! -At no point, AnonymousAlice01 and BafflingBob02 have to trust the +
At no point, AnonymousAlice01 and BafflingBob02 have to entrust the bitcoin funds to each other. In case they have a conflict, RoboSats staff will help resolving the dispute. You can find a step-by-step - description of the trade pipeline in How it works
+ description of the trade pipeline in How it works + You can also check the full guide in How to useYou can build more trust on RoboSats by - inspecting the source code.
+You can build more trust on RoboSats by + inspecting the source code.
This lightning application is provided as is. It is in active development: trade with the utmost caution. There is no private - support. Support is only offered via public channels - (Telegram). RoboSats will never contact you. + support. Support is only offered via public channels + (Telegram). RoboSats will never contact you. RoboSats will definitely never ask for your robot token.
Be patient while robots check the book. It might take some time. This box will ring 🔊 once a robot takes your order.
-Please note that if your premium is excessive, or your currency or payment +
Please note that if your premium is excessive or your currency or payment methods are not popular, your order might expire untaken. Your bond will return to you (no action needed).
Thank you for using Robosats!
Let us know how the platform could improve - (Telegram / Github)
+ (Telegram / Github)