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

3
.gitignore vendored
View File

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

View File

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

View File

@ -14,7 +14,6 @@ import time
FEE = float(config("FEE"))
MAKER_FEE_SPLIT = float(config("MAKER_FEE_SPLIT"))
BOND_SIZE = float(config("BOND_SIZE"))
ESCROW_USERNAME = config("ESCROW_USERNAME")
PENALTY_TIMEOUT = int(config("PENALTY_TIMEOUT"))
@ -27,7 +26,6 @@ EXP_TAKER_BOND_INVOICE = int(config("EXP_TAKER_BOND_INVOICE"))
BOND_EXPIRY = int(config("BOND_EXPIRY"))
ESCROW_EXPIRY = int(config("ESCROW_EXPIRY"))
PUBLIC_ORDER_DURATION = int(config("PUBLIC_ORDER_DURATION"))
INVOICE_AND_ESCROW_DURATION = int(config("INVOICE_AND_ESCROW_DURATION"))
FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION"))
@ -89,24 +87,65 @@ class Logics:
return True, None, None
def validate_order_size(order):
"""Validates if order is withing limits in satoshis at t0"""
if order.t0_satoshis > MAX_TRADE:
@classmethod
def validate_order_size(cls, order):
"""Validates if order size in Sats is within limits at t0"""
if not order.has_range:
if order.t0_satoshis > MAX_TRADE:
return False, {
"bad_request":
"Your order is too big. It is worth " +
"{:,}".format(order.t0_satoshis) +
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
" Sats"
}
if order.t0_satoshis < MIN_TRADE:
return False, {
"bad_request":
"Your order is too small. It is worth " +
"{:,}".format(order.t0_satoshis) +
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
" Sats"
}
elif order.has_range:
min_sats = cls.calc_sats(order.min_amount, order.currency.exchange_rate, order.premium)
max_sats = cls.calc_sats(order.max_amount, order.currency.exchange_rate, order.premium)
if min_sats > max_sats/1.5:
return False, {
"bad_request":
"Maximum range amount must be at least 50 percent higher than the minimum amount"
}
elif max_sats > MAX_TRADE:
return False, {
"bad_request":
"Your order maximum amount is too big. It is worth " +
"{:,}".format(int(max_sats)) +
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
" Sats"
}
elif min_sats < MIN_TRADE:
return False, {
"bad_request":
"Your order minimum amount is too small. It is worth " +
"{:,}".format(int(min_sats)) +
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
" Sats"
}
elif min_sats < max_sats/5:
return False, {
"bad_request":
f"Your order amount range is too large. Max amount can only be 5 times bigger than min amount"
}
return True, None
def validate_amount_within_range(order, amount):
if amount > float(order.max_amount) or amount < float(order.min_amount):
return False, {
"bad_request":
"Your order is too big. It is worth " +
"{:,}".format(order.t0_satoshis) +
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
" Sats"
}
if order.t0_satoshis < MIN_TRADE:
return False, {
"bad_request":
"Your order is too small. It is worth " +
"{:,}".format(order.t0_satoshis) +
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
" Sats"
"The amount specified is outside the range specified by the maker"
}
return True, None
def user_activity_status(last_seen):
@ -118,7 +157,7 @@ class Logics:
return "Inactive"
@classmethod
def take(cls, order, user):
def take(cls, order, user, amount=None):
is_penalized, time_out = cls.is_penalized(user)
if is_penalized:
return False, {
@ -126,10 +165,12 @@ class Logics:
f"You need to wait {time_out} seconds to take an order",
}
else:
if order.has_range:
order.amount= amount
order.taker = user
order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.TAK])
seconds=order.t_to_expire(Order.Status.TAK))
order.save()
# send_message.delay(order.id,'order_taken') # Too spammy
return True, None
@ -146,15 +187,19 @@ class Logics:
return (is_maker and order.type == Order.Types.SELL) or (
is_taker and order.type == Order.Types.BUY)
def satoshis_now(order):
def calc_sats(amount, exchange_rate, premium):
exchange_rate = float(exchange_rate)
premium_rate = exchange_rate * (1 + float(premium) / 100)
return (float(amount) /premium_rate) * 100 * 1000 * 1000
@classmethod
def satoshis_now(cls, order):
"""checks trade amount in sats"""
if order.is_explicit:
satoshis_now = order.satoshis
else:
exchange_rate = float(order.currency.exchange_rate)
premium_rate = exchange_rate * (1 + float(order.premium) / 100)
satoshis_now = (float(order.amount) /
premium_rate) * 100 * 1000 * 1000
amount = order.amount if order.amount != None else order.max_amount
satoshis_now = cls.calc_sats(amount, order.currency.exchange_rate, order.premium)
return int(satoshis_now)
@ -165,8 +210,8 @@ class Logics:
premium = order.premium
price = exchange_rate * (1 + float(premium) / 100)
else:
order_rate = float(
order.amount) / (float(order.satoshis) / 100000000)
amount = order.amount if not order.has_range else order.max_amount
order_rate = float(amount) / (float(order.satoshis) / 100000000)
premium = order_rate / exchange_rate - 1
premium = int(premium * 10000) / 100 # 2 decimals left
price = order_rate
@ -336,7 +381,7 @@ class Logics:
order.is_disputed = True
order.status = Order.Status.DIS
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.DIS])
seconds=order.t_to_expire(Order.Status.DIS))
order.save()
# User could be None if a dispute is open automatically due to weird expiration.
@ -380,7 +425,7 @@ class Logics:
if order.maker_statement not in [None,""] and order.taker_statement not in [None,""]:
order.status = Order.Status.WFR
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.WFR])
seconds=order.t_to_expire(Order.Status.WFR))
order.save()
return True, None
@ -442,6 +487,12 @@ class Logics:
"bad_request":
"You cannot submit a invoice while bonds are not locked."
}
if order.status == Order.Status.FAI:
if order.payout.status != LNPayment.Status.EXPIRE:
return False, {
"bad_request":
"You cannot submit an invoice only after expiration or 3 failed attempts"
}
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
@ -472,7 +523,7 @@ class Logics:
if order.status == Order.Status.WFI:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.CHA])
seconds=order.t_to_expire(Order.Status.CHA))
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
@ -483,7 +534,7 @@ class Logics:
elif order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.CHA])
seconds=order.t_to_expire(Order.Status.CHA))
else:
order.status = Order.Status.WFE
@ -661,7 +712,9 @@ class Logics:
def publish_order(order):
order.status = Order.Status.PUB
order.expires_at = order.created_at + timedelta(
seconds=Order.t_to_expire[Order.Status.PUB])
seconds=order.t_to_expire(Order.Status.PUB))
if order.has_range:
order.amount = None
order.save()
# send_message.delay(order.id,'order_published') # too spammy
return
@ -699,7 +752,7 @@ class Logics:
# If there was no maker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
bond_satoshis = int(order.last_satoshis * order.bond_size/100)
description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."
@ -708,7 +761,7 @@ class Logics:
hold_payment = LNNode.gen_hold_invoice(
bond_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.WFB],
invoice_expiry=order.t_to_expire(Order.Status.WFB),
cltv_expiry_secs=BOND_EXPIRY * 3600,
)
except Exception as e:
@ -759,7 +812,7 @@ class Logics:
# With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.WF2])
seconds=order.t_to_expire(Order.Status.WF2))
order.status = Order.Status.WF2
order.save()
@ -809,7 +862,7 @@ class Logics:
# If there was no taker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
bond_satoshis = int(order.last_satoshis * order.bond_size/100)
pos_text = "Buying" if cls.is_buyer(order, user) else "Selling"
description = (
f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
@ -822,7 +875,7 @@ class Logics:
hold_payment = LNNode.gen_hold_invoice(
bond_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.TAK],
invoice_expiry=order.t_to_expire(Order.Status.TAK),
cltv_expiry_secs=BOND_EXPIRY * 3600,
)
@ -850,7 +903,7 @@ class Logics:
)
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.TAK])
seconds=order.t_to_expire(Order.Status.TAK))
order.save()
return True, {
"bond_invoice": hold_payment["invoice"],
@ -866,7 +919,7 @@ class Logics:
elif order.status == Order.Status.WFE:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.CHA])
seconds=order.t_to_expire(Order.Status.CHA))
order.save()
@classmethod
@ -909,7 +962,7 @@ class Logics:
hold_payment = LNNode.gen_hold_invoice(
escrow_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.WF2],
invoice_expiry=order.t_to_expire(Order.Status.WF2),
cltv_expiry_secs=ESCROW_EXPIRY * 3600,
)

