Merge pull request #76 from Reckless-Satoshi/advanced-maker-options-1

Advanced maker options v1
This commit is contained in:
Reckless_Satoshi 2022-03-26 16:18:44 +00:00 committed by GitHub
commit 36134cf1a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 2466 additions and 2712 deletions

View File

@ -55,8 +55,10 @@ FEE = 0.002
# Shall incentivize order making # Shall incentivize order making
MAKER_FEE_SPLIT=0.125 MAKER_FEE_SPLIT=0.125
# Default bond size as fraction # Bond size as percentage (%)
BOND_SIZE = 0.01 DEFAULT_BOND_SIZE = 1
MIN_BOND_SIZE = 1
MAX_BOND_SIZE = 15
# Time out penalty for canceling takers in SECONDS # Time out penalty for canceling takers in SECONDS
PENALTY_TIMEOUT = 60 PENALTY_TIMEOUT = 60
@ -69,6 +71,7 @@ MAX_PUBLIC_ORDERS = 100
# Trade limits in satoshis # Trade limits in satoshis
MIN_TRADE = 20000 MIN_TRADE = 20000
MAX_TRADE = 800000 MAX_TRADE = 800000
MAX_TRADE_BONDLESS_TAKER = 50000
# Expiration (CLTV_expiry) time for HODL invoices in HOURS // 7 min/block assumed # Expiration (CLTV_expiry) time for HODL invoices in HOURS // 7 min/block assumed
BOND_EXPIRY = 54 BOND_EXPIRY = 54
@ -79,7 +82,10 @@ EXP_MAKER_BOND_INVOICE = 300
EXP_TAKER_BOND_INVOICE = 200 EXP_TAKER_BOND_INVOICE = 200
# Time a order is public in the book HOURS # 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 # Time to provide a valid invoice and the trade escrow MINUTES
INVOICE_AND_ESCROW_DURATION = 30 INVOICE_AND_ESCROW_DURATION = 30
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS # Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS

3
.gitignore vendored
View File

@ -651,4 +651,5 @@ api/lightning/invoices*
api/lightning/router* api/lightning/router*
api/lightning/googleapis* api/lightning/googleapis*
frontend/static/admin* frontend/static/admin*
frontend/static/rest_framework* frontend/static/rest_framework*
frontend/static/import_export*

View File

@ -13,21 +13,25 @@ class ProfileInline(admin.StackedInline):
can_delete = False can_delete = False
fields = ("avatar_tag", ) fields = ("avatar_tag", )
readonly_fields = ["avatar_tag"] readonly_fields = ["avatar_tag"]
show_change_link = True
# extended users with avatars # extended users with avatars
@admin.register(User) @admin.register(User)
class EUserAdmin(UserAdmin): class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
inlines = [ProfileInline] inlines = [ProfileInline]
list_display = ( list_display = (
"avatar_tag", "avatar_tag",
"id", "id",
"profile_link",
"username", "username",
"last_login", "last_login",
"date_joined", "date_joined",
"is_staff", "is_staff",
) )
list_display_links = ("id", "username") list_display_links = ("id", "username")
change_links = (
"profile",
)
ordering = ("-id", ) ordering = ("-id", )
def avatar_tag(self, obj): def avatar_tag(self, obj):
@ -42,7 +46,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"maker_link", "maker_link",
"taker_link", "taker_link",
"status", "status",
"amount", "amt",
"currency_link", "currency_link",
"t0_satoshis", "t0_satoshis",
"is_disputed", "is_disputed",
@ -65,7 +69,13 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"trade_escrow", "trade_escrow",
) )
list_filter = ("is_disputed", "is_fiat_sent", "type", "currency", "status") 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) @admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@ -74,6 +84,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"concept", "concept",
"status", "status",
"num_satoshis", "num_satoshis",
"fee",
"type", "type",
"expires_at", "expires_at",
"expiry_height", "expiry_height",
@ -95,7 +106,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
) )
list_filter = ("type", "concept", "status") list_filter = ("type", "concept", "status")
ordering = ("-expires_at", ) ordering = ("-expires_at", )
search_fields = ["payment_hash","num_satoshis"] search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"]
@admin.register(Profile) @admin.register(Profile)
@ -116,9 +127,11 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"num_disputes", "num_disputes",
"lost_disputes", "lost_disputes",
) )
list_editable = ["pending_rewards", "earned_rewards"]
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"]
search_fields = ["user__username","id"]
@admin.register(Currency) @admin.register(Currency)
@ -128,7 +141,6 @@ class CurrencieAdmin(admin.ModelAdmin):
readonly_fields = ("currency", "exchange_rate", "timestamp") readonly_fields = ("currency", "exchange_rate", "timestamp")
ordering = ("id", ) ordering = ("id", )
@admin.register(MarketTick) @admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin): class MarketTickAdmin(admin.ModelAdmin):
list_display = ("timestamp", "price", "volume", "premium", "currency", list_display = ("timestamp", "price", "volume", "premium", "currency",

View File

@ -261,6 +261,7 @@ class LNNode:
if response.status == 2: # STATUS 'SUCCEEDED' if response.status == 2: # STATUS 'SUCCEEDED'
lnpayment.status = LNPayment.Status.SUCCED lnpayment.status = LNPayment.Status.SUCCED
lnpayment.fee = float(response.fee_msat)/1000
lnpayment.save() lnpayment.save()
return True, None return True, None

View File

@ -14,7 +14,6 @@ import time
FEE = float(config("FEE")) FEE = float(config("FEE"))
MAKER_FEE_SPLIT = float(config("MAKER_FEE_SPLIT")) MAKER_FEE_SPLIT = float(config("MAKER_FEE_SPLIT"))
BOND_SIZE = float(config("BOND_SIZE"))
ESCROW_USERNAME = config("ESCROW_USERNAME") ESCROW_USERNAME = config("ESCROW_USERNAME")
PENALTY_TIMEOUT = int(config("PENALTY_TIMEOUT")) 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")) BOND_EXPIRY = int(config("BOND_EXPIRY"))
ESCROW_EXPIRY = int(config("ESCROW_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")) INVOICE_AND_ESCROW_DURATION = int(config("INVOICE_AND_ESCROW_DURATION"))
FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION")) FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION"))
@ -89,24 +87,65 @@ class Logics:
return True, None, None return True, None, None
def validate_order_size(order): @classmethod
"""Validates if order is withing limits in satoshis at t0""" def validate_order_size(cls, order):
if order.t0_satoshis > MAX_TRADE: """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, { return False, {
"bad_request": "bad_request":
"Your order is too big. It is worth " + "The amount specified is outside the range specified by the maker"
"{:,}".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"
} }
return True, None return True, None
def user_activity_status(last_seen): def user_activity_status(last_seen):
@ -118,7 +157,7 @@ class Logics:
return "Inactive" return "Inactive"
@classmethod @classmethod
def take(cls, order, user): def take(cls, order, user, amount=None):
is_penalized, time_out = cls.is_penalized(user) is_penalized, time_out = cls.is_penalized(user)
if is_penalized: if is_penalized:
return False, { return False, {
@ -126,10 +165,12 @@ class Logics:
f"You need to wait {time_out} seconds to take an order", f"You need to wait {time_out} seconds to take an order",
} }
else: else:
if order.has_range:
order.amount= amount
order.taker = user order.taker = user
order.status = Order.Status.TAK order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.TAK]) seconds=order.t_to_expire(Order.Status.TAK))
order.save() order.save()
# send_message.delay(order.id,'order_taken') # Too spammy # send_message.delay(order.id,'order_taken') # Too spammy
return True, None return True, None
@ -146,15 +187,19 @@ class Logics:
return (is_maker and order.type == Order.Types.SELL) or ( return (is_maker and order.type == Order.Types.SELL) or (
is_taker and order.type == Order.Types.BUY) 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""" """checks trade amount in sats"""
if order.is_explicit: if order.is_explicit:
satoshis_now = order.satoshis satoshis_now = order.satoshis
else: else:
exchange_rate = float(order.currency.exchange_rate) amount = order.amount if order.amount != None else order.max_amount
premium_rate = exchange_rate * (1 + float(order.premium) / 100) satoshis_now = cls.calc_sats(amount, order.currency.exchange_rate, order.premium)
satoshis_now = (float(order.amount) /
premium_rate) * 100 * 1000 * 1000
return int(satoshis_now) return int(satoshis_now)
@ -165,8 +210,8 @@ class Logics:
premium = order.premium premium = order.premium
price = exchange_rate * (1 + float(premium) / 100) price = exchange_rate * (1 + float(premium) / 100)
else: else:
order_rate = float( amount = order.amount if not order.has_range else order.max_amount
order.amount) / (float(order.satoshis) / 100000000) order_rate = float(amount) / (float(order.satoshis) / 100000000)
premium = order_rate / exchange_rate - 1 premium = order_rate / exchange_rate - 1
premium = int(premium * 10000) / 100 # 2 decimals left premium = int(premium * 10000) / 100 # 2 decimals left
price = order_rate price = order_rate
@ -336,7 +381,7 @@ class Logics:
order.is_disputed = True order.is_disputed = True
order.status = Order.Status.DIS order.status = Order.Status.DIS
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.DIS]) seconds=order.t_to_expire(Order.Status.DIS))
order.save() order.save()
# User could be None if a dispute is open automatically due to weird expiration. # 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,""]: if order.maker_statement not in [None,""] and order.taker_statement not in [None,""]:
order.status = Order.Status.WFR order.status = Order.Status.WFR
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.WFR]) seconds=order.t_to_expire(Order.Status.WFR))
order.save() order.save()
return True, None return True, None
@ -442,6 +487,12 @@ class Logics:
"bad_request": "bad_request":
"You cannot submit a invoice while bonds are not locked." "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"] num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
payout = LNNode.validate_ln_invoice(invoice, num_satoshis) payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
@ -472,7 +523,7 @@ class Logics:
if order.status == Order.Status.WFI: if order.status == Order.Status.WFI:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta( 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 the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2: if order.status == Order.Status.WF2:
@ -483,7 +534,7 @@ class Logics:
elif order.trade_escrow.status == LNPayment.Status.LOCKED: elif order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.CHA]) seconds=order.t_to_expire(Order.Status.CHA))
else: else:
order.status = Order.Status.WFE order.status = Order.Status.WFE
@ -661,7 +712,9 @@ class Logics:
def publish_order(order): def publish_order(order):
order.status = Order.Status.PUB order.status = Order.Status.PUB
order.expires_at = order.created_at + timedelta( 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() order.save()
# send_message.delay(order.id,'order_published') # too spammy # send_message.delay(order.id,'order_published') # too spammy
return return
@ -699,7 +752,7 @@ class Logics:
# If there was no maker_bond object yet, generates one # If there was no maker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order) 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." 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( hold_payment = LNNode.gen_hold_invoice(
bond_satoshis, bond_satoshis,
description, 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, cltv_expiry_secs=BOND_EXPIRY * 3600,
) )
except Exception as e: except Exception as e:
@ -759,7 +812,7 @@ class Logics:
# With the bond confirmation the order is extended 'public_order_duration' hours # With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta( 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.status = Order.Status.WF2
order.save() order.save()
@ -809,7 +862,7 @@ class Logics:
# If there was no taker_bond object yet, generates one # If there was no taker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order) 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" pos_text = "Buying" if cls.is_buyer(order, user) else "Selling"
description = ( description = (
f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}" 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( hold_payment = LNNode.gen_hold_invoice(
bond_satoshis, bond_satoshis,
description, 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, cltv_expiry_secs=BOND_EXPIRY * 3600,
) )
@ -850,7 +903,7 @@ class Logics:
) )
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.TAK]) seconds=order.t_to_expire(Order.Status.TAK))
order.save() order.save()
return True, { return True, {
"bond_invoice": hold_payment["invoice"], "bond_invoice": hold_payment["invoice"],
@ -866,7 +919,7 @@ class Logics:
elif order.status == Order.Status.WFE: elif order.status == Order.Status.WFE:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.CHA]) seconds=order.t_to_expire(Order.Status.CHA))
order.save() order.save()
@classmethod @classmethod
@ -909,7 +962,7 @@ class Logics:
hold_payment = LNNode.gen_hold_invoice( hold_payment = LNNode.gen_hold_invoice(
escrow_satoshis, escrow_satoshis,
description, 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, cltv_expiry_secs=ESCROW_EXPIRY * 3600,
) )

View File

@ -45,11 +45,19 @@ class Command(BaseCommand):
except: except:
print(f'No profile with token {token}') print(f'No profile with token {token}')
continue continue
profile.telegram_chat_id = result['message']['from']['id']
profile.telegram_lang_code = result['message']['from']['language_code'] attempts = 5
self.telegram.welcome(profile.user) while attempts >= 0:
profile.telegram_enabled = True try:
profile.save() 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'] offset = response['result'][-1]['update_id']

View File

