mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-14 03:16:24 +00:00
Merge pull request #76 from Reckless-Satoshi/advanced-maker-options-1
Advanced maker options v1
This commit is contained in:
commit
36134cf1a7
12
.env-sample
12
.env-sample
@ -55,8 +55,10 @@ FEE = 0.002
|
||||
# Shall incentivize order making
|
||||
MAKER_FEE_SPLIT=0.125
|
||||
|
||||
# Default bond size as fraction
|
||||
BOND_SIZE = 0.01
|
||||
# Bond size as percentage (%)
|
||||
DEFAULT_BOND_SIZE = 1
|
||||
MIN_BOND_SIZE = 1
|
||||
MAX_BOND_SIZE = 15
|
||||
|
||||
# Time out penalty for canceling takers in SECONDS
|
||||
PENALTY_TIMEOUT = 60
|
||||
@ -69,6 +71,7 @@ MAX_PUBLIC_ORDERS = 100
|
||||
# Trade limits in satoshis
|
||||
MIN_TRADE = 20000
|
||||
MAX_TRADE = 800000
|
||||
MAX_TRADE_BONDLESS_TAKER = 50000
|
||||
|
||||
# Expiration (CLTV_expiry) time for HODL invoices in HOURS // 7 min/block assumed
|
||||
BOND_EXPIRY = 54
|
||||
@ -79,7 +82,10 @@ EXP_MAKER_BOND_INVOICE = 300
|
||||
EXP_TAKER_BOND_INVOICE = 200
|
||||
|
||||
# Time a order is public in the book HOURS
|
||||
PUBLIC_ORDER_DURATION = 6
|
||||
DEFAULT_PUBLIC_ORDER_DURATION = 24
|
||||
MAX_PUBLIC_ORDER_DURATION = 24
|
||||
MIN_PUBLIC_ORDER_DURATION = 0.166
|
||||
|
||||
# Time to provide a valid invoice and the trade escrow MINUTES
|
||||
INVOICE_AND_ESCROW_DURATION = 30
|
||||
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -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*
|
24
api/admin.py
24
api/admin.py
@ -13,21 +13,25 @@ class ProfileInline(admin.StackedInline):
|
||||
can_delete = False
|
||||
fields = ("avatar_tag", )
|
||||
readonly_fields = ["avatar_tag"]
|
||||
|
||||
show_change_link = True
|
||||
|
||||
# extended users with avatars
|
||||
@admin.register(User)
|
||||
class EUserAdmin(UserAdmin):
|
||||
class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
|
||||
inlines = [ProfileInline]
|
||||
list_display = (
|
||||
"avatar_tag",
|
||||
"id",
|
||||
"profile_link",
|
||||
"username",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
"is_staff",
|
||||
)
|
||||
list_display_links = ("id", "username")
|
||||
change_links = (
|
||||
"profile",
|
||||
)
|
||||
ordering = ("-id", )
|
||||
|
||||
def avatar_tag(self, obj):
|
||||
@ -42,7 +46,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"maker_link",
|
||||
"taker_link",
|
||||
"status",
|
||||
"amount",
|
||||
"amt",
|
||||
"currency_link",
|
||||
"t0_satoshis",
|
||||
"is_disputed",
|
||||
@ -65,7 +69,13 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"trade_escrow",
|
||||
)
|
||||
list_filter = ("is_disputed", "is_fiat_sent", "type", "currency", "status")
|
||||
search_fields = ["id","amount"]
|
||||
search_fields = ["id","amount","min_amount","max_amount"]
|
||||
|
||||
def amt(self, obj):
|
||||
if obj.has_range and obj.amount == None:
|
||||
return str(float(obj.min_amount))+"-"+ str(float(obj.max_amount))
|
||||
else:
|
||||
return float(obj.amount)
|
||||
|
||||
@admin.register(LNPayment)
|
||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
@ -74,6 +84,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"concept",
|
||||
"status",
|
||||
"num_satoshis",
|
||||
"fee",
|
||||
"type",
|
||||
"expires_at",
|
||||
"expiry_height",
|
||||
@ -95,7 +106,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
)
|
||||
list_filter = ("type", "concept", "status")
|
||||
ordering = ("-expires_at", )
|
||||
search_fields = ["payment_hash","num_satoshis"]
|
||||
search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"]
|
||||
|
||||
|
||||
@admin.register(Profile)
|
||||
@ -116,9 +127,11 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"num_disputes",
|
||||
"lost_disputes",
|
||||
)
|
||||
list_editable = ["pending_rewards", "earned_rewards"]
|
||||
list_display_links = ("avatar_tag", "id")
|
||||
change_links = ["user"]
|
||||
readonly_fields = ["avatar_tag"]
|
||||
search_fields = ["user__username","id"]
|
||||
|
||||
|
||||
@admin.register(Currency)
|
||||
@ -128,7 +141,6 @@ class CurrencieAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("currency", "exchange_rate", "timestamp")
|
||||
ordering = ("id", )
|
||||
|
||||
|
||||
@admin.register(MarketTick)
|
||||
class MarketTickAdmin(admin.ModelAdmin):
|
||||
list_display = ("timestamp", "price", "volume", "premium", "currency",
|
||||
|
@ -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
|
||||
|
||||
|
131
api/logics.py
131
api/logics.py
@ -14,7 +14,6 @@ import time
|
||||
FEE = float(config("FEE"))
|
||||
MAKER_FEE_SPLIT = float(config("MAKER_FEE_SPLIT"))
|
||||
|
||||
BOND_SIZE = float(config("BOND_SIZE"))
|
||||
ESCROW_USERNAME = config("ESCROW_USERNAME")
|
||||
PENALTY_TIMEOUT = int(config("PENALTY_TIMEOUT"))
|
||||
|
||||
@ -27,7 +26,6 @@ EXP_TAKER_BOND_INVOICE = int(config("EXP_TAKER_BOND_INVOICE"))
|
||||
BOND_EXPIRY = int(config("BOND_EXPIRY"))
|
||||
ESCROW_EXPIRY = int(config("ESCROW_EXPIRY"))
|
||||
|
||||
PUBLIC_ORDER_DURATION = int(config("PUBLIC_ORDER_DURATION"))
|
||||
INVOICE_AND_ESCROW_DURATION = int(config("INVOICE_AND_ESCROW_DURATION"))
|
||||
FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION"))
|
||||
|
||||
@ -89,24 +87,65 @@ class Logics:
|
||||
|
||||
return True, None, None
|
||||
|
||||
def validate_order_size(order):
|
||||
"""Validates if order is withing limits in satoshis at t0"""
|
||||
if order.t0_satoshis > MAX_TRADE:
|
||||
@classmethod
|
||||
def validate_order_size(cls, order):
|
||||
"""Validates if order size in Sats is within limits at t0"""
|
||||
if not order.has_range:
|
||||
if order.t0_satoshis > MAX_TRADE:
|
||||
return False, {
|
||||
"bad_request":
|
||||
"Your order is too big. It is worth " +
|
||||
"{:,}".format(order.t0_satoshis) +
|
||||
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
|
||||
" Sats"
|
||||
}
|
||||
if order.t0_satoshis < MIN_TRADE:
|
||||
return False, {
|
||||
"bad_request":
|
||||
"Your order is too small. It is worth " +
|
||||
"{:,}".format(order.t0_satoshis) +
|
||||
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
|
||||
" Sats"
|
||||
}
|
||||
elif order.has_range:
|
||||
min_sats = cls.calc_sats(order.min_amount, order.currency.exchange_rate, order.premium)
|
||||
max_sats = cls.calc_sats(order.max_amount, order.currency.exchange_rate, order.premium)
|
||||
if min_sats > max_sats/1.5:
|
||||
return False, {
|
||||
"bad_request":
|
||||
"Maximum range amount must be at least 50 percent higher than the minimum amount"
|
||||
}
|
||||
elif max_sats > MAX_TRADE:
|
||||
return False, {
|
||||
"bad_request":
|
||||
"Your order maximum amount is too big. It is worth " +
|
||||
"{:,}".format(int(max_sats)) +
|
||||
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
|
||||
" Sats"
|
||||
}
|
||||
elif min_sats < MIN_TRADE:
|
||||
return False, {
|
||||
"bad_request":
|
||||
"Your order minimum amount is too small. It is worth " +
|
||||
"{:,}".format(int(min_sats)) +
|
||||
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
|
||||
" Sats"
|
||||
}
|
||||
elif min_sats < max_sats/5:
|
||||
return False, {
|
||||
"bad_request":
|
||||
f"Your order amount range is too large. Max amount can only be 5 times bigger than min amount"
|
||||
}
|
||||
|
||||
return True, None
|
||||
|
||||
def validate_amount_within_range(order, amount):
|
||||
if amount > float(order.max_amount) or amount < float(order.min_amount):
|
||||
return False, {
|
||||
"bad_request":
|
||||
"Your order is too big. It is worth " +
|
||||
"{:,}".format(order.t0_satoshis) +
|
||||
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
|
||||
" Sats"
|
||||
}
|
||||
if order.t0_satoshis < MIN_TRADE:
|
||||
return False, {
|
||||
"bad_request":
|
||||
"Your order is too small. It is worth " +
|
||||
"{:,}".format(order.t0_satoshis) +
|
||||
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
|
||||
" Sats"
|
||||
"The amount specified is outside the range specified by the maker"
|
||||
}
|
||||
|
||||
return True, None
|
||||
|
||||
def user_activity_status(last_seen):
|
||||
@ -118,7 +157,7 @@ class Logics:
|
||||
return "Inactive"
|
||||
|
||||
@classmethod
|
||||
def take(cls, order, user):
|
||||
def take(cls, order, user, amount=None):
|
||||
is_penalized, time_out = cls.is_penalized(user)
|
||||
if is_penalized:
|
||||
return False, {
|
||||
@ -126,10 +165,12 @@ class Logics:
|
||||
f"You need to wait {time_out} seconds to take an order",
|
||||
}
|
||||
else:
|
||||
if order.has_range:
|
||||
order.amount= amount
|
||||
order.taker = user
|
||||
order.status = Order.Status.TAK
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.TAK])
|
||||
seconds=order.t_to_expire(Order.Status.TAK))
|
||||
order.save()
|
||||
# send_message.delay(order.id,'order_taken') # Too spammy
|
||||
return True, None
|
||||
@ -146,15 +187,19 @@ class Logics:
|
||||
return (is_maker and order.type == Order.Types.SELL) or (
|
||||
is_taker and order.type == Order.Types.BUY)
|
||||
|
||||
def satoshis_now(order):
|
||||
def calc_sats(amount, exchange_rate, premium):
|
||||
exchange_rate = float(exchange_rate)
|
||||
premium_rate = exchange_rate * (1 + float(premium) / 100)
|
||||
return (float(amount) /premium_rate) * 100 * 1000 * 1000
|
||||
|
||||
@classmethod
|
||||
def satoshis_now(cls, order):
|
||||
"""checks trade amount in sats"""
|
||||
if order.is_explicit:
|
||||
satoshis_now = order.satoshis
|
||||
else:
|
||||
exchange_rate = float(order.currency.exchange_rate)
|
||||
premium_rate = exchange_rate * (1 + float(order.premium) / 100)
|
||||
satoshis_now = (float(order.amount) /
|
||||
premium_rate) * 100 * 1000 * 1000
|
||||
amount = order.amount if order.amount != None else order.max_amount
|
||||
satoshis_now = cls.calc_sats(amount, order.currency.exchange_rate, order.premium)
|
||||
|
||||
return int(satoshis_now)
|
||||
|
||||
@ -165,8 +210,8 @@ class Logics:
|
||||
premium = order.premium
|
||||
price = exchange_rate * (1 + float(premium) / 100)
|
||||
else:
|
||||
order_rate = float(
|
||||
order.amount) / (float(order.satoshis) / 100000000)
|
||||
amount = order.amount if not order.has_range else order.max_amount
|
||||
order_rate = float(amount) / (float(order.satoshis) / 100000000)
|
||||
premium = order_rate / exchange_rate - 1
|
||||
premium = int(premium * 10000) / 100 # 2 decimals left
|
||||
price = order_rate
|
||||
@ -336,7 +381,7 @@ class Logics:
|
||||
order.is_disputed = True
|
||||
order.status = Order.Status.DIS
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.DIS])
|
||||
seconds=order.t_to_expire(Order.Status.DIS))
|
||||
order.save()
|
||||
|
||||
# User could be None if a dispute is open automatically due to weird expiration.
|
||||
@ -380,7 +425,7 @@ class Logics:
|
||||
if order.maker_statement not in [None,""] and order.taker_statement not in [None,""]:
|
||||
order.status = Order.Status.WFR
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.WFR])
|
||||
seconds=order.t_to_expire(Order.Status.WFR))
|
||||
|
||||
order.save()
|
||||
return True, None
|
||||
@ -442,6 +487,12 @@ class Logics:
|
||||
"bad_request":
|
||||
"You cannot submit a invoice while bonds are not locked."
|
||||
}
|
||||
if order.status == Order.Status.FAI:
|
||||
if order.payout.status != LNPayment.Status.EXPIRE:
|
||||
return False, {
|
||||
"bad_request":
|
||||
"You cannot submit an invoice only after expiration or 3 failed attempts"
|
||||
}
|
||||
|
||||
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
|
||||
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
||||
@ -472,7 +523,7 @@ class Logics:
|
||||
if order.status == Order.Status.WFI:
|
||||
order.status = Order.Status.CHA
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.CHA])
|
||||
seconds=order.t_to_expire(Order.Status.CHA))
|
||||
|
||||
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
|
||||
if order.status == Order.Status.WF2:
|
||||
@ -483,7 +534,7 @@ class Logics:
|
||||
elif order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||
order.status = Order.Status.CHA
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.CHA])
|
||||
seconds=order.t_to_expire(Order.Status.CHA))
|
||||
else:
|
||||
order.status = Order.Status.WFE
|
||||
|
||||
@ -661,7 +712,9 @@ class Logics:
|
||||
def publish_order(order):
|
||||
order.status = Order.Status.PUB
|
||||
order.expires_at = order.created_at + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.PUB])
|
||||
seconds=order.t_to_expire(Order.Status.PUB))
|
||||
if order.has_range:
|
||||
order.amount = None
|
||||
order.save()
|
||||
# send_message.delay(order.id,'order_published') # too spammy
|
||||
return
|
||||
@ -699,7 +752,7 @@ class Logics:
|
||||
|
||||
# If there was no maker_bond object yet, generates one
|
||||
order.last_satoshis = cls.satoshis_now(order)
|
||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||
bond_satoshis = int(order.last_satoshis * order.bond_size/100)
|
||||
|
||||
description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."
|
||||
|
||||
@ -708,7 +761,7 @@ class Logics:
|
||||
hold_payment = LNNode.gen_hold_invoice(
|
||||
bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WFB],
|
||||
invoice_expiry=order.t_to_expire(Order.Status.WFB),
|
||||
cltv_expiry_secs=BOND_EXPIRY * 3600,
|
||||
)
|
||||
except Exception as e:
|
||||
@ -759,7 +812,7 @@ class Logics:
|
||||
|
||||
# With the bond confirmation the order is extended 'public_order_duration' hours
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.WF2])
|
||||
seconds=order.t_to_expire(Order.Status.WF2))
|
||||
order.status = Order.Status.WF2
|
||||
order.save()
|
||||
|
||||
@ -809,7 +862,7 @@ class Logics:
|
||||
|
||||
# If there was no taker_bond object yet, generates one
|
||||
order.last_satoshis = cls.satoshis_now(order)
|
||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||
bond_satoshis = int(order.last_satoshis * order.bond_size/100)
|
||||
pos_text = "Buying" if cls.is_buyer(order, user) else "Selling"
|
||||
description = (
|
||||
f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
|
||||
@ -822,7 +875,7 @@ class Logics:
|
||||
hold_payment = LNNode.gen_hold_invoice(
|
||||
bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.TAK],
|
||||
invoice_expiry=order.t_to_expire(Order.Status.TAK),
|
||||
cltv_expiry_secs=BOND_EXPIRY * 3600,
|
||||
)
|
||||
|
||||
@ -850,7 +903,7 @@ class Logics:
|
||||
)
|
||||
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.TAK])
|
||||
seconds=order.t_to_expire(Order.Status.TAK))
|
||||
order.save()
|
||||
return True, {
|
||||
"bond_invoice": hold_payment["invoice"],
|
||||
@ -866,7 +919,7 @@ class Logics:
|
||||
elif order.status == Order.Status.WFE:
|
||||
order.status = Order.Status.CHA
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.CHA])
|
||||
seconds=order.t_to_expire(Order.Status.CHA))
|
||||
order.save()
|
||||
|
||||
@classmethod
|
||||
@ -909,7 +962,7 @@ class Logics:
|
||||
hold_payment = LNNode.gen_hold_invoice(
|
||||
escrow_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WF2],
|
||||
invoice_expiry=order.t_to_expire(Order.Status.WF2),
|
||||
cltv_expiry_secs=ESCROW_EXPIRY * 3600,
|
||||
)
|
||||
|
||||
|
@ -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']
|
||||
|
||||
|
103
api/models.py
103
api/models.py
@ -5,6 +5,7 @@ from django.core.validators import (
|
||||
MinValueValidator,
|
||||
validate_comma_separated_integer_list,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.template.defaultfilters import truncatechars
|
||||
from django.dispatch import receiver
|
||||
@ -19,7 +20,7 @@ import json
|
||||
MIN_TRADE = int(config("MIN_TRADE"))
|
||||
MAX_TRADE = int(config("MAX_TRADE"))
|
||||
FEE = float(config("FEE"))
|
||||
BOND_SIZE = float(config("BOND_SIZE"))
|
||||
DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
|
||||
|
||||
|
||||
class Currency(models.Model):
|
||||
@ -38,7 +39,7 @@ class Currency(models.Model):
|
||||
null=True,
|
||||
validators=[MinValueValidator(0)],
|
||||
)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
def __str__(self):
|
||||
# returns currency label ( 3 letters code)
|
||||
@ -105,9 +106,11 @@ class LNPayment(models.Model):
|
||||
default=None,
|
||||
blank=True)
|
||||
num_satoshis = models.PositiveBigIntegerField(validators=[
|
||||
MinValueValidator(MIN_TRADE * BOND_SIZE),
|
||||
MaxValueValidator(MAX_TRADE * (1 + BOND_SIZE + FEE)),
|
||||
MinValueValidator(100),
|
||||
MaxValueValidator(MAX_TRADE * (1 + DEFAULT_BOND_SIZE + FEE)),
|
||||
])
|
||||
# Fee in sats with mSats decimals fee_msat
|
||||
fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False)
|
||||
created_at = models.DateTimeField()
|
||||
expires_at = models.DateTimeField()
|
||||
cltv_expiry = models.PositiveSmallIntegerField(null=True,
|
||||
@ -181,7 +184,7 @@ class Order(models.Model):
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices,
|
||||
null=False,
|
||||
default=Status.WFB)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
expires_at = models.DateTimeField()
|
||||
|
||||
# order details
|
||||
@ -189,14 +192,15 @@ class Order(models.Model):
|
||||
currency = models.ForeignKey(Currency,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
amount = models.DecimalField(max_digits=16,
|
||||
decimal_places=8,
|
||||
validators=[MinValueValidator(0.00000001)])
|
||||
amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||
has_range = models.BooleanField(default=False, null=False, blank=False)
|
||||
min_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||
max_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||
payment_method = models.CharField(max_length=35,
|
||||
null=False,
|
||||
default="not specified",
|
||||
blank=True)
|
||||
|
||||
bondless_taker = models.BooleanField(default=False, null=False, blank=False)
|
||||
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||
is_explicit = models.BooleanField(default=False, null=False)
|
||||
# marked to market
|
||||
@ -218,6 +222,29 @@ class Order(models.Model):
|
||||
],
|
||||
blank=True,
|
||||
)
|
||||
# optionally makers can choose the public order duration length (seconds)
|
||||
public_duration = models.PositiveBigIntegerField(
|
||||
default=60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1,
|
||||
null=False,
|
||||
validators=[
|
||||
MinValueValidator(60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))), # Min is 10 minutes
|
||||
MaxValueValidator(60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))), # Max is 24 Hours
|
||||
],
|
||||
blank=False,
|
||||
)
|
||||
# optionally makers can choose the fidelity bond size of the maker and taker (%)
|
||||
bond_size = models.DecimalField(
|
||||
max_digits=4,
|
||||
decimal_places=2,
|
||||
default=DEFAULT_BOND_SIZE,
|
||||
null=False,
|
||||
validators=[
|
||||
MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 %
|
||||
MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 %
|
||||
],
|
||||
blank=False,
|
||||
)
|
||||
|
||||
# how many sats at creation and at last check (relevant for marked to market)
|
||||
t0_satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
@ -311,30 +338,38 @@ class Order(models.Model):
|
||||
maker_platform_rated = models.BooleanField(default=False, null=False)
|
||||
taker_platform_rated = models.BooleanField(default=False, null=False)
|
||||
|
||||
t_to_expire = {
|
||||
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
|
||||
1: 60 * 60 * int(config("PUBLIC_ORDER_DURATION")), # 'Public'
|
||||
2: 0, # 'Deleted'
|
||||
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
|
||||
4: 0, # 'Cancelled'
|
||||
5: 0, # 'Expired'
|
||||
6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting for trade collateral and buyer invoice'
|
||||
7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for seller trade collateral'
|
||||
8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for buyer invoice'
|
||||
9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
|
||||
10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom'
|
||||
11: 1 * 24 * 60 * 60, # 'In dispute'
|
||||
12: 0, # 'Collaboratively cancelled'
|
||||
13: 24 * 60 * 60, # 'Sending satoshis to buyer'
|
||||
14: 24 * 60 * 60, # 'Sucessful trade'
|
||||
15: 24 * 60 * 60, # 'Failed lightning network routing'
|
||||
16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
|
||||
17: 24 * 60 * 60, # 'Maker lost dispute'
|
||||
18: 24 * 60 * 60, # 'Taker lost dispute'
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}"
|
||||
if self.has_range and self.amount == None:
|
||||
amt = str(float(self.min_amount))+"-"+ str(float(self.max_amount))
|
||||
else:
|
||||
amt = float(self.amount)
|
||||
return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
|
||||
|
||||
def t_to_expire(self, status):
|
||||
|
||||
t_to_expire = {
|
||||
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
|
||||
1: self.public_duration, # 'Public'
|
||||
2: 0, # 'Deleted'
|
||||
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
|
||||
4: 0, # 'Cancelled'
|
||||
5: 0, # 'Expired'
|
||||
6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting for trade collateral and buyer invoice'
|
||||
7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for seller trade collateral'
|
||||
8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")), # 'Waiting only for buyer invoice'
|
||||
9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
|
||||
10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom'
|
||||
11: 1 * 24 * 60 * 60, # 'In dispute'
|
||||
12: 0, # 'Collaboratively cancelled'
|
||||
13: 24 * 60 * 60, # 'Sending satoshis to buyer'
|
||||
14: 24 * 60 * 60, # 'Sucessful trade'
|
||||
15: 24 * 60 * 60, # 'Failed lightning network routing'
|
||||
16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
|
||||
17: 24 * 60 * 60, # 'Maker lost dispute'
|
||||
18: 24 * 60 * 60, # 'Taker lost dispute'
|
||||
}
|
||||
|
||||
return t_to_expire[status]
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Order)
|
||||
@ -393,7 +428,7 @@ class Profile(models.Model):
|
||||
null=False
|
||||
)
|
||||
telegram_lang_code = models.CharField(
|
||||
max_length=4,
|
||||
max_length=10,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
@ -529,7 +564,7 @@ class MarketTick(models.Model):
|
||||
currency = models.ForeignKey(Currency,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
|
||||
fee = models.DecimalField(
|
||||
|
@ -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,
|
||||
|
10
api/tasks.py
10
api/tasks.py
@ -111,7 +111,7 @@ def follow_send_payment(lnpayment):
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.FAI
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.FAI])
|
||||
seconds=order.t_to_expire(Order.Status.FAI))
|
||||
order.save()
|
||||
context = {
|
||||
"routing_failed":
|
||||
@ -123,10 +123,11 @@ def follow_send_payment(lnpayment):
|
||||
if response.status == 2: # Status 2 'SUCCEEDED'
|
||||
print("SUCCEEDED")
|
||||
lnpayment.status = LNPayment.Status.SUCCED
|
||||
lnpayment.fee = float(response.fee_msat)/1000
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.SUC
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.SUC])
|
||||
seconds=order.t_to_expire(Order.Status.SUC))
|
||||
order.save()
|
||||
return True, None
|
||||
|
||||
@ -138,7 +139,7 @@ def follow_send_payment(lnpayment):
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.FAI
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.FAI])
|
||||
seconds=order.t_to_expire(Order.Status.FAI))
|
||||
order.save()
|
||||
context = {"routing_failed": "The payout invoice has expired"}
|
||||
return False, context
|
||||
@ -191,6 +192,9 @@ def send_message(order_id, message):
|
||||
from api.messages import Telegram
|
||||
telegram = Telegram()
|
||||
|
||||
if message == 'welcome':
|
||||
telegram.welcome(order)
|
||||
|
||||
if message == 'order_taken':
|
||||
telegram.order_taken(order)
|
||||
|
||||
|
@ -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()),
|
||||
]
|
||||
|
@ -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)
|
||||
|
101
api/views.py
101
api/views.py
@ -30,6 +30,8 @@ from decouple import config
|
||||
|
||||
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
|
||||
RETRY_TIME = int(config("RETRY_TIME"))
|
||||
PUBLIC_DURATION = 60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1
|
||||
BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
|
||||
|
||||
avatar_path = Path(settings.AVATAR_ROOT)
|
||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||
@ -64,35 +66,76 @@ class MakerView(CreateAPIView):
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Only allow users who are not already engaged in an order
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_409_CONFLICT)
|
||||
|
||||
type = serializer.data.get("type")
|
||||
currency = serializer.data.get("currency")
|
||||
amount = serializer.data.get("amount")
|
||||
has_range = serializer.data.get("has_range")
|
||||
min_amount = serializer.data.get("min_amount")
|
||||
max_amount = serializer.data.get("max_amount")
|
||||
payment_method = serializer.data.get("payment_method")
|
||||
premium = serializer.data.get("premium")
|
||||
satoshis = serializer.data.get("satoshis")
|
||||
is_explicit = serializer.data.get("is_explicit")
|
||||
public_duration = serializer.data.get("public_duration")
|
||||
bond_size = serializer.data.get("bond_size")
|
||||
bondless_taker = serializer.data.get("bondless_taker")
|
||||
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_409_CONFLICT)
|
||||
# Optional params
|
||||
if public_duration == None: public_duration = PUBLIC_DURATION
|
||||
if bond_size == None: bond_size = BOND_SIZE
|
||||
if bondless_taker == None: bondless_taker = False
|
||||
if has_range == None: has_range = False
|
||||
|
||||
# An order can either have an amount or a range (min_amount and max_amount)
|
||||
if has_range:
|
||||
amount = None
|
||||
else:
|
||||
min_amount = None
|
||||
max_amount = None
|
||||
|
||||
# Either amount or min_max has to be specified.
|
||||
if has_range and (min_amount == None or max_amount == None):
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You must specify min_amount and max_amount for a range order"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
elif not has_range and amount == None:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You must specify an order amount"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Creates a new order
|
||||
order = Order(
|
||||
type=type,
|
||||
currency=Currency.objects.get(id=currency),
|
||||
amount=amount,
|
||||
has_range=has_range,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
payment_method=payment_method,
|
||||
premium=premium,
|
||||
satoshis=satoshis,
|
||||
is_explicit=is_explicit,
|
||||
expires_at=timezone.now() + timedelta(
|
||||
seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
|
||||
seconds=EXP_MAKER_BOND_INVOICE),
|
||||
maker=request.user,
|
||||
public_duration=public_duration,
|
||||
bond_size=bond_size,
|
||||
bondless_taker=bondless_taker,
|
||||
)
|
||||
|
||||
# TODO move to Order class method when new instance is created!
|
||||
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
|
||||
|
||||
valid, context = Logics.validate_order_size(order)
|
||||
@ -155,7 +198,7 @@ class OrderView(viewsets.ViewSet):
|
||||
)
|
||||
|
||||
data = ListOrderSerializer(order).data
|
||||
data["total_secs_exp"] = Order.t_to_expire[order.status]
|
||||
data["total_secs_exp"] = order.t_to_expire(order.status)
|
||||
|
||||
# if user is under a limit (penalty), inform him.
|
||||
is_penalized, time_out = Logics.is_penalized(request.user)
|
||||
@ -347,6 +390,10 @@ class OrderView(viewsets.ViewSet):
|
||||
"""
|
||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
||||
|
||||
import sys
|
||||
sys.stdout.write('AAAAAA')
|
||||
print('BBBBB1')
|
||||
|
||||
serializer = UpdateOrderSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
@ -367,7 +414,17 @@ class OrderView(viewsets.ViewSet):
|
||||
request.user)
|
||||
if not valid:
|
||||
return Response(context, status=status.HTTP_409_CONFLICT)
|
||||
valid, context = Logics.take(order, request.user)
|
||||
|
||||
# For order with amount range, set the amount now.
|
||||
if order.has_range:
|
||||
amount = float(serializer.data.get("amount"))
|
||||
valid, context = Logics.validate_amount_within_range(order, amount)
|
||||
if not valid:
|
||||
return Response(context, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
valid, context = Logics.take(order, request.user, amount)
|
||||
else:
|
||||
valid, context = Logics.take(order, request.user)
|
||||
if not valid:
|
||||
return Response(context, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@ -684,11 +741,11 @@ class InfoView(ListAPIView):
|
||||
context["network"] = config("NETWORK")
|
||||
context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT"))
|
||||
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
|
||||
context["bond_size"] = float(config("BOND_SIZE"))
|
||||
context["bond_size"] = float(config("DEFAULT_BOND_SIZE"))
|
||||
|
||||
if request.user.is_authenticated:
|
||||
context["nickname"] = request.user.username
|
||||
context["referral_link"] = str(config('HOST_NAME'))+'/ref/'+str(request.user.profile.referral_code)
|
||||
context["referral_code"] = str(request.user.profile.referral_code)
|
||||
context["earned_rewards"] = request.user.profile.earned_rewards
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
@ -748,4 +805,28 @@ class PriceView(CreateAPIView):
|
||||
except:
|
||||
payload[code] = None
|
||||
|
||||
return Response(payload, status.HTTP_200_OK)
|
||||
|
||||
class LimitView(ListAPIView):
|
||||
|
||||
def get(self, request):
|
||||
|
||||
# Trade limits as BTC
|
||||
min_trade = float(config('MIN_TRADE')) / 100000000
|
||||
max_trade = float(config('MAX_TRADE')) / 100000000
|
||||
max_bondless_trade = float(config('MAX_TRADE_BONDLESS_TAKER')) / 100000000
|
||||
|
||||
payload = {}
|
||||
queryset = Currency.objects.all().order_by('currency')
|
||||
|
||||
for currency in queryset:
|
||||
code = Currency.currency_dict[str(currency.currency)]
|
||||
exchange_rate = float(currency.exchange_rate)
|
||||
payload[currency.currency] = {
|
||||
'code': code,
|
||||
'min_amount': min_trade * exchange_rate,
|
||||
'max_amount': max_trade * exchange_rate,
|
||||
'max_bondless_amount': max_bondless_trade * exchange_rate,
|
||||
}
|
||||
|
||||
return Response(payload, status.HTTP_200_OK)
|
@ -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
0
control/__init__.py
Executable file
54
control/admin.py
Executable file
54
control/admin.py
Executable 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
6
control/apps.py
Executable 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
77
control/models.py
Executable 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
112
control/tasks.py
Normal 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
3
control/tests.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
3
control/views.py
Executable file
3
control/views.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
@ -27,6 +27,7 @@ services:
|
||||
build: ./frontend
|
||||
container_name: npm-dev
|
||||
restart: always
|
||||
command: npm run build
|
||||
volumes:
|
||||
- ./frontend:/usr/src/frontend
|
||||
|
||||
|
@ -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" ]
|
||||
|
161
frontend/package-lock.json
generated
161
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)} />
|
||||
|
@ -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>
|
||||
|
60
frontend/src/components/UnsafeAlert.js
Normal file
60
frontend/src/components/UnsafeAlert.js
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
18
frontend/src/components/icons/AmbossIcon.js
Normal file
18
frontend/src/components/icons/AmbossIcon.js
Normal 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>
|
||||
);
|
||||
}
|
81
frontend/src/components/icons/RoboSatsIcon.js
Normal file
81
frontend/src/components/icons/RoboSatsIcon.js
Normal 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>
|
||||
);
|
||||
}
|
32
frontend/src/components/icons/RoboSatsNoTextIcon.js
Normal file
32
frontend/src/components/icons/RoboSatsNoTextIcon.js
Normal 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>
|
||||
);
|
||||
}
|
76
frontend/src/components/icons/RoboSatsTextIcon.js
Normal file
76
frontend/src/components/icons/RoboSatsTextIcon.js
Normal 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 |
@ -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;
|
||||
|
330
frontend/static/css/loader.css
Normal file
330
frontend/static/css/loader.css
Normal 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
@ -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>
|
||||
|
@ -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'],
|
||||
|
@ -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]
|
@ -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),
|
||||
},
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user