View File

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

View File

@ -5,6 +5,7 @@ from django.core.validators import (
MinValueValidator,
validate_comma_separated_integer_list,
)
from django.utils import timezone
from django.db.models.signals import post_save, pre_delete
from django.template.defaultfilters import truncatechars
from django.dispatch import receiver
@ -19,7 +20,7 @@ import json
MIN_TRADE = int(config("MIN_TRADE"))
MAX_TRADE = int(config("MAX_TRADE"))
FEE = float(config("FEE"))
BOND_SIZE = float(config("BOND_SIZE"))
DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
class Currency(models.Model):
@ -38,7 +39,7 @@ class Currency(models.Model):
null=True,
validators=[MinValueValidator(0)],
)
timestamp = models.DateTimeField(auto_now_add=True)
timestamp = models.DateTimeField(default=timezone.now)
def __str__(self):
# returns currency label ( 3 letters code)
@ -105,9 +106,11 @@ class LNPayment(models.Model):
default=None,
blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[
MinValueValidator(MIN_TRADE * BOND_SIZE),
MaxValueValidator(MAX_TRADE * (1 + BOND_SIZE + FEE)),
MinValueValidator(100),
MaxValueValidator(MAX_TRADE * (1 + DEFAULT_BOND_SIZE + FEE)),
])
# Fee in sats with mSats decimals fee_msat
fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False)
created_at = models.DateTimeField()
expires_at = models.DateTimeField()
cltv_expiry = models.PositiveSmallIntegerField(null=True,
@ -181,7 +184,7 @@ class Order(models.Model):
status = models.PositiveSmallIntegerField(choices=Status.choices,
null=False,
default=Status.WFB)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(default=timezone.now)
expires_at = models.DateTimeField()
# order details
@ -189,14 +192,15 @@ class Order(models.Model):
currency = models.ForeignKey(Currency,
null=True,
on_delete=models.SET_NULL)
amount = models.DecimalField(max_digits=16,
decimal_places=8,
validators=[MinValueValidator(0.00000001)])
amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
has_range = models.BooleanField(default=False, null=False, blank=False)
min_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
max_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
payment_method = models.CharField(max_length=35,
null=False,
default="not specified",
blank=True)
bondless_taker = models.BooleanField(default=False, null=False, blank=False)
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
is_explicit = models.BooleanField(default=False, null=False)
# marked to market
@ -218,6 +222,29 @@ class Order(models.Model):
],
blank=True,
)
# optionally makers can choose the public order duration length (seconds)
public_duration = models.PositiveBigIntegerField(
default=60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1,
null=False,
validators=[
MinValueValidator(60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))), # Min is 10 minutes
MaxValueValidator(60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))), # Max is 24 Hours
],
blank=False,
)
# optionally makers can choose the fidelity bond size of the maker and taker (%)
bond_size = models.DecimalField(
max_digits=4,
decimal_places=2,
default=DEFAULT_BOND_SIZE,
null=False,
validators=[
MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 %
MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 %
],
blank=False,
)
# how many sats at creation and at last check (relevant for marked to market)
t0_satoshis = models.PositiveBigIntegerField(
null=True,
@ -311,30 +338,38 @@ class Order(models.Model):
maker_platform_rated = models.BooleanField(default=False, null=False)
taker_platform_rated = models.BooleanField(default=False, null=False)
t_to_expire = {
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
1: 60 * 60 * int(config("PUBLIC_ORDER_DURATION")), # 'Public'
2: 0, # 'Deleted'
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
4: 0, # 'Cancelled'
5: 0, # 'Expired'
6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting for trade collateral and buyer invoice'
7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for seller trade collateral'
8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for buyer invoice'
9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom'
11: 1 * 24 * 60 * 60, # 'In dispute'
12: 0, # 'Collaboratively cancelled'
13: 24 * 60 * 60, # 'Sending satoshis to buyer'
14: 24 * 60 * 60, # 'Sucessful trade'
15: 24 * 60 * 60, # 'Failed lightning network routing'
16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
17: 24 * 60 * 60, # 'Maker lost dispute'
18: 24 * 60 * 60, # 'Taker lost dispute'
}
def __str__(self):
return f"Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}"
if self.has_range and self.amount == None:
amt = str(float(self.min_amount))+"-"+ str(float(self.max_amount))
else:
amt = float(self.amount)
return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
def t_to_expire(self, status):
t_to_expire = {
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
1: self.public_duration, # 'Public'
2: 0, # 'Deleted'
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
4: 0, # 'Cancelled'
5: 0, # 'Expired'
6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting for trade collateral and buyer invoice'
7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for seller trade collateral'
8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for buyer invoice'
9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom'
11: 1 * 24 * 60 * 60, # 'In dispute'
12: 0, # 'Collaboratively cancelled'
13: 24 * 60 * 60, # 'Sending satoshis to buyer'
14: 24 * 60 * 60, # 'Sucessful trade'
15: 24 * 60 * 60, # 'Failed lightning network routing'
16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
17: 24 * 60 * 60, # 'Maker lost dispute'
18: 24 * 60 * 60, # 'Taker lost dispute'
}
return t_to_expire[status]
@receiver(pre_delete, sender=Order)
@ -393,7 +428,7 @@ class Profile(models.Model):
null=False
)
telegram_lang_code = models.CharField(
max_length=4,
max_length=10,
null=True,
blank=True
)
@ -529,7 +564,7 @@ class MarketTick(models.Model):
currency = models.ForeignKey(Currency,
null=True,
on_delete=models.SET_NULL)
timestamp = models.DateTimeField(auto_now_add=True)
timestamp = models.DateTimeField(default=timezone.now)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
fee = models.DecimalField(

View File

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

View File

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

View File

@ -1,5 +1,5 @@
from django.urls import path
from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView
from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView, LimitView
urlpatterns = [
path("make/", MakerView.as_view()),
@ -13,5 +13,6 @@ urlpatterns = [
# path('robot/') # Profile Info
path("info/", InfoView.as_view()),
path("price/", PriceView.as_view()),
path("limits/", LimitView.as_view()),
path("reward/", RewardView.as_view()),
]

View File

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

View File

@ -30,6 +30,8 @@ from decouple import config
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
RETRY_TIME = int(config("RETRY_TIME"))
PUBLIC_DURATION = 60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1
BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
avatar_path = Path(settings.AVATAR_ROOT)
avatar_path.mkdir(parents=True, exist_ok=True)
@ -64,35 +66,76 @@ class MakerView(CreateAPIView):
},
status.HTTP_400_BAD_REQUEST,
)
# Only allow users who are not already engaged in an order
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
if not valid:
return Response(context, status.HTTP_409_CONFLICT)
type = serializer.data.get("type")
currency = serializer.data.get("currency")
amount = serializer.data.get("amount")
has_range = serializer.data.get("has_range")
min_amount = serializer.data.get("min_amount")
max_amount = serializer.data.get("max_amount")
payment_method = serializer.data.get("payment_method")
premium = serializer.data.get("premium")
satoshis = serializer.data.get("satoshis")
is_explicit = serializer.data.get("is_explicit")
public_duration = serializer.data.get("public_duration")
bond_size = serializer.data.get("bond_size")
bondless_taker = serializer.data.get("bondless_taker")
valid, context, _ = Logics.validate_already_maker_or_taker(
request.user)
if not valid:
return Response(context, status.HTTP_409_CONFLICT)
# Optional params
if public_duration == None: public_duration = PUBLIC_DURATION
if bond_size == None: bond_size = BOND_SIZE
if bondless_taker == None: bondless_taker = False
if has_range == None: has_range = False
# An order can either have an amount or a range (min_amount and max_amount)
if has_range:
amount = None
else:
min_amount = None
max_amount = None
# Either amount or min_max has to be specified.
if has_range and (min_amount == None or max_amount == None):
return Response(
{
"bad_request":
"You must specify min_amount and max_amount for a range order"
},
status.HTTP_400_BAD_REQUEST,
)
elif not has_range and amount == None:
return Response(
{
"bad_request":
"You must specify an order amount"
},
status.HTTP_400_BAD_REQUEST,
)
# Creates a new order
order = Order(
type=type,
currency=Currency.objects.get(id=currency),
amount=amount,
has_range=has_range,
min_amount=min_amount,
max_amount=max_amount,
payment_method=payment_method,
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at=timezone.now() + timedelta(
seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
seconds=EXP_MAKER_BOND_INVOICE),
maker=request.user,
public_duration=public_duration,
bond_size=bond_size,
bondless_taker=bondless_taker,
)
# TODO move to Order class method when new instance is created!
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
valid, context = Logics.validate_order_size(order)
@ -155,7 +198,7 @@ class OrderView(viewsets.ViewSet):
)
data = ListOrderSerializer(order).data
data["total_secs_exp"] = Order.t_to_expire[order.status]
data["total_secs_exp"] = order.t_to_expire(order.status)
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
@ -347,6 +390,10 @@ class OrderView(viewsets.ViewSet):
"""
order_id = request.GET.get(self.lookup_url_kwarg)
import sys
sys.stdout.write('AAAAAA')
print('BBBBB1')
serializer = UpdateOrderSerializer(data=request.data)
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
@ -367,7 +414,17 @@ class OrderView(viewsets.ViewSet):
request.user)
if not valid:
return Response(context, status=status.HTTP_409_CONFLICT)
valid, context = Logics.take(order, request.user)
# For order with amount range, set the amount now.
if order.has_range:
amount = float(serializer.data.get("amount"))
valid, context = Logics.validate_amount_within_range(order, amount)
if not valid:
return Response(context, status=status.HTTP_400_BAD_REQUEST)
valid, context = Logics.take(order, request.user, amount)
else:
valid, context = Logics.take(order, request.user)
if not valid:
return Response(context, status=status.HTTP_403_FORBIDDEN)
@ -684,11 +741,11 @@ class InfoView(ListAPIView):
context["network"] = config("NETWORK")
context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT"))
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
context["bond_size"] = float(config("BOND_SIZE"))
context["bond_size"] = float(config("DEFAULT_BOND_SIZE"))
if request.user.is_authenticated:
context["nickname"] = request.user.username
context["referral_link"] = str(config('HOST_NAME'))+'/ref/'+str(request.user.profile.referral_code)
context["referral_code"] = str(request.user.profile.referral_code)
context["earned_rewards"] = request.user.profile.earned_rewards
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user)
@ -748,4 +805,28 @@ class PriceView(CreateAPIView):
except:
payload[code] = None
return Response(payload, status.HTTP_200_OK)
class LimitView(ListAPIView):
def get(self, request):
# Trade limits as BTC
min_trade = float(config('MIN_TRADE')) / 100000000
max_trade = float(config('MAX_TRADE')) / 100000000
max_bondless_trade = float(config('MAX_TRADE_BONDLESS_TAKER')) / 100000000
payload = {}
queryset = Currency.objects.all().order_by('currency')
for currency in queryset:
code = Currency.currency_dict[str(currency.currency)]
exchange_rate = float(currency.exchange_rate)
payload[currency.currency] = {
'code': code,
'min_amount': min_trade * exchange_rate,
'max_amount': max_trade * exchange_rate,
'max_bondless_amount': max_bondless_trade * exchange_rate,
}
return Response(payload, status.HTTP_200_OK)

View File

@ -18,4 +18,4 @@ class ChatRoomAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"room_group_name",
)
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
container_name: npm-dev
restart: always
command: npm run build
volumes:
- ./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
COPY entrypoint.sh /root/entrypoint.sh
COPY lnd.conf /tmp/lnd.conf
ENTRYPOINT [ "/root/entrypoint.sh" ]

View File

@ -1286,6 +1286,43 @@
"minimist": "^1.2.0"
}
},
"@date-io/core": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz",
"integrity": "sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg=="
},
"@date-io/date-fns": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.13.1.tgz",
"integrity": "sha512-8fmfwjiLMpFLD+t4NBwDx0eblWnNcgt4NgfT/uiiQTGI81fnPu9tpBMYdAcuWxaV7LLpXgzLBx1SYWAMDVUDQQ==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@date-io/dayjs": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.13.1.tgz",
"integrity": "sha512-5bL4WWWmlI4uGZVScANhHJV7Mjp93ec2gNeUHDqqLaMZhp51S0NgD25oqj/k0LqBn1cdU2MvzNpk/ObMmVv5cQ==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@date-io/luxon": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz",
"integrity": "sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@date-io/moment": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.13.1.tgz",
"integrity": "sha512-XX1X/Tlvl3TdqQy2j0ZUtEJV6Rl8tOyc5WOS3ki52He28Uzme4Ro/JuPWTMBDH63weSWIZDlbR7zBgp3ZA2y1A==",
"requires": {
"@date-io/core": "^2.13.1"
}
},
"@discoveryjs/json-ext": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz",
@ -1589,6 +1626,120 @@
"@babel/runtime": "^7.16.3"
}
},
"@mui/lab": {
"version": "5.0.0-alpha.73",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.73.tgz",
"integrity": "sha512-10Uj0Atc7gBTXKX4VV38P6RdqTQrJZxcl3HeEcytIO1S3NAGfc7gZ3Hdpnhtj5U8kcRJZZPH9LtrBbMZzxU/1A==",
"requires": {
"@babel/runtime": "^7.17.2",
"@date-io/date-fns": "^2.13.1",
"@date-io/dayjs": "^2.13.1",
"@date-io/luxon": "^2.13.1",
"@date-io/moment": "^2.13.1",
"@mui/base": "5.0.0-alpha.72",
"@mui/system": "^5.5.1",
"@mui/utils": "^5.4.4",
"clsx": "^1.1.1",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"react-transition-group": "^4.4.2",
"rifm": "^0.12.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.7.tgz",
"integrity": "sha512-L6rvG9GDxaLgFjg41K+5Yv9OMrU98sWe+Ykmc6FDJW/+vYZMhdOMKkISgzptMaERHvS2Y2lw9MDRm2gHhlQQoA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@emotion/is-prop-valid": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz",
"integrity": "sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==",
"requires": {
"@emotion/memoize": "^0.7.4"
}
},
"@mui/base": {
"version": "5.0.0-alpha.72",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.72.tgz",
"integrity": "sha512-WCAooa9eqbsC68LhyKtDBRumH4hV1eRZ0A3SDKFHSwYG9fCOdsFv/H1dIYRJM0rwD45bMnuDiG3Qmx7YsTiptw==",
"requires": {
"@babel/runtime": "^7.17.2",
"@emotion/is-prop-valid": "^1.1.2",
"@mui/utils": "^5.4.4",
"@popperjs/core": "^2.11.3",
"clsx": "^1.1.1",
"prop-types": "^15.7.2",
"react-is": "^17.0.2"
}
},
"@mui/private-theming": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.4.4.tgz",
"integrity": "sha512-V/gxttr6736yJoU9q+4xxXsa0K/w9Hn9pg99zsOHt7i/O904w2CX5NHh5WqDXtoUzVcayLF0RB17yr6l79CE+A==",
"requires": {
"@babel/runtime": "^7.17.2",
"@mui/utils": "^5.4.4",
"prop-types": "^15.7.2"
}
},
"@mui/styled-engine": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.4.4.tgz",
"integrity": "sha512-AKx3rSgB6dmt5f7iP4K18mLFlE5/9EfJe/5EH9Pyqez8J/CPkTgYhJ/Va6qtlrcunzpui+uG/vfuf04yAZekSg==",
"requires": {
"@babel/runtime": "^7.17.2",
"@emotion/cache": "^11.7.1",
"prop-types": "^15.7.2"
}
},
"@mui/system": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.5.1.tgz",
"integrity": "sha512-2hynI4hN8304hOCT8sc4knJviwUUYJ7XK3mXwQ0nagVGOPnWSOad/nYADm7K0vdlCeUXLIbDbe7oNN3Kaiu2kA==",
"requires": {
"@babel/runtime": "^7.17.2",
"@mui/private-theming": "^5.4.4",
"@mui/styled-engine": "^5.4.4",
"@mui/types": "^7.1.3",
"@mui/utils": "^5.4.4",
"clsx": "^1.1.1",
"csstype": "^3.0.11",
"prop-types": "^15.7.2"
}
},
"@mui/types": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.3.tgz",
"integrity": "sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA=="
},
"@mui/utils": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.4.4.tgz",
"integrity": "sha512-hfYIXEuhc2mXMGN5nUPis8beH6uE/zl3uMWJcyHX0/LN/+QxO9zhYuV6l8AsAaphHFyS/fBv0SW3Nid7jw5hKQ==",
"requires": {
"@babel/runtime": "^7.17.2",
"@types/prop-types": "^15.7.4",
"@types/react-is": "^16.7.1 || ^17.0.0",
"prop-types": "^15.7.2",
"react-is": "^17.0.2"
}
},
"@popperjs/core": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz",
"integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg=="
},
"csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
}
}
},
"@mui/material": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz",
@ -3306,6 +3457,11 @@
"type": "^1.0.1"
}
},
"date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
},
"dayjs": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
@ -6749,6 +6905,11 @@
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
},
"rifm": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz",
"integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg=="
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",

View File

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

View File

@ -1,8 +1,12 @@
import React, { Component } from "react";
import { render } from "react-dom";
import HomePage from "./HomePage";
import BottomBar from "./BottomBar";
import { CssBaseline, IconButton} from "@mui/material";
import { ThemeProvider, createTheme } from '@mui/material/styles';
import UnsafeAlert from "./UnsafeAlert";
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
export default class App extends Component {
constructor(props) {
@ -10,6 +14,7 @@ export default class App extends Component {
this.state = {
nickname: null,
token: null,
dark: false,
}
}
@ -17,11 +22,28 @@ export default class App extends Component {
this.setState(newState)
}
lightTheme = createTheme({
});
darkTheme = createTheme({
palette: {
mode: 'dark',
background: {
default: "#070707"
},
},
});
render() {
return (
<>
<HomePage setAppState={this.setAppState}/>
</>
<ThemeProvider theme={this.state.dark ? this.darkTheme : this.lightTheme}>
<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=='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=()=>{
return (
@ -90,7 +99,10 @@ export default class BookPage extends Component {
robot: order.maker_nick,
robot_status: order.maker_status,
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),
payment_method: order.payment_method,
price: order.price,
@ -123,9 +135,9 @@ export default class BookPage extends Component {
);
} },
{ 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 (
<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,
renderCell: (params) => {return (
@ -163,7 +175,10 @@ export default class BookPage extends Component {
robot: order.maker_nick,
robot_status: order.maker_status,
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),
payment_method: order.payment_method,
price: order.price,
@ -191,10 +206,10 @@ export default class BookPage extends Component {
);
} },
{ 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 (
<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>
)} },
{ field: 'currency', headerName: 'Currency', width: 100,

View File

@ -1,7 +1,7 @@
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 { Link } from 'react-router-dom'
import { Link as LinkRouter } from 'react-router-dom'
// Icons
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 PersonAddAltIcon from '@mui/icons-material/PersonAddAlt';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import AmbossIcon from "./icons/AmbossIcon"
// pretty numbers
function pn(x) {
@ -74,7 +75,8 @@ export default class BottomBar extends Component {
profileShown: false,
alternative_site: 'robosats...',
node_id: '00000000',
referral_link: 'Loading...',
showRewards: false,
referral_code: '',
earned_rewards: 0,
rewardInvoice: null,
badInvoice: false,
@ -88,8 +90,8 @@ export default class BottomBar extends Component {
this.setState(null)
fetch('/api/info/')
.then((response) => response.json())
.then((data) => this.setState(data) &
this.props.setAppState({nickname:data.nickname, loading:false}));
.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}));
}
handleClickOpenStatsForNerds = () => {
@ -123,31 +125,28 @@ export default class BottomBar extends Component {
<ListItem>
<ListItemIcon><DnsIcon/></ListItemIcon>
<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)"}
</a>
</Link>
</ListItemText>
</ListItem>
:
<ListItem>
<ListItemAvatar>
<Avatar sx={{ width: 25, height:25, }} src='/static/assets/images/amboss.png' variant="rounded"/>
</ListItemAvatar>
<ListItemIcon><AmbossIcon/></ListItemIcon>
<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)"}
</a>
</Link>
</ListItemText>
</ListItem>
}
<Divider/>
<ListItem>
<ListItemIcon><WebIcon/></ListItemIcon>
<ListItemText secondary={this.state.alternative_name}>
<a target="_blank" href={"http://"+this.state.alternative_site}>{this.state.alternative_site.slice(0, 12)+"...onion"}
</a>
<Link target="_blank" href={"http://"+this.state.alternative_site}>{this.state.alternative_site.slice(0, 12)+"...onion"}
</Link>
</ListItemText>
</ListItem>
@ -155,9 +154,9 @@ export default class BottomBar extends Component {
<ListItem>
<ListItemIcon><GitHubIcon/></ListItemIcon>
<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)+"..."}
</a>
</Link>
</ListItemText>
</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 =() =>{
return(
<Dialog
@ -301,8 +305,7 @@ export default class BottomBar extends Component {
<Divider/>
{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={Link}>
<ListItemButton onClick={this.handleClickCloseProfile} to={'/order/'+this.state.active_order_id} component={LinkRouter}>
<ListItemIcon>
<Badge badgeContent="" color="primary">
<NumbersIcon color="primary"/>
@ -343,80 +346,92 @@ export default class BottomBar extends Component {
</ListItemText>
</ListItem>
<Divider><Chip label='Rewards & Compensations'/></Divider>
<ListItem>
<ListItemIcon>
<PersonAddAltIcon/>
</ListItemIcon>
<ListItemText secondary="Share to earn 100 Sats per trade">
<TextField
label='Your Referral Link'
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>,
}}
<Divider/>
<Grid spacing={1} align="center">
<FormControlLabel labelPlacement="start"control={
<Switch
checked={this.state.showRewards}
onChange={()=> this.setState({showRewards: !this.state.showRewards})}/>}
label="Rewards and compensations"
/>
</ListItemText>
</ListItem>
</Grid>
<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>
</ListItemText>
:
<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 });
<div style={{ display: this.state.showRewards ? '':'none'}}>
<ListItem>
<ListItemIcon>
<PersonAddAltIcon/>
</ListItemIcon>
<ListItemText secondary="Share to earn 100 Sats per trade">
<TextField
label='Your referral link'
value={this.getHost()+'/ref/'+this.state.referral_code}
// variant='filled'
size='small'
InputProps={{
endAdornment:
<Tooltip disableHoverListener enterTouchDelay="0" title="Copied!">
<IconButton onClick= {()=>navigator.clipboard.writeText('http://'+this.getHost()+'/ref/'+this.state.referral_code)}>
<ContentCopy />
</IconButton>
</Tooltip>,
}}
/>
</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 item alignItems="stretch" style={{ display: "flex" }}>
<Button sx={{maxHeight:38}} onClick={this.handleSubmitInvoiceClicked} variant="contained" color="primary" size="small" > Submit </Button>
</ListItemText>
:
<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>
</form>
}
</ListItem>
</form>
}
</ListItem>
{this.state.showRewardsSpinner?
<div style={{display: 'flex', justifyContent: 'center'}}>
<CircularProgress/>
</div>
:""}
{this.state.withdrawn?
<div style={{display: 'flex', justifyContent: 'center'}}>
<Typography color="primary" variant="body2"><b>There it goes, thank you!🥇</b></Typography>
</div>
:""}
{this.state.showRewardsSpinner?
<div style={{display: 'flex', justifyContent: 'center'}}>
<CircularProgress/>
</div>
:""}
{this.state.withdrawn?
<div style={{display: 'flex', justifyContent: 'center'}}>
<Typography color="primary" variant="body2"><b>There it goes, thank you!🥇</b></Typography>
</div>
:""}
</div>
</List>
</DialogContent>

View File

@ -1,5 +1,5 @@
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';
export default class Chat extends Component {
@ -85,7 +85,7 @@ export default class Chat extends Component {
<Grid item xs={0.3}/>
<Grid item xs={5.5}>
<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'}
</Typography>
</Paper>
@ -93,19 +93,19 @@ export default class Chat extends Component {
<Grid item xs={0.4}/>
<Grid item xs={5.5}>
<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'}
</Typography>
</Paper>
</Grid>
<Grid item xs={0.3}/>
</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 => <>
<Card elevation={5} align="left" >
{/* If message sender is not our nick, gray color, if it is our nick, green color */}
{message.userNick == this.props.ur_nick ?
<CardHeader
<CardHeader sx={{color: '#111111'}}
avatar={
<Badge variant="dot" overlap="circular" badgeContent="" color={this.state.connected ? "success" : "error"}>
<Avatar className="flippedSmallAvatar"
@ -117,10 +117,10 @@ export default class Chat extends Component {
style={{backgroundColor: '#eeeeee'}}
title={message.userNick}
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={
<Badge variant="dot" overlap="circular" badgeContent="" color={this.state.peer_connected ? "success" : "error"}>
<Avatar className="flippedSmallAvatar"
@ -132,7 +132,7 @@ export default class Chat extends Component {
style={{backgroundColor: '#fafafa'}}
title={message.userNick}
subheader={message.msg}
subheaderTypographyProps={{sx: {wordWrap: "break-word", width: 200}}}
subheaderTypographyProps={{sx: {wordWrap: "break-word", width: '200px', color: '#444444'}}}
/>}
</Card>
</>)}
@ -160,7 +160,7 @@ export default class Chat extends Component {
</Grid>
</form>
<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>
</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 Image from 'material-ui-image'
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
matchmaking and minimizes the need of trust. RoboSats focuses in privacy and speed.</p>
<p>RoboSats is an open source project <a
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a>
<p>RoboSats is an open source project <Link
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</Link>
</p>
</Typography>
</Grid>
@ -38,13 +38,13 @@ export default class InfoDialog extends Component {
<Typography component="h4" variant="h4">What is <i>RoboSats</i>?</Typography>
<Typography component="body2" variant="body2">
<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
width='100%'
src={window.location.origin +'/static/assets/images/robosats_0.1.0_banner.png'}
/>
<p>RoboSats is an open source project <a
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a>
<p>RoboSats is an open source project <Link
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</Link>
</p>
</Typography>
</MediaQuery>
@ -59,10 +59,11 @@ export default class InfoDialog extends Component {
received the fiat, then the satoshis are released to Bob. Enjoy your satoshis,
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
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 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>
</p>
<p> You can build more trust on <i>RoboSats</i> by <a href='https://github.com/reckless-satoshi/robosats'>
inspecting the source code. </a> </p>
<p> You can build more trust on <i>RoboSats</i> by <Link href='https://github.com/reckless-satoshi/robosats'>
inspecting the source code. </Link> </p>
</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">
<p> This lightning application is provided as is. It is in active
development: trade with the utmost caution. There is no private
support. Support is only offered via public channels <a href='https://t.me/robosats'>
(Telegram)</a>. <i>RoboSats</i> will never contact you. <i>
support. Support is only offered via public channels <Link href='https://t.me/robosats'>
(Telegram)</Link>. <i>RoboSats</i> will never contact you. <i>
RoboSats</i> will definitely never ask for your robot token.
</p>
</Typography>

View File

@ -1,8 +1,13 @@
import React, { Component } from 'react';
import { Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
import { Link } from 'react-router-dom'
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 { 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 LockIcon from '@mui/icons-material/Lock';
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
@ -37,6 +42,9 @@ export default class MakerPage extends Component {
defaultPremium = 0;
minTradeSats = 20000;
maxTradeSats = 800000;
maxBondlessSats = 50000;
maxRangeAmountMultiple = 4.8;
minRangeAmountMultiple = 1.6;
constructor(props) {
super(props);
@ -48,11 +56,40 @@ export default class MakerPage extends Component {
payment_method: this.defaultPaymentMethod,
premium: 0,
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()
}
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)=>{
this.setState({
type: e.target.value,
@ -63,12 +100,64 @@ export default class MakerPage extends Component {
currency: 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)=>{
this.setState({
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)=>{
this.setState({
payment_method: e.target.value,
@ -118,7 +207,6 @@ export default class MakerPage extends Component {
handleCreateOfferButtonPressed=()=>{
this.state.amount == null ? this.setState({amount: 0}) : null;
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
@ -126,10 +214,16 @@ export default class MakerPage extends Component {
type: this.state.type,
currency: this.state.currency,
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,
is_explicit: this.state.is_explicit,
premium: this.state.is_explicit ? null: this.state.premium,
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)
@ -152,8 +246,406 @@ export default class MakerPage extends Component {
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() {
return (
<Grid container xs={12} align="center" spacing={1} sx={{minWidth:380}}>
{/* <Grid item xs={12} align="center" sx={{minWidth:380}}>
@ -161,154 +653,14 @@ export default class MakerPage extends Component {
ORDER MAKER
</Typography>
</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">
<Tooltip placement="top" enterTouchDelay="300" enterDelay="700" enterNextDelay="2000" title="Enter your prefered fiat payment methods (instant recommended)">
<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>
<this.makeOrderBox/>
</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">
{/* conditions to disable the make button */}
{(this.state.amount == null ||
this.state.amount <= 0 ||
{(this.state.amount == null & (this.state.enableAmountRange == false & this.state.minAmount == null) ||
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.badPremium != null))
?
@ -328,7 +680,8 @@ export default class MakerPage extends Component {
: ""}
<Typography component="subtitle2" variant="subtitle2">
<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.premium == 0 ? " at market price" :
(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>
</Typography>
<Grid item xs={12} align="center">
<Button color="secondary" variant="contained" to="/" component={Link}>
<Button color="secondary" variant="contained" to="/" component={LinkRouter}>
Back
</Button>
</Grid>

View File

@ -1,5 +1,5 @@
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 MediaQuery from 'react-responsive'
@ -89,6 +89,7 @@ export default class OrderPage extends Component {
}
var otherStateVars = {
amount: newStateVars.amount ? newStateVars.amount : null,
loading: false,
delay: this.setDelay(newStateVars.status),
currencyCode: this.getCurrencyCode(newStateVars.currency),
@ -130,7 +131,7 @@ export default class OrderPage extends Component {
return (<span> The order has expired</span>);
} else {
var col = 'black'
var col = 'inherit'
var fraction_left = (total/1000) / this.state.total_secs_exp
// Make orange at 25% of time left
if (fraction_left < 0.25){col = 'orange'}
@ -157,32 +158,92 @@ export default class OrderPage extends Component {
}
};
countdownTakeOrderRenderer = ({ seconds, completed }) => {
if(isNaN(seconds)){
return (
<>
handleTakeAmountChange = (e) => {
if (e.target.value != "" & e.target.value != null){
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/>
<Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>)
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>
)
}
}
countdownTakeOrderRenderer = ({ seconds, completed }) => {
if(isNaN(seconds)){return (<this.takeOrderButton/>)}
if (completed) {
// Render a completed state
return (
<>
<this.InactiveMakerDialog/>
<Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>
);
return ( <this.takeOrderButton/>);
} else{
return(
<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>)
}
};
@ -212,12 +273,13 @@ export default class OrderPage extends Component {
takeOrder=()=>{
this.setState({loading:true})
console.log(this.state.takeAmount)
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'take',
'amount':this.state.takeAmount,
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
@ -483,10 +545,17 @@ export default class OrderPage extends Component {
<ListItemIcon>
{getFlags(this.state.currencyCode)}
</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))
+" "+this.state.currencyCode} secondary="Amount"/>
}
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<PaymentsIcon/>
@ -621,7 +690,7 @@ export default class OrderPage extends Component {
};
return(
<Box sx={{ width: '100%' }}>
<Box sx={{ width: '100%'}}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} variant="fullWidth" >
<Tab label="Order" {...this.a11yProps(0)} />

View File

@ -1,5 +1,5 @@
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 Countdown, { zeroPad} from 'react-countdown';
import Chat from "./Chat"
@ -214,7 +214,9 @@ export default class TradeBox extends Component {
}
</Grid>
<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!">
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bond_invoice)}} align="center"> <ContentCopy/> Copy to clipboard</Button>
</Tooltip>
@ -284,7 +286,9 @@ export default class TradeBox extends Component {
</Typography>
</Grid>
<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!">
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrow_invoice)}} align="center"> <ContentCopy/>Copy to clipboard</Button>
</Tooltip>
@ -385,7 +389,7 @@ export default class TradeBox extends Component {
<Typography component="body2" variant="body2" align="left">
<p>Be patient while robots check the book.
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
return to you (no action needed).</p>
</Typography>
@ -910,7 +914,7 @@ handleRatingRobosatsChange=(e)=>{
<Typography component="body2" variant="body2" align="center">
<p><b>Thank you for using Robosats!</b></p>
<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>
</Grid>
: null}
@ -970,8 +974,8 @@ handleRatingRobosatsChange=(e)=>{
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
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
compatible wallets</a>
Muun wallet is not recommended, <Link href="https://github.com/Reckless-Satoshi/robosats/issues/44">check the list of
compatible wallets</Link>
</Typography>
</Grid>
<Grid item xs={12} align="center">
@ -1011,7 +1015,7 @@ handleRatingRobosatsChange=(e)=>{
<Grid item xs={12} 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
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.
</Typography>
<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 CasinoIcon from '@mui/icons-material/Casino';
import ContentCopy from "@mui/icons-material/ContentCopy";
import RoboSatsNoTextIcon from "./icons/RoboSatsNoTextIcon"
function getCookie(name) {
let cookieValue = null;
@ -149,6 +150,9 @@ export default class UserGenPage extends Component {
render() {
return (
<Grid container spacing={1}>
<Grid item>
<div className='clickTrough'/>
</Grid>
<Grid item xs={12} align="center" sx={{width:370, height:260}}>
{!this.state.loadingRobot ?
<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>
</ButtonGroup>
</Grid>
<Grid item xs={12} align="center">
<Typography component="h5" variant="h5">
Simple and Private Lightning peer-to-peer Exchange
</Typography>
<Grid item xs={12} align="center" spacing={2} sx={{width:370}}>
<Grid item>
<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>
);

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%;
margin: 0;
padding: 0;
font-family: 'Roboto';
}
#main {
@ -18,6 +19,11 @@ body {
height: 100%;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.appCenter {
position: absolute;
top: 50%;
@ -25,6 +31,33 @@ body {
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 {
position: fixed;
bottom: 0;
@ -32,6 +65,18 @@ body {
height: 40px;
}
.amboss{
fill:url(#SVGID_1_);
}
.advancedSwitch{
width: 20;
left: 50%;
transform: translate(62px, 0px);
margin-right: 0;
margin-left: auto;
}
.bottomItem {
margin: 0;
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>
{% load static %}
<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" %}"/>
</head>
<body>
@ -23,7 +24,33 @@
</div>
</noscript>
<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>
<script src="{% static "frontend/main.js" %}"></script>

View File

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

View File

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

View File

@ -39,7 +39,11 @@ app.conf.beat_schedule = {
"task": "give_rewards",
"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",
"schedule": timedelta(seconds=60),
},

View File

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