@ -5,6 +5,7 @@ from django.core.validators import (
MinValueValidator, MinValueValidator,
validate_comma_separated_integer_list, validate_comma_separated_integer_list,
) )
from django.utils import timezone
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.template.defaultfilters import truncatechars from django.template.defaultfilters import truncatechars
from django.dispatch import receiver from django.dispatch import receiver
@ -19,7 +20,7 @@ import json
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")) DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
class Currency(models.Model): class Currency(models.Model):
@ -38,7 +39,7 @@ class Currency(models.Model):
null=True, null=True,
validators=[MinValueValidator(0)], validators=[MinValueValidator(0)],
) )
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(default=timezone.now)
def __str__(self): def __str__(self):
# returns currency label ( 3 letters code) # returns currency label ( 3 letters code)
@ -105,9 +106,11 @@ class LNPayment(models.Model):
default=None, default=None,
blank=True) blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[ num_satoshis = models.PositiveBigIntegerField(validators=[
MinValueValidator(MIN_TRADE * BOND_SIZE), MinValueValidator(100),
MaxValueValidator(MAX_TRADE * (1 + BOND_SIZE + FEE)), 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() created_at = models.DateTimeField()
expires_at = models.DateTimeField() expires_at = models.DateTimeField()
cltv_expiry = models.PositiveSmallIntegerField(null=True, cltv_expiry = models.PositiveSmallIntegerField(null=True,
@ -181,7 +184,7 @@ class Order(models.Model):
status = models.PositiveSmallIntegerField(choices=Status.choices, status = models.PositiveSmallIntegerField(choices=Status.choices,
null=False, null=False,
default=Status.WFB) default=Status.WFB)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(default=timezone.now)
expires_at = models.DateTimeField() expires_at = models.DateTimeField()
# order details # order details
@ -189,14 +192,15 @@ class Order(models.Model):
currency = models.ForeignKey(Currency, currency = models.ForeignKey(Currency,
null=True, null=True,
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
amount = models.DecimalField(max_digits=16, amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
decimal_places=8, has_range = models.BooleanField(default=False, null=False, blank=False)
validators=[MinValueValidator(0.00000001)]) 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, payment_method = models.CharField(max_length=35,
null=False, null=False,
default="not specified", default="not specified",
blank=True) 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. # 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)
# marked to market # marked to market
@ -218,6 +222,29 @@ class Order(models.Model):
], ],
blank=True, 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) # how many sats at creation and at last check (relevant for marked to market)
t0_satoshis = models.PositiveBigIntegerField( t0_satoshis = models.PositiveBigIntegerField(
null=True, null=True,
@ -311,30 +338,38 @@ class Order(models.Model):
maker_platform_rated = models.BooleanField(default=False, null=False) maker_platform_rated = models.BooleanField(default=False, null=False)
taker_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): 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) @receiver(pre_delete, sender=Order)
@ -393,7 +428,7 @@ class Profile(models.Model):
null=False null=False
) )
telegram_lang_code = models.CharField( telegram_lang_code = models.CharField(
max_length=4, max_length=10,
null=True, null=True,
blank=True blank=True
) )
@ -529,7 +564,7 @@ class MarketTick(models.Model):
currency = models.ForeignKey(Currency, currency = models.ForeignKey(Currency,
null=True, null=True,
on_delete=models.SET_NULL) 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 # Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
fee = models.DecimalField( fee = models.DecimalField(

View File

@ -14,10 +14,14 @@ class ListOrderSerializer(serializers.ModelSerializer):
"type", "type",
"currency", "currency",
"amount", "amount",
"has_range",
"min_amount",
"max_amount",
"payment_method", "payment_method",
"is_explicit", "is_explicit",
"premium", "premium",
"satoshis", "satoshis",
"bondless_taker",
"maker", "maker",
"taker", "taker",
) )
@ -31,13 +35,18 @@ class MakeOrderSerializer(serializers.ModelSerializer):
"type", "type",
"currency", "currency",
"amount", "amount",
"has_range",
"min_amount",
"max_amount",
"payment_method", "payment_method",
"is_explicit", "is_explicit",
"premium", "premium",
"satoshis", "satoshis",
"public_duration",
"bond_size",
"bondless_taker",
) )
class UpdateOrderSerializer(serializers.Serializer): class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, invoice = serializers.CharField(max_length=2000,
allow_null=True, allow_null=True,
@ -66,6 +75,7 @@ class UpdateOrderSerializer(serializers.Serializer):
allow_blank=True, allow_blank=True,
default=None, default=None,
) )
amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None)
class ClaimRewardSerializer(serializers.Serializer): class ClaimRewardSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, invoice = serializers.CharField(max_length=2000,

View File

@ -111,7 +111,7 @@ def follow_send_payment(lnpayment):
lnpayment.save() lnpayment.save()
order.status = Order.Status.FAI order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.FAI]) seconds=order.t_to_expire(Order.Status.FAI))
order.save() order.save()
context = { context = {
"routing_failed": "routing_failed":
@ -123,10 +123,11 @@ def follow_send_payment(lnpayment):
if response.status == 2: # Status 2 'SUCCEEDED' if response.status == 2: # Status 2 'SUCCEEDED'
print("SUCCEEDED") print("SUCCEEDED")
lnpayment.status = LNPayment.Status.SUCCED lnpayment.status = LNPayment.Status.SUCCED
lnpayment.fee = float(response.fee_msat)/1000
lnpayment.save() lnpayment.save()
order.status = Order.Status.SUC order.status = Order.Status.SUC
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.SUC]) seconds=order.t_to_expire(Order.Status.SUC))
order.save() order.save()
return True, None return True, None
@ -138,7 +139,7 @@ def follow_send_payment(lnpayment):
lnpayment.save() lnpayment.save()
order.status = Order.Status.FAI order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.FAI]) seconds=order.t_to_expire(Order.Status.FAI))
order.save() order.save()
context = {"routing_failed": "The payout invoice has expired"} context = {"routing_failed": "The payout invoice has expired"}
return False, context return False, context
@ -191,6 +192,9 @@ def send_message(order_id, message):
from api.messages import Telegram from api.messages import Telegram
telegram = Telegram() telegram = Telegram()
if message == 'welcome':
telegram.welcome(order)
if message == 'order_taken': if message == 'order_taken':
telegram.order_taken(order) telegram.order_taken(order)

View File

@ -1,5 +1,5 @@
from django.urls import path 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 = [ urlpatterns = [
path("make/", MakerView.as_view()), path("make/", MakerView.as_view()),
@ -13,5 +13,6 @@ urlpatterns = [
# path('robot/') # Profile Info # path('robot/') # Profile Info
path("info/", InfoView.as_view()), path("info/", InfoView.as_view()),
path("price/", PriceView.as_view()), path("price/", PriceView.as_view()),
path("limits/", LimitView.as_view()),
path("reward/", RewardView.as_view()), path("reward/", RewardView.as_view()),
] ]

View File

@ -101,11 +101,13 @@ def compute_premium_percentile(order):
if len(queryset) <= 1: if len(queryset) <= 1:
return 0.5 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 = [] rates = []
for similar_order in queryset: for similar_order in queryset:
similar_order_amount = similar_order.amount if not similar_order.has_range else similar_order.max_amount
rates.append( rates.append(
float(similar_order.last_satoshis) / float(similar_order.amount)) float(similar_order.last_satoshis) / float(similar_order_amount))
rates = np.array(rates) rates = np.array(rates)
return round(np.sum(rates < order_rate) / len(rates), 2) return round(np.sum(rates < order_rate) / len(rates), 2)

View File

@ -30,6 +30,8 @@ from decouple import config
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE")) EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
RETRY_TIME = int(config("RETRY_TIME")) 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 = Path(settings.AVATAR_ROOT)
avatar_path.mkdir(parents=True, exist_ok=True) avatar_path.mkdir(parents=True, exist_ok=True)
@ -64,35 +66,76 @@ class MakerView(CreateAPIView):
}, },
status.HTTP_400_BAD_REQUEST, 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") type = serializer.data.get("type")
currency = serializer.data.get("currency") currency = serializer.data.get("currency")
amount = serializer.data.get("amount") 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") payment_method = serializer.data.get("payment_method")
premium = serializer.data.get("premium") premium = serializer.data.get("premium")
satoshis = serializer.data.get("satoshis") satoshis = serializer.data.get("satoshis")
is_explicit = serializer.data.get("is_explicit") 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( # Optional params
request.user) if public_duration == None: public_duration = PUBLIC_DURATION
if not valid: if bond_size == None: bond_size = BOND_SIZE
return Response(context, status.HTTP_409_CONFLICT) 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 # Creates a new order
order = Order( order = Order(
type=type, type=type,
currency=Currency.objects.get(id=currency), currency=Currency.objects.get(id=currency),
amount=amount, amount=amount,
has_range=has_range,
min_amount=min_amount,
max_amount=max_amount,
payment_method=payment_method, payment_method=payment_method,
premium=premium, premium=premium,
satoshis=satoshis, satoshis=satoshis,
is_explicit=is_explicit, is_explicit=is_explicit,
expires_at=timezone.now() + timedelta( expires_at=timezone.now() + timedelta(
seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method seconds=EXP_MAKER_BOND_INVOICE),
maker=request.user, 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) order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
valid, context = Logics.validate_order_size(order) valid, context = Logics.validate_order_size(order)
@ -155,7 +198,7 @@ class OrderView(viewsets.ViewSet):
) )
data = ListOrderSerializer(order).data 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. # if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user) 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) order_id = request.GET.get(self.lookup_url_kwarg)
import sys
sys.stdout.write('AAAAAA')
print('BBBBB1')
serializer = UpdateOrderSerializer(data=request.data) serializer = UpdateOrderSerializer(data=request.data)
if not serializer.is_valid(): if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
@ -367,7 +414,17 @@ class OrderView(viewsets.ViewSet):
request.user) request.user)
if not valid: if not valid:
return Response(context, status=status.HTTP_409_CONFLICT) 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: if not valid:
return Response(context, status=status.HTTP_403_FORBIDDEN) return Response(context, status=status.HTTP_403_FORBIDDEN)
@ -684,11 +741,11 @@ class InfoView(ListAPIView):
context["network"] = config("NETWORK") context["network"] = config("NETWORK")
context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT")) context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT"))
context["taker_fee"] = float(config("FEE"))*(1 - 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: if request.user.is_authenticated:
context["nickname"] = request.user.username 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 context["earned_rewards"] = request.user.profile.earned_rewards
has_no_active_order, _, order = Logics.validate_already_maker_or_taker( has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user) request.user)
@ -748,4 +805,28 @@ class PriceView(CreateAPIView):
except: except:
payload[code] = None 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) return Response(payload, status.HTTP_200_OK)

View File

@ -18,4 +18,4 @@ class ChatRoomAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"room_group_name", "room_group_name",
) )
change_links = ["order","maker","taker"] change_links = ["order","maker","taker"]
search_fields = ["id","maker__chat_maker"] search_fields = ["id"]

0
control/__init__.py Executable file
View File

54
control/admin.py Executable file
View File

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

6
control/apps.py Executable file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ControlConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'control'

77
control/models.py Executable file
View File

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

112
control/tasks.py Normal file
View File

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

3
control/tests.py Executable file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
control/views.py Executable file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -27,6 +27,7 @@ services:
build: ./frontend build: ./frontend
container_name: npm-dev container_name: npm-dev
restart: always restart: always
command: npm run build
volumes: volumes:
- ./frontend:/usr/src/frontend - ./frontend:/usr/src/frontend

View File

@ -15,4 +15,5 @@ RUN apk --no-cache --no-progress add shadow=~4 sudo=~1 gettext=~0.21 && \
USER root USER root
COPY entrypoint.sh /root/entrypoint.sh COPY entrypoint.sh /root/entrypoint.sh
COPY lnd.conf /tmp/lnd.conf COPY lnd.conf /tmp/lnd.conf
ENTRYPOINT [ "/root/entrypoint.sh" ] ENTRYPOINT [ "/root/entrypoint.sh" ]

View File

@ -1286,6 +1286,43 @@
"minimist": "^1.2.0" "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": { "@discoveryjs/json-ext": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz",
@ -1589,6 +1626,120 @@
"@babel/runtime": "^7.16.3" "@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": { "@mui/material": {
"version": "5.2.7", "version": "5.2.7",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz",
@ -3306,6 +3457,11 @@
"type": "^1.0.1" "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": { "dayjs": {
"version": "1.10.7", "version": "1.10.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz", "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", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" "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": { "rimraf": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",

View File

@ -27,9 +27,11 @@
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@mui/icons-material": "^5.2.5", "@mui/icons-material": "^5.2.5",
"@mui/lab": "^5.0.0-alpha.73",
"@mui/material": "^5.2.7", "@mui/material": "^5.2.7",
"@mui/system": "^5.2.6", "@mui/system": "^5.2.6",
"@mui/x-data-grid": "^5.2.2", "@mui/x-data-grid": "^5.2.2",
"date-fns": "^2.28.0",
"material-ui-image": "^3.3.2", "material-ui-image": "^3.3.2",
"react-countdown": "^2.3.2", "react-countdown": "^2.3.2",
"react-native": "^0.66.4", "react-native": "^0.66.4",

View File

@ -1,8 +1,12 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { render } from "react-dom"; import { render } from "react-dom";
import HomePage from "./HomePage"; 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 { export default class App extends Component {
constructor(props) { constructor(props) {
@ -10,6 +14,7 @@ export default class App extends Component {
this.state = { this.state = {
nickname: null, nickname: null,
token: null, token: null,
dark: false,
} }
} }
@ -17,11 +22,28 @@ export default class App extends Component {
this.setState(newState) this.setState(newState)
} }
lightTheme = createTheme({
});
darkTheme = createTheme({
palette: {
mode: 'dark',
background: {
default: "#070707"
},
},
});
render() { render() {
return ( return (
<> <ThemeProvider theme={this.state.dark ? this.darkTheme : this.lightTheme}>
<HomePage setAppState={this.setAppState}/> <CssBaseline/>
</> <IconButton sx={{position:'fixed',right:'0px'}} onClick={()=>this.setState({dark:!this.state.dark})}>
{this.state.dark ? <LightModeIcon/>:<DarkModeIcon/>}
</IconButton>
<UnsafeAlert className="unsafeAlert"/>
<HomePage setAppState={this.setAppState}/>
</ThemeProvider>
); );
} }
} }

View File

@ -78,6 +78,15 @@ export default class BookPage extends Component {
if(status=='Seen recently'){return("warning")} if(status=='Seen recently'){return("warning")}
if(status=='Inactive'){return('error')} if(status=='Inactive'){return('error')}
} }
amountToString = (amount,has_range,min_amount,max_amount) => {
if (has_range){
console.log(this.pn(parseFloat(Number(min_amount).toPrecision(2))))
console.log(this.pn(parseFloat(Number(min_amount).toPrecision(2)))+'-'+this.pn(parseFloat(Number(max_amount).toPrecision(2))))
return this.pn(parseFloat(Number(min_amount).toPrecision(2)))+'-'+this.pn(parseFloat(Number(max_amount).toPrecision(2)))
}else{
return this.pn(parseFloat(Number(amount).toPrecision(3)))
}
}
bookListTableDesktop=()=>{ bookListTableDesktop=()=>{
return ( return (
@ -90,7 +99,10 @@ export default class BookPage extends Component {
robot: order.maker_nick, robot: order.maker_nick,
robot_status: order.maker_status, robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer", type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(5)), amount: order.amount,
has_range: order.has_range,
min_amount: order.min_amount,
max_amount: order.max_amount,
currency: this.getCurrencyCode(order.currency), currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method, payment_method: order.payment_method,
price: order.price, price: order.price,
@ -123,9 +135,9 @@ export default class BookPage extends Component {
); );
} }, } },
{ field: 'type', headerName: 'Is', width: 60 }, { field: 'type', headerName: 'Is', width: 60 },
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80, { field: 'amount', headerName: 'Amount', type: 'number', width: 90,
renderCell: (params) => {return ( renderCell: (params) => {return (
<div style={{ cursor: "pointer" }}>{this.pn(params.row.amount)}</div> <div style={{ cursor: "pointer" }}>{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}</div>
)}}, )}},
{ field: 'currency', headerName: 'Currency', width: 100, { field: 'currency', headerName: 'Currency', width: 100,
renderCell: (params) => {return ( renderCell: (params) => {return (
@ -163,7 +175,10 @@ export default class BookPage extends Component {
robot: order.maker_nick, robot: order.maker_nick,
robot_status: order.maker_status, robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer", type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(4)), amount: order.amount,
has_range: order.has_range,
min_amount: order.min_amount,
max_amount: order.max_amount,
currency: this.getCurrencyCode(order.currency), currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method, payment_method: order.payment_method,
price: order.price, price: order.price,
@ -191,10 +206,10 @@ export default class BookPage extends Component {
); );
} }, } },
{ field: 'type', headerName: 'Is', width: 60, hide:'true'}, { field: 'type', headerName: 'Is', width: 60, hide:'true'},
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80, { field: 'amount', headerName: 'Amount', type: 'number', width: 90,
renderCell: (params) => {return ( renderCell: (params) => {return (
<Tooltip placement="right" enterTouchDelay="0" title={params.row.type}> <Tooltip placement="right" enterTouchDelay="0" title={params.row.type}>
<div style={{ cursor: "pointer" }}>{this.pn(params.row.amount)}</div> <div style={{ cursor: "pointer" }}>{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}</div>
</Tooltip> </Tooltip>
)} }, )} },
{ field: 'currency', headerName: 'Currency', width: 100, { field: 'currency', headerName: 'Currency', width: 100,

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import {Chip, CircularProgress, Badge, Tooltip, TextField, ListItemAvatar, Button, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material"; import {FormControlLabel, Link, Switch, CircularProgress, Badge, Tooltip, TextField, ListItemAvatar, Button, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
import MediaQuery from 'react-responsive' import MediaQuery from 'react-responsive'
import { Link } from 'react-router-dom' import { Link as LinkRouter } from 'react-router-dom'
// Icons // Icons
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
@ -24,6 +24,7 @@ import WebIcon from '@mui/icons-material/Web';
import BookIcon from '@mui/icons-material/Book'; import BookIcon from '@mui/icons-material/Book';
import PersonAddAltIcon from '@mui/icons-material/PersonAddAlt'; import PersonAddAltIcon from '@mui/icons-material/PersonAddAlt';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import AmbossIcon from "./icons/AmbossIcon"
// pretty numbers // pretty numbers
function pn(x) { function pn(x) {
@ -74,7 +75,8 @@ export default class BottomBar extends Component {
profileShown: false, profileShown: false,
alternative_site: 'robosats...', alternative_site: 'robosats...',
node_id: '00000000', node_id: '00000000',
referral_link: 'Loading...', showRewards: false,
referral_code: '',
earned_rewards: 0, earned_rewards: 0,
rewardInvoice: null, rewardInvoice: null,
badInvoice: false, badInvoice: false,
@ -88,8 +90,8 @@ export default class BottomBar extends Component {
this.setState(null) this.setState(null)
fetch('/api/info/') fetch('/api/info/')
.then((response) => response.json()) .then((response) => response.json())
.then((data) => this.setState(data) & .then((data) => this.setState(data) & this.setState({active_order_id: data.active_order_id ? data.active_order_id : null})
this.props.setAppState({nickname:data.nickname, loading:false})); & this.props.setAppState({nickname:data.nickname, loading:false}));
} }
handleClickOpenStatsForNerds = () => { handleClickOpenStatsForNerds = () => {
@ -123,31 +125,28 @@ export default class BottomBar extends Component {
<ListItem> <ListItem>
<ListItemIcon><DnsIcon/></ListItemIcon> <ListItemIcon><DnsIcon/></ListItemIcon>
<ListItemText secondary={this.state.node_alias}> <ListItemText secondary={this.state.node_alias}>
<a target="_blank" href={"https://1ml.com/testnet/node/" <Link target="_blank" href={"https://1ml.com/testnet/node/"
+ this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (1ML)"} + this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (1ML)"}
</a> </Link>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
: :
<ListItem> <ListItem>
<ListItemAvatar> <ListItemIcon><AmbossIcon/></ListItemIcon>
<Avatar sx={{ width: 25, height:25, }} src='/static/assets/images/amboss.png' variant="rounded"/>
</ListItemAvatar>
<ListItemText secondary={this.state.node_alias}> <ListItemText secondary={this.state.node_alias}>
<a target="_blank" href={"https://amboss.space/node/" <Link target="_blank" href={"https://amboss.space/node/"
+ this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (AMBOSS)"} + this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (AMBOSS)"}
</a> </Link>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
} }
<Divider/> <Divider/>
<ListItem> <ListItem>
<ListItemIcon><WebIcon/></ListItemIcon> <ListItemIcon><WebIcon/></ListItemIcon>
<ListItemText secondary={this.state.alternative_name}> <ListItemText secondary={this.state.alternative_name}>
<a target="_blank" href={"http://"+this.state.alternative_site}>{this.state.alternative_site.slice(0, 12)+"...onion"} <Link target="_blank" href={"http://"+this.state.alternative_site}>{this.state.alternative_site.slice(0, 12)+"...onion"}
</a> </Link>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
@ -155,9 +154,9 @@ export default class BottomBar extends Component {
<ListItem> <ListItem>
<ListItemIcon><GitHubIcon/></ListItemIcon> <ListItemIcon><GitHubIcon/></ListItemIcon>
<ListItemText secondary="Currently running commit hash"> <ListItemText secondary="Currently running commit hash">
<a target="_blank" href={"https://github.com/Reckless-Satoshi/robosats/tree/" <Link target="_blank" href={"https://github.com/Reckless-Satoshi/robosats/tree/"
+ this.state.robosats_running_commit_hash}>{this.state.robosats_running_commit_hash.slice(0, 12)+"..."} + this.state.robosats_running_commit_hash}>{this.state.robosats_running_commit_hash.slice(0, 12)+"..."}
</a> </Link>
</ListItemText> </ListItemText>
</ListItem> </ListItem>
@ -272,6 +271,11 @@ export default class BottomBar extends Component {
})); }));
} }
getHost(){
var url = (window.location != window.parent.location) ? this.getHost(document.referrer) : document.location.href;
return url.split('/')[2]
}
dialogProfile =() =>{ dialogProfile =() =>{
return( return(
<Dialog <Dialog
@ -301,8 +305,7 @@ export default class BottomBar extends Component {
<Divider/> <Divider/>
{this.state.active_order_id ? {this.state.active_order_id ?
// TODO Link to router and do this.props.history.push <ListItemButton onClick={this.handleClickCloseProfile} to={'/order/'+this.state.active_order_id} component={LinkRouter}>
<ListItemButton onClick={this.handleClickCloseProfile} to={'/order/'+this.state.active_order_id} component={Link}>
<ListItemIcon> <ListItemIcon>
<Badge badgeContent="" color="primary"> <Badge badgeContent="" color="primary">
<NumbersIcon color="primary"/> <NumbersIcon color="primary"/>
@ -343,80 +346,92 @@ export default class BottomBar extends Component {
</ListItemText> </ListItemText>
</ListItem> </ListItem>
<Divider><Chip label='Rewards & Compensations'/></Divider> <Divider/>
<ListItem>
<ListItemIcon> <Grid spacing={1} align="center">
<PersonAddAltIcon/> <FormControlLabel labelPlacement="start"control={
</ListItemIcon> <Switch
<ListItemText secondary="Share to earn 100 Sats per trade"> checked={this.state.showRewards}
<TextField onChange={()=> this.setState({showRewards: !this.state.showRewards})}/>}
label='Your Referral Link' label="Rewards and compensations"
value={this.state.referral_link}
// variant='filled'
size='small'
InputProps={{
endAdornment:
<Tooltip disableHoverListener enterTouchDelay="0" title="Copied!">
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.referral_link)}>
<ContentCopy />
</IconButton>
</Tooltip>,
}}
/> />
</ListItemText> </Grid>
</ListItem>
<ListItem> <div style={{ display: this.state.showRewards ? '':'none'}}>
<ListItemIcon> <ListItem>
<EmojiEventsIcon/> <ListItemIcon>
</ListItemIcon> <PersonAddAltIcon/>
{!this.state.openClaimRewards ? </ListItemIcon>
<ListItemText secondary="Your earned rewards"> <ListItemText secondary="Share to earn 100 Sats per trade">
<Grid container xs={12}> <TextField
<Grid item xs={9}> label='Your referral link'
<Typography>{this.state.earned_rewards+" Sats"}</Typography> value={this.getHost()+'/ref/'+this.state.referral_code}
</Grid> // variant='filled'
<Grid item xs={3}> size='small'
<Button disabled={this.state.earned_rewards==0? true : false} onClick={() => this.setState({openClaimRewards:true})} variant="contained" size="small">Claim</Button> InputProps={{
</Grid> endAdornment:
</Grid> <Tooltip disableHoverListener enterTouchDelay="0" title="Copied!">
</ListItemText> <IconButton onClick= {()=>navigator.clipboard.writeText('http://'+this.getHost()+'/ref/'+this.state.referral_code)}>
: <ContentCopy />
<form style={{maxWidth: 270}}> </IconButton>
<Grid alignItems="stretch" style={{ display: "flex"}} align="center"> </Tooltip>,
<Grid item alignItems="stretch" style={{ display: "flex" }} align="center">
<TextField
error={this.state.badInvoice}
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
label={"Invoice for " + this.state.earned_rewards + " Sats"}
//variant="standard"
size="small"
value={this.state.rewardInvoice}
onChange={e => {
this.setState({ rewardInvoice: e.target.value });
}} }}
/> />
</ListItemText>
</ListItem>
<ListItem>
<ListItemIcon>
<EmojiEventsIcon/>
</ListItemIcon>
{!this.state.openClaimRewards ?
<ListItemText secondary="Your earned rewards">
<Grid container xs={12}>
<Grid item xs={9}>
<Typography>{this.state.earned_rewards+" Sats"}</Typography>
</Grid>
<Grid item xs={3}>
<Button disabled={this.state.earned_rewards==0? true : false} onClick={() => this.setState({openClaimRewards:true})} variant="contained" size="small">Claim</Button>
</Grid>
</Grid> </Grid>
<Grid item alignItems="stretch" style={{ display: "flex" }}> </ListItemText>
<Button sx={{maxHeight:38}} onClick={this.handleSubmitInvoiceClicked} variant="contained" color="primary" size="small" > Submit </Button> :
<form style={{maxWidth: 270}}>
<Grid alignItems="stretch" style={{ display: "flex"}} align="center">
<Grid item alignItems="stretch" style={{ display: "flex" }} align="center">
<TextField
error={this.state.badInvoice}
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
label={"Invoice for " + this.state.earned_rewards + " Sats"}
//variant="standard"
size="small"
value={this.state.rewardInvoice}
onChange={e => {
this.setState({ rewardInvoice: e.target.value });
}}
/>
</Grid>
<Grid item alignItems="stretch" style={{ display: "flex" }}>
<Button sx={{maxHeight:38}} onClick={this.handleSubmitInvoiceClicked} variant="contained" color="primary" size="small" > Submit </Button>
</Grid>
</Grid> </Grid>
</Grid> </form>
</form> }
} </ListItem>
</ListItem>
{this.state.showRewardsSpinner? {this.state.showRewardsSpinner?
<div style={{display: 'flex', justifyContent: 'center'}}> <div style={{display: 'flex', justifyContent: 'center'}}>
<CircularProgress/> <CircularProgress/>
</div> </div>
:""} :""}
{this.state.withdrawn? {this.state.withdrawn?
<div style={{display: 'flex', justifyContent: 'center'}}> <div style={{display: 'flex', justifyContent: 'center'}}>
<Typography color="primary" variant="body2"><b>There it goes, thank you!🥇</b></Typography> <Typography color="primary" variant="body2"><b>There it goes, thank you!🥇</b></Typography>
</div> </div>
:""} :""}
</div>
</List> </List>
</DialogContent> </DialogContent>

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import {Button, Badge, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText, Typography} from "@mui/material"; import {Button, Link, Badge, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText, Typography} from "@mui/material";
import ReconnectingWebSocket from 'reconnecting-websocket'; import ReconnectingWebSocket from 'reconnecting-websocket';
export default class Chat extends Component { export default class Chat extends Component {
@ -85,7 +85,7 @@ export default class Chat extends Component {
<Grid item xs={0.3}/> <Grid item xs={0.3}/>
<Grid item xs={5.5}> <Grid item xs={5.5}>
<Paper elevation={1} style={this.state.connected ? {backgroundColor: '#e8ffe6'}: {backgroundColor: '#FFF1C5'}}> <Paper elevation={1} style={this.state.connected ? {backgroundColor: '#e8ffe6'}: {backgroundColor: '#FFF1C5'}}>
<Typography variant='caption' > <Typography variant='caption' sx={{color: '#111111'}}>
You: {this.state.connected ? 'connected': 'disconnected'} You: {this.state.connected ? 'connected': 'disconnected'}
</Typography> </Typography>
</Paper> </Paper>
@ -93,19 +93,19 @@ export default class Chat extends Component {
<Grid item xs={0.4}/> <Grid item xs={0.4}/>
<Grid item xs={5.5}> <Grid item xs={5.5}>
<Paper elevation={1} style={this.state.peer_connected ? {backgroundColor: '#e8ffe6'}: {backgroundColor: '#FFF1C5'}}> <Paper elevation={1} style={this.state.peer_connected ? {backgroundColor: '#e8ffe6'}: {backgroundColor: '#FFF1C5'}}>
<Typography variant='caption'> <Typography variant='caption' sx={{color: '#111111'}}>
Peer: {this.state.peer_connected ? 'connected': 'disconnected'} Peer: {this.state.peer_connected ? 'connected': 'disconnected'}
</Typography> </Typography>
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={0.3}/> <Grid item xs={0.3}/>
</Grid> </Grid>
<Paper elevation={1} style={{ height: 300, maxHeight: 300, width:280,overflow: 'auto', backgroundColor: '#F7F7F7' }}> <Paper elevation={1} style={{ height: '300px', maxHeight: '300px' , width: '280px' ,overflow: 'auto', backgroundColor: '#F7F7F7' }}>
{this.state.messages.map(message => <> {this.state.messages.map(message => <>
<Card elevation={5} align="left" > <Card elevation={5} align="left" >
{/* If message sender is not our nick, gray color, if it is our nick, green color */} {/* If message sender is not our nick, gray color, if it is our nick, green color */}
{message.userNick == this.props.ur_nick ? {message.userNick == this.props.ur_nick ?
<CardHeader <CardHeader sx={{color: '#111111'}}
avatar={ avatar={
<Badge variant="dot" overlap="circular" badgeContent="" color={this.state.connected ? "success" : "error"}> <Badge variant="dot" overlap="circular" badgeContent="" color={this.state.connected ? "success" : "error"}>
<Avatar className="flippedSmallAvatar" <Avatar className="flippedSmallAvatar"
@ -117,10 +117,10 @@ export default class Chat extends Component {
style={{backgroundColor: '#eeeeee'}} style={{backgroundColor: '#eeeeee'}}
title={message.userNick} title={message.userNick}
subheader={message.msg} subheader={message.msg}
subheaderTypographyProps={{sx: {wordWrap: "break-word", width: 200}}} subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}}
/> />
: :
<CardHeader <CardHeader sx={{color: '#111111'}}
avatar={ avatar={
<Badge variant="dot" overlap="circular" badgeContent="" color={this.state.peer_connected ? "success" : "error"}> <Badge variant="dot" overlap="circular" badgeContent="" color={this.state.peer_connected ? "success" : "error"}>
<Avatar className="flippedSmallAvatar" <Avatar className="flippedSmallAvatar"
@ -132,7 +132,7 @@ export default class Chat extends Component {
style={{backgroundColor: '#fafafa'}} style={{backgroundColor: '#fafafa'}}
title={message.userNick} title={message.userNick}
subheader={message.msg} subheader={message.msg}
subheaderTypographyProps={{sx: {wordWrap: "break-word", width: 200}}} subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}}
/>} />}
</Card> </Card>
</>)} </>)}
@ -160,7 +160,7 @@ export default class Chat extends Component {
</Grid> </Grid>
</form> </form>
<FormHelperText> <FormHelperText>
The chat has no memory: if you leave, messages are lost. <a target="_blank" href="https://github.com/Reckless-Satoshi/robosats/blob/main/docs/sensitive-data-PGP-guide.md/"> Learn easy PGP encryption.</a> The chat has no memory: if you leave, messages are lost. <Link target="_blank" href="https://github.com/Reckless-Satoshi/robosats/blob/main/docs/sensitive-data-PGP-guide.md/"> Learn easy PGP encryption.</Link>
</FormHelperText> </FormHelperText>
</Container> </Container>
) )

View File

@ -1,5 +1,5 @@
import {Typography, DialogActions, DialogContent, Button, Grid} from "@mui/material" import {Typography, Link, DialogActions, DialogContent, Button, Grid} from "@mui/material"
import React, { Component } from 'react' import React, { Component } from 'react'
import Image from 'material-ui-image' import Image from 'material-ui-image'
import MediaQuery from 'react-responsive' import MediaQuery from 'react-responsive'
@ -18,8 +18,8 @@ export default class InfoDialog extends Component {
<p>It is a BTC/FIAT peer-to-peer exchange over lightning. <br/> It simplifies <p>It is a BTC/FIAT peer-to-peer exchange over lightning. <br/> It simplifies
matchmaking and minimizes the need of trust. RoboSats focuses in privacy and speed.</p> matchmaking and minimizes the need of trust. RoboSats focuses in privacy and speed.</p>
<p>RoboSats is an open source project <a <p>RoboSats is an open source project <Link
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a> href='https://github.com/reckless-satoshi/robosats'>(GitHub).</Link>
</p> </p>
</Typography> </Typography>
</Grid> </Grid>
@ -38,13 +38,13 @@ export default class InfoDialog extends Component {
<Typography component="h4" variant="h4">What is <i>RoboSats</i>?</Typography> <Typography component="h4" variant="h4">What is <i>RoboSats</i>?</Typography>
<Typography component="body2" variant="body2"> <Typography component="body2" variant="body2">
<p>It is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies <p>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.</p> matchmaking and minimizes the need for trust. RoboSats focuses in privacy and speed.</p>
<img <img
width='100%' width='100%'
src={window.location.origin +'/static/assets/images/robosats_0.1.0_banner.png'} src={window.location.origin +'/static/assets/images/robosats_0.1.0_banner.png'}
/> />
<p>RoboSats is an open source project <a <p>RoboSats is an open source project <Link
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a> href='https://github.com/reckless-satoshi/robosats'>(GitHub).</Link>
</p> </p>
</Typography> </Typography>
</MediaQuery> </MediaQuery>
@ -59,10 +59,11 @@ export default class InfoDialog extends Component {
received the fiat, then the satoshis are released to Bob. Enjoy your satoshis, received the fiat, then the satoshis are released to Bob. Enjoy your satoshis,
Bob!</p> Bob!</p>
<p>At no point, AnonymousAlice01 and BafflingBob02 have to trust the <p>At no point, AnonymousAlice01 and BafflingBob02 have to entrust the
bitcoin funds to each other. In case they have a conflict, <i>RoboSats</i> staff bitcoin funds to each other. In case they have a conflict, <i>RoboSats</i> staff
will help resolving the dispute. You can find a step-by-step will help resolving the dispute. You can find a step-by-step
description of the trade pipeline in <a href='https://github.com/Reckless-Satoshi/robosats/blob/main/README.md#how-it-works'>How it works</a></p> description of the trade pipeline in <Link href='https://github.com/Reckless-Satoshi/robosats/blob/main/README.md#how-it-works'>How it works</Link>
You can also check the full guide in <Link href='https://github.com/Reckless-Satoshi/robosats/blob/main/docs/how-to-use.md'>How to use</Link></p>
</Typography> </Typography>
<Typography component="h5" variant="h5">What payment methods are accepted?</Typography> <Typography component="h5" variant="h5">What payment methods are accepted?</Typography>
@ -119,8 +120,8 @@ export default class InfoDialog extends Component {
time. For large amounts use an onchain escrow service such as <i>Bisq</i> time. For large amounts use an onchain escrow service such as <i>Bisq</i>
</p> </p>
<p> You can build more trust on <i>RoboSats</i> by <a href='https://github.com/reckless-satoshi/robosats'> <p> You can build more trust on <i>RoboSats</i> by <Link href='https://github.com/reckless-satoshi/robosats'>
inspecting the source code. </a> </p> inspecting the source code. </Link> </p>
</Typography> </Typography>
<Typography component="h5" variant="h5">What happens if <i>RoboSats</i> suddenly disappears?</Typography> <Typography component="h5" variant="h5">What happens if <i>RoboSats</i> suddenly disappears?</Typography>
@ -148,8 +149,8 @@ export default class InfoDialog extends Component {
<Typography component="body2" variant="body2"> <Typography component="body2" variant="body2">
<p> This lightning application is provided as is. It is in active <p> This lightning application is provided as is. It is in active
development: trade with the utmost caution. There is no private development: trade with the utmost caution. There is no private
support. Support is only offered via public channels <a href='https://t.me/robosats'> support. Support is only offered via public channels <Link href='https://t.me/robosats'>
(Telegram)</a>. <i>RoboSats</i> will never contact you. <i> (Telegram)</Link>. <i>RoboSats</i> will never contact you. <i>
RoboSats</i> will definitely never ask for your robot token. RoboSats</i> will definitely never ask for your robot token.
</p> </p>
</Typography> </Typography>

View File

@ -1,8 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material" import { LinearProgress, Link, Checkbox, Slider, Box, Tab, Tabs, SliderThumb, Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
import { Link } from 'react-router-dom' import { LocalizationProvider, TimePicker} from '@mui/lab';
import DateFnsUtils from "@date-io/date-fns";
import { Link as LinkRouter } from 'react-router-dom'
import { styled } from '@mui/material/styles';
import getFlags from './getFlags' import getFlags from './getFlags'
import LockIcon from '@mui/icons-material/Lock';
function getCookie(name) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
if (document.cookie && document.cookie !== '') { if (document.cookie && document.cookie !== '') {
@ -37,6 +42,9 @@ export default class MakerPage extends Component {
defaultPremium = 0; defaultPremium = 0;
minTradeSats = 20000; minTradeSats = 20000;
maxTradeSats = 800000; maxTradeSats = 800000;
maxBondlessSats = 50000;
maxRangeAmountMultiple = 4.8;
minRangeAmountMultiple = 1.6;
constructor(props) { constructor(props) {
super(props); super(props);
@ -48,11 +56,40 @@ export default class MakerPage extends Component {
payment_method: this.defaultPaymentMethod, payment_method: this.defaultPaymentMethod,
premium: 0, premium: 0,
satoshis: null, satoshis: null,
currencies_dict: {"1":"USD"} currencies_dict: {"1":"USD"},
showAdvanced: false,
allowBondless: false,
publicExpiryTime: new Date(0, 0, 0, 23, 59),
enableAmountRange: false,
minAmount: null,
bondSize: 1,
limits: null,
minAmount: null,
maxAmount: null,
loadingLimits: false,
} }
this.getCurrencyDict() this.getCurrencyDict()
} }
getLimits() {
this.setState({loadingLimits:true})
fetch('/api/limits/')
.then((response) => response.json())
.then((data) => this.setState({
limits:data,
loadingLimits:false,
minAmount: this.state.amount ? parseFloat((this.state.amount/2).toPrecision(2)) : parseFloat(Number(data[this.state.currency]['max_amount']*0.25).toPrecision(2)),
maxAmount: this.state.amount ? this.state.amount : parseFloat(Number(data[this.state.currency]['max_amount']*0.75).toPrecision(2)),
}));
}
a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
handleTypeChange=(e)=>{ handleTypeChange=(e)=>{
this.setState({ this.setState({
type: e.target.value, type: e.target.value,
@ -63,12 +100,64 @@ export default class MakerPage extends Component {
currency: e.target.value, currency: e.target.value,
currencyCode: this.getCurrencyCode(e.target.value), currencyCode: this.getCurrencyCode(e.target.value),
}); });
if(this.state.enableAmountRange){
this.setState({
minAmount: parseFloat(Number(this.state.limits[e.target.value]['max_amount']*0.25).toPrecision(2)),
maxAmount: parseFloat(Number(this.state.limits[e.target.value]['max_amount']*0.75).toPrecision(2)),
})
}
} }
handleAmountChange=(e)=>{ handleAmountChange=(e)=>{
this.setState({ this.setState({
amount: e.target.value, amount: e.target.value,
}); });
} }
handleMinAmountChange=(e)=>{
this.setState({
minAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
});
}
handleMaxAmountChange=(e)=>{
this.setState({
maxAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
});
}
handleRangeAmountChange = (e, newValue, activeThumb) => {
var maxAmount = this.getMaxAmount();
var minAmount = this.getMinAmount();
var lowerValue = e.target.value[0];
var upperValue = e.target.value[1];
var minRange = this.minRangeAmountMultiple;
var maxRange = this.maxRangeAmountMultiple;
if (lowerValue > maxAmount/minRange){
lowerValue = maxAmount/minRange
}
if (upperValue < minRange*minAmount){
upperValue = minRange*minAmount
}
if (lowerValue > upperValue/minRange) {
if (activeThumb === 0) {
upperValue = minRange*lowerValue
} else {
lowerValue = upperValue/minRange
}
}else if(lowerValue < upperValue/maxRange){
if (activeThumb === 0) {
upperValue = maxRange*lowerValue
} else {
lowerValue = upperValue/maxRange
}
}
this.setState({
minAmount: parseFloat(Number(lowerValue).toPrecision(lowerValue < 100 ? 2 : 3)),
maxAmount: parseFloat(Number(upperValue).toPrecision(upperValue < 100 ? 2 : 3)),
});
}
handlePaymentMethodChange=(e)=>{ handlePaymentMethodChange=(e)=>{
this.setState({ this.setState({
payment_method: e.target.value, payment_method: e.target.value,
@ -118,7 +207,6 @@ export default class MakerPage extends Component {
handleCreateOfferButtonPressed=()=>{ handleCreateOfferButtonPressed=()=>{
this.state.amount == null ? this.setState({amount: 0}) : null; this.state.amount == null ? this.setState({amount: 0}) : null;
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
@ -126,10 +214,16 @@ export default class MakerPage extends Component {
type: this.state.type, type: this.state.type,
currency: this.state.currency, currency: this.state.currency,
amount: this.state.amount, amount: this.state.amount,
has_range: this.state.enableAmountRange,
min_amount: this.state.minAmount,
max_amount: this.state.maxAmount,
payment_method: this.state.payment_method, payment_method: this.state.payment_method,
is_explicit: this.state.is_explicit, is_explicit: this.state.is_explicit,
premium: this.state.is_explicit ? null: this.state.premium, premium: this.state.is_explicit ? null: this.state.premium,
satoshis: this.state.is_explicit ? this.state.satoshis: null, satoshis: this.state.is_explicit ? this.state.satoshis: null,
public_duration: this.state.publicDuration,
bond_size: this.state.bondSize,
bondless_taker: this.state.allowBondless,
}), }),
}; };
fetch("/api/make/",requestOptions) fetch("/api/make/",requestOptions)
@ -152,8 +246,406 @@ export default class MakerPage extends Component {
return this.state.currencies_dict[val.toString()] return this.state.currencies_dict[val.toString()]
} }
handleInputBondSizeChange = (event) => {
this.setState({bondSize: event.target.value === '' ? 1 : Number(event.target.value)});
};
StandardMakerOptions = () => {
return(
<Paper elevation={12} style={{ padding: 8, width:'260px', align:'center'}}>
<Grid item xs={12} align="center" spacing={1}>
<FormControl component="fieldset">
<FormHelperText>
Buy or Sell Bitcoin?
</FormHelperText>
<RadioGroup row defaultValue="0" onChange={this.handleTypeChange}>
<FormControlLabel
value="0"
control={<Radio color="primary"/>}
label="Buy"
labelPlacement="Top"
/>
<FormControlLabel
value="1"
control={<Radio color="secondary"/>}
label="Sell"
labelPlacement="Top"
/>
</RadioGroup>
</FormControl>
</Grid>
<Grid containter xs={12} alignItems="stretch" style={{ display: "flex" }}>
<div style={{maxWidth:140}}>
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Amount of fiat to exchange for bitcoin">
<TextField
disabled = {this.state.enableAmountRange}
variant = {this.state.enableAmountRange ? 'filled' : 'outlined'}
error={this.state.amount <= 0 & this.state.amount != "" }
helperText={this.state.amount <= 0 & this.state.amount != "" ? 'Invalid' : null}
label="Amount"
type="number"
required="true"
value={this.state.amount}
inputProps={{
min:0 ,
style: {textAlign:"center"}
}}
onChange={this.handleAmountChange}
/>
</Tooltip>
</div>
<div >
<Select
required="true"
defaultValue={this.defaultCurrency}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}>
{Object.entries(this.state.currencies_dict)
.map( ([key, value]) => <MenuItem value={parseInt(key)}>
<div style={{display:'flex',alignItems:'center', flexWrap:'wrap'}}>{getFlags(value)}{" "+value}</div>
</MenuItem> )}
</Select>
</div>
</Grid>
<br/>
<Grid item xs={12} align="center">
<Tooltip placement="top" enterTouchDelay="300" enterDelay="700" enterNextDelay="2000" title="Enter your preferred fiat payment methods. Instant recommended (e.g., Revolut, CashApp ...)">
<TextField
sx={{width:240}}
label={this.state.currency==1000 ? "Swap Destination (e.g. rBTC)":"Fiat Payment Method(s)"}
error={this.state.badPaymentMethod}
helperText={this.state.badPaymentMethod ? "Must be shorter than 35 characters":""}
type="text"
require={true}
inputProps={{
style: {textAlign:"center"},
maxLength: 35
}}
onChange={this.handlePaymentMethodChange}
/>
</Tooltip>
</Grid>
<Grid item xs={12} align="center">
<FormControl component="fieldset">
<FormHelperText >
<div align='center'>
Choose a Pricing Method
</div>
</FormHelperText>
<RadioGroup row defaultValue="relative">
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Let the price move with the market">
<FormControlLabel
value="relative"
control={<Radio color="primary"/>}
label="Relative"
labelPlacement="Top"
onClick={this.handleClickRelative}
/>
</Tooltip>
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Set a fix amount of satoshis">
<FormControlLabel
disabled={this.state.enableAmountRange}
value="explicit"
control={<Radio color="secondary"/>}
label="Explicit"
labelPlacement="Top"
onClick={this.handleClickExplicit}
/>
</Tooltip>
</RadioGroup>
</FormControl>
</Grid>
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
<Grid item xs={12} align="center">
<div style={{display: this.state.is_explicit ? '':'none'}}>
<TextField
sx={{width:240}}
label="Satoshis"
error={this.state.badSatoshis}
helperText={this.state.badSatoshis}
type="number"
required="true"
value={this.state.satoshis}
inputProps={{
// TODO read these from .env file
min:this.minTradeSats ,
max:this.maxTradeSats ,
style: {textAlign:"center"}
}}
onChange={this.handleSatoshisChange}
// defaultValue={this.defaultSatoshis}
/>
</div>
<div style={{display: this.state.is_explicit ? 'none':''}}>
<TextField
sx={{width:240}}
error={this.state.badPremium}
helperText={this.state.badPremium}
label="Premium over Market (%)"
type="number"
// defaultValue={this.defaultPremium}
inputProps={{
min: -100,
max: 999,
style: {textAlign:"center"}
}}
onChange={this.handlePremiumChange}
/>
</div>
</Grid>
</Paper>
)
}
handleChangePublicDuration = (date) => {
let d = new Date(date),
hours = d.getHours(),
minutes = d.getMinutes();
var total_secs = hours*60*60 + minutes * 60;
this.setState({
changedPublicExpiryTime: true,
publicExpiryTime: date,
publicDuration: total_secs,
badDuration: false,
});
}
getMaxAmount = () => {
if (this.state.limits == null){
var max_amount = null
}else{
var max_amount = this.state.limits[this.state.currency]['max_amount']*(1+this.state.premium/100)
}
// times 0.98 to allow a bit of margin with respect to the backend minimum
return parseFloat(Number(max_amount*0.98).toPrecision(2))
}
getMinAmount = () => {
if (this.state.limits == null){
var min_amount = null
}else{
var min_amount = this.state.limits[this.state.currency]['min_amount']*(1+this.state.premium/100)
}
// times 1.1 to allow a bit of margin with respect to the backend minimum
return parseFloat(Number(min_amount*1.1).toPrecision(2))
}
RangeSlider = styled(Slider)(({ theme }) => ({
color: 'primary',
height: 3,
padding: '13px 0',
'& .MuiSlider-thumb': {
height: 27,
width: 27,
backgroundColor: '#fff',
border: '1px solid currentColor',
'&:hover': {
boxShadow: '0 0 0 8px rgba(58, 133, 137, 0.16)',
},
'& .range-bar': {
height: 9,
width: 1,
backgroundColor: 'currentColor',
marginLeft: 1,
marginRight: 1,
},
},
'& .MuiSlider-track': {
height: 3,
},
'& .MuiSlider-rail': {
color: theme.palette.mode === 'dark' ? '#bfbfbf' : '#d8d8d8',
opacity: theme.palette.mode === 'dark' ? undefined : 1,
height: 3,
},
}));
RangeThumbComponent(props) {
const { children, ...other } = props;
return (
<SliderThumb {...other}>
{children}
<span className="range-bar" />
<span className="range-bar" />
<span className="range-bar" />
</SliderThumb>
);
}
rangeText =()=> {
return (
<div style={{display:'flex',alignItems:'center', flexWrap:'wrap'}}>
<span style={{width: 40}}>From</span>
<TextField
variant="standard"
type="number"
size="small"
value={this.state.minAmount}
onChange={this.handleMinAmountChange}
error={this.state.minAmount < this.getMinAmount() || this.state.maxAmount < this.state.minAmount}
sx={{width: this.state.minAmount.toString().length * 9, maxWidth: 40}}
/>
<span style={{width: 20}}>to</span>
<TextField
variant="standard"
size="small"
type="number"
value={this.state.maxAmount}
error={this.state.maxAmount > this.getMaxAmount() || this.state.maxAmount < this.state.minAmount}
onChange={this.handleMaxAmountChange}
sx={{width: this.state.maxAmount.toString().length * 9, maxWidth: 50}}
/>
<span>{this.state.currencyCode}</span>
</div>
)
}
AdvancedMakerOptions = () => {
return(
<Paper elevation={12} style={{ padding: 8, width:'280px', align:'center'}}>
<Grid container xs={12} spacing={1}>
<Grid item xs={12} align="center" spacing={1}>
<FormControl align="center">
<FormHelperText>
<Tooltip enterTouchDelay="0" placement="top" align="center"title={"Let the taker chose an amount within the range"}>
<div align="center" style={{display:'flex',alignItems:'center', flexWrap:'wrap'}}>
<Checkbox onChange={(e)=>this.setState({enableAmountRange:e.target.checked}) & (e.target.checked ? this.getLimits() : null)}/>
{this.state.enableAmountRange & this.state.minAmount != null? <this.rangeText/> : "Enable Amount Range"}
</div>
</Tooltip>
</FormHelperText>
<div style={{ display: this.state.loadingLimits == true ? '':'none'}}>
<LinearProgress />
</div>
<div style={{ display: this.state.loadingLimits == false ? '':'none'}}>
<this.RangeSlider
disableSwap={true}
sx={{width:200, align:"center"}}
disabled={!this.state.enableAmountRange || this.state.loadingLimits}
value={[this.state.minAmount, this.state.maxAmount]}
step={(this.getMaxAmount()-this.getMinAmount())/5000}
valueLabelDisplay="auto"
components={{ Thumb: this.RangeThumbComponent }}
valueLabelFormat={(x) => (parseFloat(Number(x).toPrecision(x < 100 ? 2 : 3))+" "+this.state.currencyCode)}
marks={this.state.limits == null?
null
:
[{value: this.getMinAmount(),label: this.getMinAmount()+" "+ this.state.currencyCode},
{value: this.getMaxAmount(),label: this.getMaxAmount()+" "+this.state.currencyCode}]}
min={this.getMinAmount()}
max={this.getMaxAmount()}
onChange={this.handleRangeAmountChange}
/>
</div>
</FormControl>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<LocalizationProvider dateAdapter={DateFnsUtils}>
<TimePicker
sx={{width:210, align:"center"}}
ampm={false}
openTo="hours"
views={['hours', 'minutes']}
inputFormat="HH:mm"
mask="__:__"
renderInput={(props) => <TextField {...props} />}
label="Public Duration (HH:mm)"
value={this.state.publicExpiryTime}
onChange={this.handleChangePublicDuration}
minTime={new Date(0, 0, 0, 0, 10)}
maxTime={new Date(0, 0, 0, 23, 59)}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<FormControl align="center">
<Tooltip enterDelay="800" enterTouchDelay="0" placement="top" title={"Set the skin-in-the-game, increase for higher safety assurance"}>
<FormHelperText>
<div align="center" style={{display:'flex',flexWrap:'wrap', transform: 'translate(20%, 0)'}}>
Fidelity Bond Size <LockIcon sx={{height:20,width:20}}/>
</div>
</FormHelperText>
</Tooltip>
<Slider
sx={{width:220, align:"center"}}
aria-label="Bond Size (%)"
defaultValue={1}
valueLabelDisplay="auto"
valueLabelFormat={(x) => (x+'%')}
step={0.25}
marks={[{value: 1,label: '1%'},{value: 5,label: '5%'},{value: 10,label: '10%'},{value: 15,label: '15%'}]}
min={1}
max={15}
onChange={(e) => this.setState({bondSize: e.target.value})}
/>
</FormControl>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<Tooltip enterTouchDelay="0" title={"COMING SOON - High risk! Limited to "+ this.maxBondlessSats/1000 +"K Sats"}>
<FormControlLabel
label={<a>Allow bondless taker (<Link href="https://git.robosats.com" target="_blank">info</Link>)</a>}
control={
<Checkbox
disabled
//disabled={this.state.type==0}
color="secondary"
checked={this.state.allowBondless}
onChange={()=> this.setState({allowBondless: !this.state.allowBondless})}
/>
}
/>
</Tooltip>
</Grid>
</Grid>
</Paper>
)
}
makeOrderBox=()=>{
const [value, setValue] = React.useState(this.state.showAdvanced);
const handleChange = (event, newValue) => {
this.setState({showAdvanced:newValue})
setValue(newValue);
};
return(
<Box sx={{width: this.state.showAdvanced? '270px':'252px'}}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', position:'relative',left:'5px'}}>
<Tabs value={value? value:0} onChange={handleChange} variant="fullWidth" >
<Tab label="Basic" {...this.a11yProps(0)} />
<Tab label="Advanced" {...this.a11yProps(1)} />
</Tabs>
</Box>
<Grid item xs={12} align="center" spacing={1}>
<div style={{ display: this.state.showAdvanced == false ? '':'none'}}>
<this.StandardMakerOptions/>
</div>
<div style={{ display: this.state.showAdvanced == true ? '':'none'}}>
<this.AdvancedMakerOptions/>
</div>
</Grid>
</Box>
)
}
render() { render() {
return ( return (
<Grid container xs={12} align="center" spacing={1} sx={{minWidth:380}}> <Grid container xs={12} align="center" spacing={1} sx={{minWidth:380}}>
{/* <Grid item xs={12} align="center" sx={{minWidth:380}}> {/* <Grid item xs={12} align="center" sx={{minWidth:380}}>
@ -161,154 +653,14 @@ export default class MakerPage extends Component {
ORDER MAKER ORDER MAKER
</Typography> </Typography>
</Grid> */} </Grid> */}
<Grid item xs={12} align="center" spacing={1}>
<Paper elevation={12} style={{ padding: 8, width:240, align:'center'}}>
<Grid item xs={12} align="center" spacing={1}>
<FormControl component="fieldset">
<FormHelperText>
Buy or Sell Bitcoin?
</FormHelperText>
<RadioGroup row defaultValue="0" onChange={this.handleTypeChange}>
<FormControlLabel
value="0"
control={<Radio color="primary"/>}
label="Buy"
labelPlacement="Top"
/>
<FormControlLabel
value="1"
control={<Radio color="secondary"/>}
label="Sell"
labelPlacement="Top"
/>
</RadioGroup>
</FormControl>
</Grid>
<Grid containter xs={12} alignItems="stretch" style={{ display: "flex" }}>
<div style={{maxWidth:140}}>
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Amount of fiat to exchange for bitcoin">
<TextField
error={this.state.amount <= 0}
helperText={this.state.amount <= 0 ? 'Invalid' : null}
label="Amount"
type="number"
required="true"
inputProps={{
min:0 ,
style: {textAlign:"center"}
}}
onChange={this.handleAmountChange}
/>
</Tooltip>
</div>
<div >
<Select
required="true"
defaultValue={this.defaultCurrency}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}>
{Object.entries(this.state.currencies_dict)
.map( ([key, value]) => <MenuItem value={parseInt(key)}>
<div style={{display:'flex',alignItems:'center', flexWrap:'wrap'}}>{getFlags(value)}{" "+value}</div>
</MenuItem> )}
</Select>
</div>
</Grid>
<br/>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Tooltip placement="top" enterTouchDelay="300" enterDelay="700" enterNextDelay="2000" title="Enter your prefered fiat payment methods (instant recommended)"> <this.makeOrderBox/>
<TextField
sx={{width:240}}
label={this.state.currency==1000 ? "Swap Destination (e.g. rBTC)":"Fiat Payment Method(s)"}
error={this.state.badPaymentMethod}
helperText={this.state.badPaymentMethod ? "Must be shorter than 35 characters":""}
type="text"
require={true}
inputProps={{
style: {textAlign:"center"},
maxLength: 35
}}
onChange={this.handlePaymentMethodChange}
/>
</Tooltip>
</Grid> </Grid>
<Grid item xs={12} align="center">
<FormControl component="fieldset">
<FormHelperText >
<div align='center'>
Choose a Pricing Method
</div>
</FormHelperText>
<RadioGroup row defaultValue="relative">
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Let the price move with the market">
<FormControlLabel
value="relative"
control={<Radio color="primary"/>}
label="Relative"
labelPlacement="Top"
onClick={this.handleClickRelative}
/>
</Tooltip>
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Set a fix amount of satoshis">
<FormControlLabel
value="explicit"
control={<Radio color="secondary"/>}
label="Explicit"
labelPlacement="Top"
onClick={this.handleClickExplicit}
/>
</Tooltip>
</RadioGroup>
</FormControl>
</Grid>
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
<Grid item xs={12} align="center">
<div style={{display: this.state.is_explicit ? '':'none'}}>
<TextField
sx={{width:240}}
label="Satoshis"
error={this.state.badSatoshis}
helperText={this.state.badSatoshis}
type="number"
required="true"
value={this.state.satoshis}
inputProps={{
// TODO read these from .env file
min:this.minTradeSats ,
max:this.maxTradeSats ,
style: {textAlign:"center"}
}}
onChange={this.handleSatoshisChange}
// defaultValue={this.defaultSatoshis}
/>
</div>
<div style={{display: this.state.is_explicit ? 'none':''}}>
<TextField
sx={{width:240}}
error={this.state.badPremium}
helperText={this.state.badPremium}
label="Premium over Market (%)"
type="number"
// defaultValue={this.defaultPremium}
inputProps={{
min: -100,
max: 999,
style: {textAlign:"center"}
}}
onChange={this.handlePremiumChange}
/>
</div>
</Grid>
</Paper>
</Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
{/* conditions to disable the make button */} {/* conditions to disable the make button */}
{(this.state.amount == null || {(this.state.amount == null & (this.state.enableAmountRange == false & this.state.minAmount == null) ||
this.state.amount <= 0 || this.state.amount <= 0 & !this.state.enableAmountRange ||
(this.state.is_explicit & (this.state.badSatoshis != null || this.state.satoshis == null)) || (this.state.is_explicit & (this.state.badSatoshis != null || this.state.satoshis == null)) ||
(!this.state.is_explicit & this.state.badPremium != null)) (!this.state.is_explicit & this.state.badPremium != null))
? ?
@ -328,7 +680,8 @@ export default class MakerPage extends Component {
: ""} : ""}
<Typography component="subtitle2" variant="subtitle2"> <Typography component="subtitle2" variant="subtitle2">
<div align='center'> <div align='center'>
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {pn(this.state.amount)} {this.state.currencyCode} Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.enableAmountRange & this.state.minAmount != null?
" "+this.state.minAmount+"-"+this.state.maxAmount : pn(this.state.amount)} {this.state.currencyCode}
{this.state.is_explicit ? " of " + pn(this.state.satoshis) + " Satoshis" : {this.state.is_explicit ? " of " + pn(this.state.satoshis) + " Satoshis" :
(this.state.premium == 0 ? " at market price" : (this.state.premium == 0 ? " at market price" :
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount") (this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount")
@ -337,7 +690,7 @@ export default class MakerPage extends Component {
</div> </div>
</Typography> </Typography>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button color="secondary" variant="contained" to="/" component={Link}> <Button color="secondary" variant="contained" to="/" component={LinkRouter}>
Back Back
</Button> </Button>
</Grid> </Grid>

View File

@ -1,5 +1,5 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Chip, Tooltip, Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material" import {TextField,Chip, Tooltip, Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown'; import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
import MediaQuery from 'react-responsive' import MediaQuery from 'react-responsive'
@ -89,6 +89,7 @@ export default class OrderPage extends Component {
} }
var otherStateVars = { var otherStateVars = {
amount: newStateVars.amount ? newStateVars.amount : null,
loading: false, loading: false,
delay: this.setDelay(newStateVars.status), delay: this.setDelay(newStateVars.status),
currencyCode: this.getCurrencyCode(newStateVars.currency), currencyCode: this.getCurrencyCode(newStateVars.currency),
@ -130,7 +131,7 @@ export default class OrderPage extends Component {
return (<span> The order has expired</span>); return (<span> The order has expired</span>);
} else { } else {
var col = 'black' var col = 'inherit'
var fraction_left = (total/1000) / this.state.total_secs_exp var fraction_left = (total/1000) / this.state.total_secs_exp
// Make orange at 25% of time left // Make orange at 25% of time left
if (fraction_left < 0.25){col = 'orange'} if (fraction_left < 0.25){col = 'orange'}
@ -157,32 +158,92 @@ export default class OrderPage extends Component {
} }
}; };
countdownTakeOrderRenderer = ({ seconds, completed }) => { handleTakeAmountChange = (e) => {
if(isNaN(seconds)){ if (e.target.value != "" & e.target.value != null){
return ( this.setState({takeAmount: parseFloat(e.target.value)})
<> }else{
this.setState({takeAmount: e.target.value})
}
}
amountHelperText=()=>{
if(this.state.takeAmount < this.state.min_amount & this.state.takeAmount != ""){
return "Too low"
}else if (this.state.takeAmount > this.state.max_amount & this.state.takeAmount != ""){
return "Too high"
}else{
return null
}
}
takeOrderButton = () => {
if(this.state.has_range){
return(
<Grid containter xs={12} align="center" alignItems="stretch" justifyContent="center" style={{ display: "flex"}}>
<this.InactiveMakerDialog/>
<div style={{maxWidth:120}}>
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Enter amount of fiat to exchange for bitcoin">
<Paper elevation={5} sx={{maxHeight:40}}>
<TextField
error={(this.state.takeAmount < this.state.min_amount || this.state.takeAmount > this.state.max_amount) & this.state.takeAmount != "" }
helperText={this.amountHelperText()}
label={"Amount "+this.state.currencyCode}
size="small"
type="number"
required="true"
value={this.state.takeAmount}
inputProps={{
min:this.state.min_amount ,
max:this.state.max_amount ,
style: {textAlign:"center"}
}}
onChange={this.handleTakeAmountChange}
/>
</Paper>
</Tooltip>
</div>
<div style={{height:38, top:'1px', position:'relative', display: (this.state.takeAmount < this.state.min_amount || this.state.takeAmount > this.state.max_amount || this.state.takeAmount == "" || this.state.takeAmount == null) ? '':'none'}}>
<Tooltip placement="top" enterTouchDelay="0" enterDelay="500" enterNextDelay="1200" title="You must specify an amount first">
<Paper elevation={4}>
<Button sx={{height:38}} variant='contained' color='primary'
disabled={true}>
Take Order
</Button>
</Paper>
</Tooltip>
</div>
<div style={{height:38, top:'1px', position:'relative', display: (this.state.takeAmount < this.state.min_amount || this.state.takeAmount > this.state.max_amount || this.state.takeAmount == "" || this.state.takeAmount == null) ? 'none':''}}>
<Paper elevation={4}>
<Button sx={{height:38}} variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</Paper>
</div>
</Grid>
)
}else{
return(
<>
<this.InactiveMakerDialog/> <this.InactiveMakerDialog/>
<Button variant='contained' color='primary' <Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}> onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order Take Order
</Button> </Button>
</>) </>
)
} }
}
countdownTakeOrderRenderer = ({ seconds, completed }) => {
if(isNaN(seconds)){return (<this.takeOrderButton/>)}
if (completed) { if (completed) {
// Render a completed state // Render a completed state
return ( return ( <this.takeOrderButton/>);
<>
<this.InactiveMakerDialog/>
<Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>
);
} else{ } else{
return( return(
<Tooltip enterTouchDelay="0" title="Wait until you can take an order"><div> <Tooltip enterTouchDelay="0" title="Wait until you can take an order"><div>
<Button disabled={true} variant='contained' color='primary' onClick={this.takeOrder}>Take Order</Button> <Button disabled={true} variant='contained' color='primary'>Take Order</Button>
</div></Tooltip>) </div></Tooltip>)
} }
}; };
@ -212,12 +273,13 @@ export default class OrderPage extends Component {
takeOrder=()=>{ takeOrder=()=>{
this.setState({loading:true}) this.setState({loading:true})
console.log(this.state.takeAmount)
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({ body: JSON.stringify({
'action':'take', 'action':'take',
'amount':this.state.takeAmount,
}), }),
}; };
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
@ -483,10 +545,17 @@ export default class OrderPage extends Component {
<ListItemIcon> <ListItemIcon>
{getFlags(this.state.currencyCode)} {getFlags(this.state.currencyCode)}
</ListItemIcon> </ListItemIcon>
{this.state.has_range & this.state.amount == null ?
<ListItemText primary={parseFloat(Number(this.state.min_amount).toPrecision(2))
+"-" + parseFloat(Number(this.state.max_amount).toPrecision(2)) +" "+this.state.currencyCode} secondary="Amount range"/>
:
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4)) <ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))
+" "+this.state.currencyCode} secondary="Amount"/> +" "+this.state.currencyCode} secondary="Amount"/>
}
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemIcon> <ListItemIcon>
<PaymentsIcon/> <PaymentsIcon/>
@ -621,7 +690,7 @@ export default class OrderPage extends Component {
}; };
return( return(
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%'}}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} variant="fullWidth" > <Tabs value={value} onChange={handleChange} variant="fullWidth" >
<Tab label="Order" {...this.a11yProps(0)} /> <Tab label="Order" {...this.a11yProps(0)} />

View File

@ -1,5 +1,5 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { IconButton, Paper, Rating, Button, Tooltip, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material" import { IconButton, Box, Link, Paper, Rating, Button, Tooltip, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
import Countdown, { zeroPad} from 'react-countdown'; import Countdown, { zeroPad} from 'react-countdown';
import Chat from "./Chat" import Chat from "./Chat"
@ -214,7 +214,9 @@ export default class TradeBox extends Component {
} }
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<QRCode value={this.props.data.bond_invoice} size={305}/> <Box sx={{bgcolor:'#ffffff', width:'315px', position:'relative', left:'-5px'}} >
<QRCode value={this.props.data.bond_invoice} size={305} style={{position:'relative', top:'3px'}}/>
</Box>
<Tooltip disableHoverListener enterTouchDelay="0" title="Copied!"> <Tooltip disableHoverListener enterTouchDelay="0" title="Copied!">
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bond_invoice)}} align="center"> <ContentCopy/> Copy to clipboard</Button> <Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bond_invoice)}} align="center"> <ContentCopy/> Copy to clipboard</Button>
</Tooltip> </Tooltip>
@ -284,7 +286,9 @@ export default class TradeBox extends Component {
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<QRCode value={this.props.data.escrow_invoice} size={305}/> <Box sx={{bgcolor:'#ffffff', width:'315px', position:'relative', left:'-5px'}} >
<QRCode value={this.props.data.escrow_invoice} size={305} style={{position:'relative', top:'3px'}}/>
</Box>
<Tooltip disableHoverListener enterTouchDelay="0" title="Copied!"> <Tooltip disableHoverListener enterTouchDelay="0" title="Copied!">
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrow_invoice)}} align="center"> <ContentCopy/>Copy to clipboard</Button> <Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrow_invoice)}} align="center"> <ContentCopy/>Copy to clipboard</Button>
</Tooltip> </Tooltip>
@ -385,7 +389,7 @@ export default class TradeBox extends Component {
<Typography component="body2" variant="body2" align="left"> <Typography component="body2" variant="body2" align="left">
<p>Be patient while robots check the book. <p>Be patient while robots check the book.
It might take some time. This box will ring 🔊 once a robot takes your order. </p> It might take some time. This box will ring 🔊 once a robot takes your order. </p>
<p>Please note that if your premium is excessive, or your currency or payment <p>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 methods are not popular, your order might expire untaken. Your bond will
return to you (no action needed).</p> return to you (no action needed).</p>
</Typography> </Typography>
@ -910,7 +914,7 @@ handleRatingRobosatsChange=(e)=>{
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
<p><b>Thank you for using Robosats!</b></p> <p><b>Thank you for using Robosats!</b></p>
<p>Let us know how the platform could improve <p>Let us know how the platform could improve
(<a href="https://t.me/robosats">Telegram</a> / <a href="https://github.com/Reckless-Satoshi/robosats/issues">Github</a>)</p> (<Link href="https://t.me/robosats">Telegram</Link> / <Link href="https://github.com/Reckless-Satoshi/robosats/issues">Github</Link>)</p>
</Typography> </Typography>
</Grid> </Grid>
: null} : null}
@ -970,8 +974,8 @@ handleRatingRobosatsChange=(e)=>{
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
Your invoice has expired or more than 3 payment attempts have been made. Your invoice has expired or more than 3 payment attempts have been made.
Muun is not recommended, <a href="https://github.com/Reckless-Satoshi/robosats/issues/44">check the list of Muun wallet is not recommended, <Link href="https://github.com/Reckless-Satoshi/robosats/issues/44">check the list of
compatible wallets</a> compatible wallets</Link>
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -1011,7 +1015,7 @@ handleRatingRobosatsChange=(e)=>{
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
RoboSats will try to pay your invoice 3 times every 5 minutes. If it keeps failing, you RoboSats will try to pay your invoice 3 times every 5 minutes. If it keeps failing, you
will be able to submit a new invoice. Check whether you have enough inboud liquidity. will be able to submit a new invoice. Check whether you have enough inbound liquidity.
Remember that lightning nodes must be online in order to receive payments. Remember that lightning nodes must be online in order to receive payments.
</Typography> </Typography>
<List> <List>

View File

@ -0,0 +1,60 @@
import {Paper, Alert, AlertTitle, Button, Link} from "@mui/material"
import React, { Component } from 'react'
import MediaQuery from 'react-responsive'
export default class UnsafeAlert extends Component {
constructor(props) {
super(props);
}
state = {
show: true,
};
getHost(){
var url = (window.location != window.parent.location) ? this.getHost(document.referrer) : document.location.href;
return url.split('/')[2]
}
safe_urls = [
'robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion',
'robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion',
'robodevs7ixniseezbv7uryxhamtz3hvcelzfwpx3rvoipttjomrmpqd.onion',
]
render() {
return (
(!this.safe_urls.includes(this.getHost()) & this.state.show) ?
<div>
<MediaQuery minWidth={800}>
<Paper elevation={6} className="alertUnsafe">
<Alert severity="warning" sx={{maxHeight:"100px"}}
action={<Button onClick={() => this.setState({show:false})}>Hide</Button>}
>
<AlertTitle>You are not using RoboSats privately</AlertTitle>
Some features are disabled for your protection (e.g. chat) and you will not be able to complete a
trade without them. To protect your privacy and fully enable RoboSats, use <Link href='https://www.torproject.org/download/' target="_blank">Tor Browser</Link> and visit the <Link chref='http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion' target="_blank">Onion</Link> site.
</Alert>
</Paper>
</MediaQuery>
<MediaQuery maxWidth={799}>
<Paper elevation={6} className="alertUnsafe">
<Alert severity="warning" sx={{maxHeight:"120px"}}>
<AlertTitle>You are not using RoboSats privately</AlertTitle>
You will not be able to complete a
trade. Use <Link href='https://www.torproject.org/download/' target="_blank">Tor Browser</Link> and visit the <Link chref='http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion' target="_blank">Onion</Link> site.
<div style={{width: '100%'}}>
</div>
<div align="center">
<Button className="hideAlertButton" onClick={() => this.setState({show:false})}>Hide</Button>
</div>
</Alert>
</Paper>
</MediaQuery>
</div>
:
null
)
}
}

View File

@ -6,6 +6,7 @@ import InfoDialog from './InfoDialog'
import SmartToyIcon from '@mui/icons-material/SmartToy'; import SmartToyIcon from '@mui/icons-material/SmartToy';
import CasinoIcon from '@mui/icons-material/Casino'; import CasinoIcon from '@mui/icons-material/Casino';
import ContentCopy from "@mui/icons-material/ContentCopy"; import ContentCopy from "@mui/icons-material/ContentCopy";
import RoboSatsNoTextIcon from "./icons/RoboSatsNoTextIcon"
function getCookie(name) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
@ -149,6 +150,9 @@ export default class UserGenPage extends Component {
render() { render() {
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item>
<div className='clickTrough'/>
</Grid>
<Grid item xs={12} align="center" sx={{width:370, height:260}}> <Grid item xs={12} align="center" sx={{width:370, height:260}}>
{!this.state.loadingRobot ? {!this.state.loadingRobot ?
<div> <div>
@ -243,10 +247,24 @@ export default class UserGenPage extends Component {
<Button disabled={this.state.loadingRobot} color='secondary' to='/book/' component={Link}>View Book</Button> <Button disabled={this.state.loadingRobot} color='secondary' to='/book/' component={Link}>View Book</Button>
</ButtonGroup> </ButtonGroup>
</Grid> </Grid>
<Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Grid item xs={12} align="center" spacing={2} sx={{width:370}}>
Simple and Private Lightning peer-to-peer Exchange <Grid item>
</Typography> <div style={{height:40}}/>
</Grid>
<div style={{width:370, left:30}}>
<Grid container xs={12} align="center">
<Grid item xs={0.8}/>
<Grid item xs={7.5} align="right">
<Typography component="h5" variant="h5">
Simple and Private LN P2P Exchange
</Typography>
</Grid>
<Grid item xs={2.5} align="left">
<RoboSatsNoTextIcon color="primary" sx={{height:72, width:72}}/>
</Grid>
</Grid>
</div>
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@ -0,0 +1,18 @@
import React, { Component } from "react";
import { SvgIcon } from "@mui/material"
export default function AmbossIcon(props) {
return (
<SvgIcon {...props} x="0px" y="0px" viewBox="0 0 95.7 84.9">
<g id="Layer_2_00000052094167160547307180000012226084410257483709_">
<g id="Layer_1-2">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0" y1="42.45" x2="95.7" y2="42.45">
<stop offset="0" style={{stopColor:'#925bc9'}}/>
<stop offset="1" style={{stopColor:'#ff59ac'}}/>
</linearGradient>
<path class="amboss" d="M55.3,84.9V61.3h-15v23.6H0V0h95.7v84.9H55.3z M55.3,28.1h-15v17.1h15V28.1z"/>
</g>
</g>
</SvgIcon>
);
}

View File

@ -0,0 +1,81 @@
import React, { Component } from "react";
import { SvgIcon } from "@mui/material"
export default function RoboSatsIcon(props) {
return (
<SvgIcon {...props} x="0px" y="0px" width="1000px" height="1000px" viewBox="0 0 1000 900">
<g>
<path d="M602.336,731.51c16.747-16.94,29.249-35.524,37.504-56.694c18.792-48.193,16.967-94.996-10.46-139.81
c-10.255-16.756-24.983-29.293-39.461-42.103c-67.731-59.932-135.412-119.919-203.104-179.895
c-0.368-0.326-0.644-0.755-1.331-1.579c18.529-12.477,36.983-24.903,55.872-37.62c-9.61-6.799-18.917-13.385-28.648-20.27
c11.763-14.483,23.273-28.656,34.738-42.773c13.313,7.081,24.784,5.523,32.075-4.132c6.395-8.467,5.794-20.59-1.412-28.52
c-7.011-7.713-19.494-9.295-28.343-3.592c-10.274,6.623-12.651,17.652-6.576,31.65c-22.681,16.451-45.436,32.955-68.921,49.989
c9.33,6.786,18.246,13.271,27.611,20.084c-9.232,8.573-18.09,16.797-27.064,25.131c-68.348-47.917-134.895-38.158-199.771,10.745
c0-100.562,0-201.3,0-302.535c1.811-0.082,3.562-0.23,5.313-0.23c97.991-0.011,195.983-0.214,293.973,0.094
c37.661,0.119,75.351,1.898,112.093,11.01c52.81,13.096,95.741,40.904,125.379,87.462c13.802,21.681,20.643,45.764,23.136,71.039
c3.595,36.436,1.313,72.517-8.858,107.873c-11.943,41.515-37.09,74.011-69.641,101.357c-16.133,13.552-33.803,24.811-52.581,34.343
c-1.3,0.659-2.533,1.445-4.148,2.375c80.735,102.152,161.255,204.034,242.318,306.6C761.843,731.51,682.637,731.51,602.336,731.51z
"/>
<path d="M282.877,389.186c25.706-0.109,46.42,20.376,46.55,46.038c0.131,25.994-20.404,46.852-46.238,46.96
c-25.588,0.108-46.928-21.172-46.758-46.627C236.602,409.95,257.291,389.295,282.877,389.186z"/>
<path d="M445.93,607.736c0.705-26.031,21.515-46.381,46.915-45.881c26.295,0.52,46.657,21.756,45.918,47.887
c-0.721,25.455-21.862,45.67-47.178,45.104C465.779,654.273,445.244,633.082,445.93,607.736z"/>
<path d="M175.223,550.758c23.365,20.689,46.15,40.865,69.337,61.396c-4.974,5.619-9.792,11.063-14.91,16.846
c-5.634-4.988-11.167-9.738-16.519-14.684c-3.131-2.896-5.343-2.492-8.415,0.467c-9.944,9.58-20.234,18.801-29.493,27.332
C175.223,613.414,175.223,582.512,175.223,550.758z"/>
<path d="M379.124,731.533c-30.045,0-59.057,0-89.151,0c8.955-9.23,17.236-17.769,25.724-26.519
c-6.368-5.709-12.409-11.127-18.739-16.803c4.904-5.559,9.594-10.877,14.65-16.608C334.013,691.492,356.2,711.186,379.124,731.533z
"/>
</g>
<g>
<path d="M208.875,819.362h-15.495v34.557h-19.45v-94.397h35.075c11.151,0,19.752,2.485,25.804,7.455
c6.051,4.972,9.077,11.995,9.077,21.071c0,6.44-1.394,11.811-4.182,16.111s-7.013,7.727-12.675,10.276l20.422,38.576v0.907h-20.876
L208.875,819.362z M193.379,803.608h15.69c4.884,0,8.666-1.242,11.346-3.729c2.68-2.484,4.02-5.91,4.02-10.276
c0-4.451-1.264-7.952-3.792-10.503c-2.529-2.55-6.409-3.825-11.638-3.825h-15.625V803.608z"/>
<path d="M336.208,808.859c0,9.294-1.643,17.44-4.927,24.442c-3.285,7.002-7.985,12.405-14.101,16.209
c-6.117,3.804-13.129,5.705-21.039,5.705c-7.824,0-14.805-1.881-20.941-5.641c-6.138-3.761-10.892-9.131-14.263-16.111
c-3.372-6.979-5.08-15.009-5.122-24.086v-4.668c0-9.292,1.674-17.473,5.024-24.539c3.349-7.067,8.082-12.491,14.199-16.273
c6.116-3.781,13.106-5.673,20.974-5.673c7.866,0,14.857,1.892,20.974,5.673c6.116,3.782,10.849,9.206,14.199,16.273
c3.349,7.066,5.024,15.226,5.024,24.475V808.859z M316.499,804.58c0-9.896-1.773-17.418-5.316-22.562
c-3.545-5.144-8.602-7.716-15.171-7.716c-6.527,0-11.563,2.54-15.106,7.618c-3.545,5.079-5.339,12.524-5.381,22.335v4.604
c0,9.639,1.772,17.116,5.316,22.433c3.543,5.316,8.644,7.975,15.301,7.975c6.526,0,11.541-2.561,15.042-7.683
s5.272-12.588,5.316-22.4V804.58z"/>
<path d="M350.342,853.919v-94.397h33.065c11.453,0,20.141,2.193,26.063,6.58c5.921,4.388,8.882,10.817,8.882,19.288
c0,4.626-1.189,8.699-3.566,12.222c-2.377,3.522-5.684,6.105-9.919,7.747c4.84,1.211,8.655,3.653,11.443,7.326
c2.788,3.675,4.182,8.169,4.182,13.485c0,9.077-2.896,15.949-8.688,20.617c-5.792,4.668-14.047,7.046-24.767,7.132H350.342z
M369.792,799.069h14.393c9.811-0.172,14.717-4.084,14.717-11.734c0-4.279-1.243-7.358-3.728-9.239
c-2.486-1.88-6.408-2.82-11.767-2.82h-13.615V799.069z M369.792,812.814v25.479h16.662c4.581,0,8.158-1.091,10.73-3.274
c2.571-2.182,3.858-5.196,3.858-9.044c0-8.645-4.474-13.031-13.421-13.161H369.792z"/>
<path d="M512.621,808.859c0,9.294-1.645,17.44-4.928,24.442c-3.285,7.002-7.986,12.405-14.102,16.209
c-6.117,3.804-13.129,5.705-21.039,5.705c-7.824,0-14.805-1.881-20.941-5.641c-6.138-3.761-10.892-9.131-14.263-16.111
c-3.372-6.979-5.08-15.009-5.122-24.086v-4.668c0-9.292,1.674-17.473,5.024-24.539c3.349-7.067,8.082-12.491,14.199-16.273
c6.116-3.781,13.106-5.673,20.974-5.673c7.866,0,14.857,1.892,20.974,5.673c6.116,3.782,10.849,9.206,14.199,16.273
c3.35,7.066,5.025,15.226,5.025,24.475V808.859z M492.911,804.58c0-9.896-1.773-17.418-5.316-22.562
c-3.545-5.144-8.602-7.716-15.171-7.716c-6.527,0-11.563,2.54-15.106,7.618c-3.545,5.079-5.339,12.524-5.381,22.335v4.604
c0,9.639,1.772,17.116,5.316,22.433c3.543,5.316,8.644,7.975,15.301,7.975c6.526,0,11.541-2.561,15.042-7.683
s5.272-12.588,5.316-22.4V804.58z"/>
<path d="M575.704,829.152c0-3.673-1.297-6.493-3.891-8.461c-2.593-1.966-7.261-4.041-14.004-6.224
c-6.742-2.183-12.081-4.333-16.014-6.451c-10.72-5.791-16.079-13.593-16.079-23.405c0-5.1,1.437-9.648,4.312-13.647
c2.874-3.997,7.002-7.12,12.384-9.368c5.381-2.247,11.421-3.371,18.121-3.371c6.742,0,12.75,1.222,18.023,3.663
c5.272,2.442,9.368,5.89,12.286,10.341c2.917,4.452,4.376,9.509,4.376,15.171h-19.45c0-4.321-1.361-7.683-4.084-10.081
c-2.724-2.399-6.549-3.599-11.476-3.599c-4.756,0-8.451,1.005-11.087,3.015c-2.637,2.01-3.955,4.658-3.955,7.942
c0,3.069,1.545,5.641,4.636,7.715c3.09,2.075,7.64,4.02,13.647,5.835c11.064,3.329,19.126,7.456,24.184,12.384
c5.057,4.927,7.585,11.065,7.585,18.412c0,8.169-3.091,14.578-9.271,19.224c-6.181,4.646-14.501,6.97-24.961,6.97
c-7.261,0-13.874-1.329-19.839-3.987s-10.514-6.299-13.647-10.925c-3.134-4.624-4.7-9.984-4.7-16.078h19.515
c0,10.416,6.225,15.624,18.672,15.624c4.625,0,8.234-0.939,10.827-2.82C574.407,835.149,575.704,832.523,575.704,829.152z"/>
<path d="M661.673,834.469H627.57l-6.483,19.45h-20.682l35.14-94.397h18.023l35.335,94.397h-20.683L661.673,834.469z
M632.822,818.714h23.599l-11.864-35.334L632.822,818.714z"/>
<path d="M760.999,775.275h-28.916v78.644h-19.45v-78.644h-28.526v-15.754h76.893V775.275z"/>
<path d="M819.997,829.152c0-3.673-1.297-6.493-3.891-8.461c-2.593-1.966-7.261-4.041-14.004-6.224
c-6.742-2.183-12.081-4.333-16.014-6.451c-10.72-5.791-16.079-13.593-16.079-23.405c0-5.1,1.437-9.648,4.312-13.647
c2.874-3.997,7.002-7.12,12.384-9.368c5.381-2.247,11.421-3.371,18.121-3.371c6.742,0,12.75,1.222,18.023,3.663
c5.272,2.442,9.368,5.89,12.286,10.341c2.917,4.452,4.376,9.509,4.376,15.171h-19.45c0-4.321-1.361-7.683-4.084-10.081
c-2.724-2.399-6.549-3.599-11.476-3.599c-4.756,0-8.451,1.005-11.087,3.015c-2.637,2.01-3.955,4.658-3.955,7.942
c0,3.069,1.545,5.641,4.636,7.715c3.09,2.075,7.64,4.02,13.647,5.835c11.064,3.329,19.126,7.456,24.184,12.384
c5.057,4.927,7.585,11.065,7.585,18.412c0,8.169-3.091,14.578-9.271,19.224c-6.181,4.646-14.501,6.97-24.961,6.97
c-7.261,0-13.874-1.329-19.839-3.987s-10.514-6.299-13.647-10.925c-3.134-4.624-4.7-9.984-4.7-16.078h19.515
c0,10.416,6.225,15.624,18.672,15.624c4.625,0,8.234-0.939,10.827-2.82C818.7,835.149,819.997,832.523,819.997,829.152z"/>
</g>
</SvgIcon>
);
}

View File

@ -0,0 +1,32 @@
import React, { Component } from "react";
import { SvgIcon } from "@mui/material"
export default function RoboSatsNoTextIcon(props) {
return (
<SvgIcon {...props} x="0px" y="0px" width="1000px" height="1000px" viewBox="0 0 1000 800">
<g>
<path d="M602.336,731.51c16.747-16.94,29.249-35.524,37.504-56.694c18.792-48.193,16.967-94.996-10.46-139.81
c-10.255-16.756-24.983-29.293-39.461-42.103c-67.731-59.932-135.412-119.919-203.104-179.895
c-0.368-0.326-0.644-0.755-1.331-1.579c18.529-12.477,36.983-24.903,55.872-37.62c-9.61-6.799-18.917-13.385-28.648-20.27
c11.763-14.483,23.273-28.656,34.738-42.773c13.313,7.081,24.784,5.523,32.075-4.132c6.395-8.467,5.794-20.59-1.412-28.52
c-7.011-7.713-19.494-9.295-28.343-3.592c-10.274,6.623-12.651,17.652-6.576,31.65c-22.681,16.451-45.436,32.955-68.921,49.989
c9.33,6.786,18.246,13.271,27.611,20.084c-9.232,8.573-18.09,16.797-27.064,25.131c-68.348-47.917-134.895-38.158-199.771,10.745
c0-100.562,0-201.3,0-302.535c1.811-0.082,3.562-0.23,5.313-0.23c97.991-0.011,195.983-0.214,293.973,0.094
c37.661,0.119,75.351,1.898,112.093,11.01c52.81,13.096,95.741,40.904,125.379,87.462c13.802,21.681,20.643,45.764,23.136,71.039
c3.595,36.436,1.313,72.517-8.858,107.873c-11.943,41.515-37.09,74.011-69.641,101.357c-16.133,13.552-33.803,24.811-52.581,34.343
c-1.3,0.659-2.533,1.445-4.148,2.375c80.735,102.152,161.255,204.034,242.318,306.6C761.843,731.51,682.637,731.51,602.336,731.51z
"/>
<path d="M282.877,389.186c25.706-0.109,46.42,20.376,46.55,46.038c0.131,25.994-20.404,46.852-46.238,46.96
c-25.588,0.108-46.928-21.172-46.758-46.627C236.602,409.95,257.291,389.295,282.877,389.186z"/>
<path d="M445.93,607.736c0.705-26.031,21.515-46.381,46.915-45.881c26.295,0.52,46.657,21.756,45.918,47.887
c-0.721,25.455-21.862,45.67-47.178,45.104C465.779,654.273,445.244,633.082,445.93,607.736z"/>
<path d="M175.223,550.758c23.365,20.689,46.15,40.865,69.337,61.396c-4.974,5.619-9.792,11.063-14.91,16.846
c-5.634-4.988-11.167-9.738-16.519-14.684c-3.131-2.896-5.343-2.492-8.415,0.467c-9.944,9.58-20.234,18.801-29.493,27.332
C175.223,613.414,175.223,582.512,175.223,550.758z"/>
<path d="M379.124,731.533c-30.045,0-59.057,0-89.151,0c8.955-9.23,17.236-17.769,25.724-26.519
c-6.368-5.709-12.409-11.127-18.739-16.803c4.904-5.559,9.594-10.877,14.65-16.608C334.013,691.492,356.2,711.186,379.124,731.533z
"/>
</g>
</SvgIcon>
);
}

View File

@ -0,0 +1,76 @@
import React, { Component } from "react";
import { SvgIcon } from "@mui/material"
export default function RoboSatsTextIcon(props) {
return (
<SvgIcon {...props} x="0px" y="0px" width="2000px" height="1000px" viewBox="0 300 2000 1000">
<g>
<path d="M455.556,849.519c10.487-10.606,18.315-22.243,23.484-35.499c11.767-30.177,10.624-59.483-6.55-87.546
c-6.421-10.492-15.644-18.342-24.709-26.363c-42.412-37.528-84.791-75.089-127.178-112.646c-0.23-0.204-0.403-0.473-0.833-0.988
c11.603-7.813,23.158-15.593,34.985-23.557c-6.017-4.258-11.845-8.382-17.938-12.692c7.366-9.069,14.573-17.943,21.752-26.783
c8.336,4.434,15.519,3.458,20.084-2.588c4.005-5.302,3.629-12.893-0.884-17.858c-4.39-4.829-12.207-5.82-17.747-2.248
c-6.434,4.146-7.922,11.053-4.118,19.817c-14.202,10.302-28.45,20.636-43.156,31.302c5.842,4.249,11.425,8.311,17.289,12.576
c-5.781,5.368-11.328,10.518-16.947,15.736c-42.797-30.003-84.466-23.893-125.09,6.729c0-62.969,0-126.048,0-189.438
c1.134-0.051,2.23-0.144,3.327-0.144c61.359-0.006,122.719-0.134,184.077,0.059c23.582,0.074,47.182,1.188,70.189,6.894
c33.068,8.2,59.95,25.613,78.508,54.766c8.642,13.576,12.927,28.656,14.487,44.482c2.252,22.815,0.823,45.408-5.545,67.547
c-7.479,25.995-23.225,46.343-43.608,63.466c-10.102,8.486-21.167,15.536-32.924,21.505c-0.814,0.413-1.585,0.905-2.597,1.487
c50.553,63.965,100.971,127.76,151.731,191.983C555.434,849.519,505.838,849.519,455.556,849.519z"/>
<path d="M255.521,635.166c16.096-0.067,29.067,12.759,29.148,28.827c0.083,16.276-12.776,29.339-28.953,29.405
c-16.022,0.067-29.385-13.258-29.278-29.196C226.544,648.168,239.5,635.234,255.521,635.166z"/>
<path d="M357.619,772.016c0.441-16.3,13.472-29.043,29.376-28.729c16.465,0.325,29.215,13.623,28.752,29.985
c-0.451,15.939-13.688,28.597-29.541,28.242C370.048,801.155,357.19,787.886,357.619,772.016z"/>
<path d="M188.111,736.337c14.63,12.955,28.898,25.589,43.417,38.445c-3.115,3.519-6.132,6.927-9.336,10.548
c-3.528-3.123-6.993-6.098-10.344-9.194c-1.96-1.813-3.346-1.561-5.269,0.292c-6.227,5.999-12.67,11.772-18.468,17.114
C188.111,775.57,188.111,756.221,188.111,736.337z"/>
<path d="M315.788,849.533c-18.813,0-36.98,0-55.824,0c5.607-5.78,10.793-11.127,16.108-16.606
c-3.987-3.574-7.77-6.967-11.734-10.521c3.071-3.48,6.007-6.811,9.173-10.398C287.54,824.461,301.433,836.792,315.788,849.533z"/>
</g>
<g>
<path d="M766.812,758.155c0,18.361-3.246,34.457-9.734,48.289c-6.49,13.834-15.776,24.51-27.859,32.022
c-12.085,7.516-25.938,11.273-41.564,11.273c-15.458,0-29.249-3.715-41.374-11.145c-12.127-7.429-21.519-18.039-28.181-31.831
c-6.66-13.789-10.035-29.653-10.118-47.584v-9.223c0-18.358,3.308-34.521,9.927-48.481c6.617-13.962,15.968-24.678,28.052-32.15
c12.083-7.471,25.894-11.207,41.437-11.207c15.541,0,29.352,3.735,41.437,11.207c12.083,7.473,21.433,18.188,28.052,32.15
c6.616,13.961,9.926,30.081,9.926,48.354V758.155L766.812,758.155z M727.873,749.701c0-19.553-3.503-34.411-10.504-44.574
c-7.003-10.163-16.993-15.243-29.972-15.243c-12.895,0-22.845,5.017-29.846,15.05c-7.003,10.036-10.546,24.744-10.631,44.127v9.093
c0,19.043,3.5,33.817,10.503,44.319c7,10.504,17.079,15.755,30.229,15.755c12.893,0,22.799-5.059,29.715-15.178
c6.917-10.119,10.418-24.868,10.504-44.255L727.873,749.701L727.873,749.701z"/>
<path d="M794.736,847.177V660.678h65.326c22.627,0,39.791,4.336,51.491,13.001c11.699,8.668,17.549,21.372,17.549,38.107
c0,9.138-2.35,17.187-7.045,24.146c-4.697,6.961-11.23,12.062-19.597,15.307c9.562,2.39,17.1,7.216,22.607,14.474
c5.508,7.259,8.263,16.138,8.263,26.642c0,17.934-5.723,31.51-17.165,40.733c-11.446,9.223-27.754,13.919-48.93,14.089
L794.736,847.177L794.736,847.177z M833.162,738.813h28.437c19.383-0.341,29.076-8.069,29.076-23.186
c0-8.453-2.455-14.538-7.364-18.252c-4.913-3.714-12.662-5.572-23.248-5.572h-26.9V738.813L833.162,738.813z M833.162,765.968
v50.341h32.919c9.051,0,16.118-2.155,21.198-6.469c5.08-4.313,7.621-10.269,7.621-17.868c0-17.078-8.838-25.746-26.514-26.003
L833.162,765.968L833.162,765.968z"/>
<path d="M1115.343,758.155c0,18.361-3.245,34.457-9.734,48.289c-6.492,13.834-15.776,24.51-27.858,32.022
c-12.085,7.516-25.94,11.273-41.567,11.273c-15.457,0-29.246-3.715-41.37-11.145c-12.127-7.429-21.521-18.039-28.182-31.831
c-6.66-13.789-10.035-29.653-10.119-47.584v-9.223c0-18.358,3.309-34.521,9.928-48.481c6.616-13.962,15.966-24.678,28.051-32.15
c12.081-7.471,25.894-11.207,41.436-11.207s29.354,3.735,41.439,11.207c12.079,7.473,21.432,18.188,28.05,32.15
c6.616,13.961,9.926,30.081,9.926,48.354v8.325H1115.343z M1076.405,749.701c0-19.553-3.504-34.411-10.505-44.574
s-16.992-15.243-29.973-15.243c-12.895,0-22.844,5.017-29.845,15.05c-7.004,10.036-10.548,24.744-10.632,44.127v9.093
c0,19.043,3.501,33.817,10.503,44.319c7.002,10.504,17.079,15.755,30.229,15.755c12.896,0,22.802-5.059,29.717-15.178
c6.918-10.119,10.419-24.868,10.505-44.255L1076.405,749.701L1076.405,749.701z"/>
<path d="M1239.975,798.248c0-7.258-2.563-12.829-7.686-16.717c-5.123-3.884-14.346-7.982-27.666-12.296
c-13.323-4.312-23.869-8.561-31.64-12.744c-21.178-11.443-31.766-26.855-31.766-46.241c0-10.075,2.838-19.064,8.517-26.963
c5.679-7.897,13.835-14.067,24.466-18.508c10.631-4.439,22.563-6.66,35.801-6.66c13.323,0,25.191,2.412,35.609,7.236
c10.417,4.826,18.509,11.636,24.272,20.43c5.765,8.796,8.647,18.787,8.647,29.973h-38.426c0-8.536-2.692-15.177-8.071-19.917
s-12.938-7.11-22.672-7.11c-9.395,0-16.695,1.988-21.903,5.957c-5.209,3.972-7.814,9.202-7.814,15.693
c0,6.063,3.053,11.143,9.16,15.241c6.104,4.099,15.091,7.941,26.962,11.528c21.859,6.577,37.786,14.73,47.776,24.465
c9.991,9.735,14.987,21.861,14.987,36.377c0,16.139-6.106,28.8-18.317,37.979c-12.212,9.181-28.649,13.771-49.313,13.771
c-14.347,0-27.411-2.626-39.195-7.878s-20.773-12.444-26.964-21.583c-6.192-9.136-9.286-19.725-9.286-31.767h38.556
c0,20.581,12.296,30.87,36.891,30.87c9.136,0,16.269-1.858,21.391-5.573C1237.414,810.097,1239.975,804.907,1239.975,798.248z"/>
<path d="M1409.822,808.75h-67.376l-12.809,38.427h-40.861l69.424-186.498h35.611l69.809,186.498h-40.861L1409.822,808.75z
M1352.822,777.625h46.624l-23.44-69.809L1352.822,777.625z"/>
<path d="M1606.055,691.805h-57.127v155.372h-38.429V691.805h-56.358v-31.126h151.914V691.805L1606.055,691.805z"/>
<path d="M1722.617,798.248c0-7.258-2.563-12.829-7.687-16.717c-5.123-3.884-14.346-7.982-27.666-12.296
c-13.323-4.312-23.87-8.561-31.639-12.744c-21.179-11.443-31.767-26.855-31.767-46.241c0-10.075,2.837-19.064,8.517-26.963
c5.679-7.897,13.835-14.067,24.466-18.508c10.632-4.439,22.563-6.66,35.801-6.66c13.323,0,25.19,2.412,35.609,7.236
c10.417,4.826,18.509,11.636,24.273,20.43c5.764,8.796,8.646,18.787,8.646,29.973h-38.426c0-8.536-2.692-15.177-8.072-19.917
c-5.379-4.74-12.936-7.11-22.671-7.11c-9.395,0-16.695,1.988-21.904,5.957c-5.208,3.972-7.813,9.202-7.813,15.693
c0,6.063,3.053,11.143,9.16,15.241c6.104,4.099,15.091,7.941,26.962,11.528c21.858,6.577,37.786,14.73,47.776,24.465
c9.991,9.735,14.987,21.861,14.987,36.377c0,16.139-6.106,28.8-18.317,37.979c-12.212,9.181-28.648,13.771-49.313,13.771
c-14.346,0-27.411-2.626-39.195-7.878s-20.774-12.444-26.964-21.583c-6.192-9.136-9.286-19.725-9.286-31.767h38.556
c0,20.581,12.296,30.87,36.891,30.87c9.136,0,16.268-1.858,21.391-5.573C1720.055,810.097,1722.617,804.907,1722.617,798.248z"/>
</g>
</SvgIcon>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -3,6 +3,7 @@ body {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: 'Roboto';
} }
#main { #main {
@ -18,6 +19,11 @@ body {
height: 100%; height: 100%;
} }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.appCenter { .appCenter {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -25,6 +31,33 @@ body {
transform: translate(-50%,-50%) translate(0,-20px); transform: translate(-50%,-50%) translate(0,-20px);
} }
.alertUnsafe{
position: absolute;
width: 100%;
z-index: 9999;
}
.hideAlertButton{
position: fixed;
}
.clickTrough{
height: 50px;
pointer-events: none;
z-index: 1;
}
/* No arrows on numeric inputs */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
.bottomBar { .bottomBar {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@ -32,6 +65,18 @@ body {
height: 40px; height: 40px;
} }
.amboss{
fill:url(#SVGID_1_);
}
.advancedSwitch{
width: 20;
left: 50%;
transform: translate(62px, 0px);
margin-right: 0;
margin-left: auto;
}
.bottomItem { .bottomItem {
margin: 0; margin: 0;
top: -14px; top: -14px;

View File

@ -0,0 +1,330 @@
.loaderCenter{
margin:0 auto;
position: absolute;
left:50%;
top:50%;
margin-top:-120px;
margin-left:-175px;
width:350px;
height:120px;
text-align: center;
}
.loaderSpinner,
.loaderSpinner:before,
.loaderSpinner:after {
border-radius: 50%;
}
.loaderSpinner {
color: #1976d2;
font-size: 11px;
text-indent: -99999em;
margin: 55px auto;
position: relative;
width: 10em;
height: 10em;
box-shadow: inset 0 0 0 1em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.loaderSpinner:before,
.loaderSpinner:after {
position: absolute;
content: '';
}
.loaderSpinner:before {
width: 5.2em;
height: 10.2em;
background: #ffffff;
border-radius: 9.2em 0 0 10.2em;
top: -0.1em;
left: -0.1em;
-webkit-transform-origin: 5.1em 5.1em;
transform-origin: 5.1em 5.1em;
-webkit-animation: load2 2s infinite ease 1.5s;
animation: load2 2s infinite ease 1.5s;
}
.loaderSpinner:after {
width: 5.2em;
height: 10.2em;
background: #ffffff;
border-radius: 0 10.2em 10.2em 0;
top: -0.1em;
left: 4.9em;
-webkit-transform-origin: 0.1em 5.1em;
transform-origin: 0.1em 5.1em;
-webkit-animation: load2 2s infinite ease;
animation: load2 2s infinite ease;
}
@-webkit-keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.content-slider {
width: 100%;
height: 180px;
}
.slider {
height: 140px;
width: 350px;
margin: 40px auto 0;
overflow: visible;
position: relative;
}
.mask {
overflow: hidden;
height: 140px;
}
.slider ul {
margin: 0;
padding: 0;
position: relative;
}
.slider li {
text-align: center;
width: 350px;
height: 140px;
position: absolute;
top: -105px;
list-style: none;
}
.slider .quote {
text-align: center;
font-size: 20px;
}
.slider li.anim1 {
animation: cycle 12s linear infinite;
}
.slider li.anim2 {
animation: cycle2 12s linear infinite;
}
.slider li.anim3 {
animation: cycle3 12s linear infinite;
}
.slider li.anim4 {
animation: cycle4 12s linear infinite;
}
.slider li.anim5 {
animation: cycle5 12s linear infinite;
}
.slider:hover li {
animation-play-state: paused;
}
@keyframes cycle {
0% {
top: 0px;
}
4% {
top: 0px;
}
16% {
top: 0px;
opacity: 1;
z-index: 0;
}
20% {
top: 105px;
opacity: 0;
z-index: 0;
}
21% {
top: -105px;
opacity: 0;
z-index: -1;
}
50% {
top: -105px;
opacity: 0;
z-index: -1;
}
92% {
top: -105px;
opacity: 0;
z-index: 0;
}
96% {
top: -105px;
opacity: 0;
}
100% {
top: 0px;
opacity: 1;
}
}
@keyframes cycle2 {
0% {
top: -105px;
opacity: 0;
}
16% {
top: -105px;
opacity: 0;
}
20% {
top: 0px;
opacity: 1;
}
24% {
top: 0px;
opacity: 1;
}
36% {
top: 0px;
opacity: 1;
z-index: 0;
}
40% {
top: 105px;
opacity: 0;
z-index: 0;
}
41% {
top: -105px;
opacity: 0;
z-index: -1;
}
100% {
top: -105px;
opacity: 0;
z-index: -1;
}
}
@keyframes cycle3 {
0% {
top: -105px;
opacity: 0;
}
36% {
top: -105px;
opacity: 0;
}
40% {
top: 0px;
opacity: 1;
}
44% {
top: 0px;
opacity: 1;
}
56% {
top: 0px;
opacity: 1;
z-index: 0;
}
60% {
top: 105px;
opacity: 0;
z-index: 0;
}
61% {
top: -105px;
opacity: 0;
z-index: -1;
}
100% {
top: -105px;
opacity: 0;
z-index: -1;
}
}
@keyframes cycle4 {
0% {
top: -105px;
opacity: 0;
}
56% {
top: -105px;
opacity: 0;
}
60% {
top: 0px;
opacity: 1;
}
64% {
top: 0px;
opacity: 1;
}
76% {
top: 0px;
opacity: 1;
z-index: 0;
}
80% {
top: 105px;
opacity: 0;
z-index: 0;
}
81% {
top: -105px;
opacity: 0;
z-index: -1;
}
100% {
top: -105px;
opacity: 0;
z-index: -1;
}
}
@keyframes cycle5 {
0% {
top: -105px;
opacity: 0;
}
76% {
top: -105px;
opacity: 0;
}
80% {
top: 0px;
opacity: 1;
}
84% {
top: 0px;
opacity: 1;
}
96% {
top: 0px;
opacity: 1;
z-index: 0;
}
100% {
top: 105px;
opacity: 0;
z-index: 0;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
<title>RoboSats - Simple and Private Bitcoin Exchange</title> <title>RoboSats - Simple and Private Bitcoin Exchange</title>
{% load static %} {% load static %}
<link rel="stylesheet" href="{% static "css/fonts.css" %}"/> <link rel="stylesheet" href="{% static "css/fonts.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "css/loader.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "css/index.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "css/index.css" %}"/>
</head> </head>
<body> <body>
@ -23,7 +24,33 @@
</div> </div>
</noscript> </noscript>
<div id="main"> <div id="main">
<div id="app"></div> <div id="app">
<div class="loaderCenter">
<div class="loaderSpinner"></div>
<div class="content-slider">
<div class="slider">
<div class="mask">
<ul>
<li class="anim1">
<div class="quote">Looking for robot parts ...</div>
</li>
<li class="anim2">
<div class="quote">Adding layers to the onion ...</div>
</li>
<li class="anim3">
<div class="quote">Winning at game theory ...</div>
</li>
<li class="anim4">
<div class="quote">Moving Sats at light speed ...</div>
</li>
<li class="anim5">
<div class="quote">Hiding in 2^256 bits of entropy...</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div> </div>
<script src="{% static "frontend/main.js" %}"></script> <script src="{% static "frontend/main.js" %}"></script>

View File

@ -21,6 +21,7 @@ module.exports = {
optimization: { optimization: {
minimize: true, minimize: true,
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env": { "process.env": {
@ -28,6 +29,7 @@ module.exports = {
NODE_ENV: JSON.stringify("production"), NODE_ENV: JSON.stringify("production"),
}, },
}), }),
//new webpack.optimize.AggressiveMergingPlugin() //Merge chunks
], ],
resolve: { resolve: {
extensions: ['.ts', '.js'], extensions: ['.ts', '.js'],

View File

@ -23,4 +23,5 @@ scipy==1.8.0
gunicorn==20.1.0 gunicorn==20.1.0
psycopg2==2.9.3 psycopg2==2.9.3
SQLAlchemy==1.4.31 SQLAlchemy==1.4.31
django-import-export==2.7.1
requests[socks] requests[socks]

View File

@ -39,7 +39,11 @@ app.conf.beat_schedule = {
"task": "give_rewards", "task": "give_rewards",
"schedule": crontab(hour=0, minute=0), "schedule": crontab(hour=0, minute=0),
}, },
"cache-market-prices": { # Cache market prices every minutes for now. "do-accounting": { # Does accounting for the last day
"task": "do_accounting",
"schedule": crontab(hour=23, minute=55),
},
"cache-market-prices": { # Cache market prices every minute
"task": "cache_external_market_prices", "task": "cache_external_market_prices",
"schedule": timedelta(seconds=60), "schedule": timedelta(seconds=60),
}, },

View File

@ -56,8 +56,10 @@ INSTALLED_APPS = [
"channels", "channels",
"django_celery_beat", "django_celery_beat",
"django_celery_results", "django_celery_results",
"import_export",
"api", "api",
"chat", "chat",
"control",
"frontend.apps.FrontendConfig", "frontend.apps.FrontendConfig",
] ]
from .celery.conf import * from .celery.conf import *
@ -73,6 +75,7 @@ MIDDLEWARE = [
] ]
ROOT_URLCONF = "robosats.urls" ROOT_URLCONF = "robosats.urls"
IMPORT_EXPORT_USE_TRANSACTIONS = True
TEMPLATES = [ TEMPLATES = [
{ {