mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 10:56:24 +00:00
Merge branch '38-a-privacy-friendly-referral-program' into main
This commit is contained in:
commit
aa89fa603e
@ -89,6 +89,10 @@ FIAT_EXCHANGE_DURATION = 24
|
||||
PROPORTIONAL_ROUTING_FEE_LIMIT = 0.0002
|
||||
# Base flat limit fee for routing in Sats (used only when proportional is lower than this)
|
||||
MIN_FLAT_ROUTING_FEE_LIMIT = 10
|
||||
MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2
|
||||
|
||||
# Reward tip. Reward for every finished trade in the referral program (Satoshis)
|
||||
REWARD_TIP = 100
|
||||
|
||||
# Username for HTLCs escrows
|
||||
ESCROW_USERNAME = 'admin'
|
||||
|
@ -103,6 +103,7 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"avatar_tag",
|
||||
"id",
|
||||
"user_link",
|
||||
"is_referred",
|
||||
"telegram_enabled",
|
||||
"total_contracts",
|
||||
"platform_rating",
|
||||
|
@ -222,15 +222,15 @@ class LNNode:
|
||||
return payout
|
||||
|
||||
@classmethod
|
||||
def pay_invoice(cls, invoice, num_satoshis):
|
||||
"""Sends sats to buyer"""
|
||||
def pay_invoice(cls, lnpayment):
|
||||
"""Sends sats. Used for rewards payouts"""
|
||||
|
||||
fee_limit_sat = int(
|
||||
max(
|
||||
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
||||
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
|
||||
lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
||||
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
|
||||
)) # 200 ppm or 10 sats
|
||||
request = routerrpc.SendPaymentRequest(payment_request=invoice,
|
||||
request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice,
|
||||
fee_limit_sat=fee_limit_sat,
|
||||
timeout_seconds=60)
|
||||
|
||||
@ -238,15 +238,15 @@ class LNNode:
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())
|
||||
]):
|
||||
print(response)
|
||||
print(response.status)
|
||||
|
||||
# TODO ERROR HANDLING
|
||||
if response.status == 0: # Status 0 'UNKNOWN'
|
||||
# Not sure when this status happens
|
||||
pass
|
||||
|
||||
if response.status == 1: # Status 1 'IN_FLIGHT'
|
||||
return True, "In flight"
|
||||
if response.status == 3: # 4 'FAILED' ??
|
||||
|
||||
if response.status == 3: # Status 3 'FAILED'
|
||||
"""0 Payment isn't failed (yet).
|
||||
1 There are more routes to try, but the payment timeout was exceeded.
|
||||
2 All possible routes were tried and failed permanently. Or were no routes to the destination at all.
|
||||
@ -256,12 +256,10 @@ class LNNode:
|
||||
"""
|
||||
context = cls.payment_failure_context[response.failure_reason]
|
||||
return False, context
|
||||
|
||||
if response.status == 2: # STATUS 'SUCCEEDED'
|
||||
return True, None
|
||||
|
||||
# How to catch the errors like:"grpc_message":"invoice is already paid","grpc_status":6}
|
||||
# These are not in the response only printed to commandline
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
|
@ -384,8 +384,10 @@ class Logics:
|
||||
|
||||
fee_sats = order.last_satoshis * fee_fraction
|
||||
|
||||
reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0
|
||||
|
||||
if cls.is_buyer(order, user):
|
||||
invoice_amount = round(order.last_satoshis - fee_sats) # Trading fee to buyer is charged here.
|
||||
invoice_amount = round(order.last_satoshis - fee_sats - reward_tip) # Trading fee to buyer is charged here.
|
||||
|
||||
return True, {"invoice_amount": invoice_amount}
|
||||
|
||||
@ -399,10 +401,12 @@ class Logics:
|
||||
elif user == order.taker:
|
||||
fee_fraction = FEE * (1 - MAKER_FEE_SPLIT)
|
||||
|
||||
fee_sats = order.last_satoshis * fee_fraction
|
||||
fee_sats = order.last_satoshis * fee_fraction
|
||||
|
||||
reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0
|
||||
|
||||
if cls.is_seller(order, user):
|
||||
escrow_amount = round(order.last_satoshis + fee_sats) # Trading fee to seller is charged here.
|
||||
escrow_amount = round(order.last_satoshis + fee_sats + reward_tip) # Trading fee to seller is charged here.
|
||||
|
||||
return True, {"escrow_amount": escrow_amount}
|
||||
|
||||
@ -1029,12 +1033,19 @@ class Logics:
|
||||
cls.return_bond(order.taker_bond)
|
||||
cls.return_bond(order.maker_bond)
|
||||
##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||
##### Backgroun process "follow_invoices" will try to pay this invoice until success
|
||||
##### Background process "follow_invoices" will try to pay this invoice until success
|
||||
order.status = Order.Status.PAY
|
||||
order.payout.status = LNPayment.Status.FLIGHT
|
||||
order.payout.save()
|
||||
order.save()
|
||||
send_message.delay(order.id,'trade_successful')
|
||||
|
||||
# Add referral rewards (safe)
|
||||
try:
|
||||
Logics.add_rewards(order)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True, None
|
||||
|
||||
else:
|
||||
@ -1082,3 +1093,57 @@ class Logics:
|
||||
user.profile.save()
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def add_rewards(cls, order):
|
||||
'''
|
||||
This function is called when a trade is finished.
|
||||
If participants of the order were referred, the reward is given to the referees.
|
||||
'''
|
||||
|
||||
if order.maker.profile.is_referred:
|
||||
profile = order.maker.profile.referred_by
|
||||
profile.pending_rewards += int(config('REWARD_TIP'))
|
||||
profile.save()
|
||||
|
||||
if order.taker.profile.is_referred:
|
||||
profile = order.taker.profile.referred_by
|
||||
profile.pending_rewards += int(config('REWARD_TIP'))
|
||||
profile.save()
|
||||
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def withdraw_rewards(cls, user, invoice):
|
||||
|
||||
# only a user with positive withdraw balance can use this
|
||||
|
||||
if user.profile.earned_rewards < 1:
|
||||
return False, {"bad_invoice": "You have not earned rewards"}
|
||||
|
||||
num_satoshis = user.profile.earned_rewards
|
||||
reward_payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
||||
|
||||
if not reward_payout["valid"]:
|
||||
return False, reward_payout["context"]
|
||||
|
||||
lnpayment = LNPayment.objects.create(
|
||||
concept= LNPayment.Concepts.WITHREWA,
|
||||
type= LNPayment.Types.NORM,
|
||||
sender= User.objects.get(username=ESCROW_USERNAME),
|
||||
status= LNPayment.Status.VALIDI,
|
||||
receiver=user,
|
||||
invoice= invoice,
|
||||
num_satoshis= num_satoshis,
|
||||
description= reward_payout["description"],
|
||||
payment_hash= reward_payout["payment_hash"],
|
||||
created_at= reward_payout["created_at"],
|
||||
expires_at= reward_payout["expires_at"],
|
||||
)
|
||||
|
||||
if LNNode.pay_invoice(lnpayment):
|
||||
user.profile.earned_rewards = 0
|
||||
user.profile.claimed_rewards += num_satoshis
|
||||
user.profile.save()
|
||||
|
||||
return True, None
|
||||
|
||||
|
@ -60,6 +60,7 @@ class LNPayment(models.Model):
|
||||
TAKEBOND = 1, "Taker bond"
|
||||
TRESCROW = 2, "Trade escrow"
|
||||
PAYBUYER = 3, "Payment to buyer"
|
||||
WITHREWA = 4, "Withdraw rewards"
|
||||
|
||||
class Status(models.IntegerChoices):
|
||||
INVGEN = 0, "Generated"
|
||||
@ -405,6 +406,32 @@ class Profile(models.Model):
|
||||
default=False,
|
||||
null=False
|
||||
)
|
||||
|
||||
# Referral program
|
||||
is_referred = models.BooleanField(
|
||||
default=False,
|
||||
null=False
|
||||
)
|
||||
referred_by = models.ForeignKey(
|
||||
'self',
|
||||
related_name="referee",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
)
|
||||
referral_code = models.CharField(
|
||||
max_length=15,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
# Recent rewards from referred trades that will be "earned" at a later point to difficult spionage.
|
||||
pending_rewards = models.PositiveIntegerField(null=False, default=0)
|
||||
# Claimable rewards
|
||||
earned_rewards = models.PositiveIntegerField(null=False, default=0)
|
||||
# Total claimed rewards
|
||||
claimed_rewards = models.PositiveIntegerField(null=False, default=0)
|
||||
|
||||
# Disputes
|
||||
num_disputes = models.PositiveIntegerField(null=False, default=0)
|
||||
lost_disputes = models.PositiveIntegerField(null=False, default=0)
|
||||
|
@ -66,3 +66,9 @@ class UpdateOrderSerializer(serializers.Serializer):
|
||||
allow_blank=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
class ClaimRewardSerializer(serializers.Serializer):
|
||||
invoice = serializers.CharField(max_length=2000,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
default=None)
|
27
api/tasks.py
27
api/tasks.py
@ -17,9 +17,11 @@ def users_cleansing():
|
||||
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
|
||||
queryset = queryset.filter(is_staff=False) # Do not delete staff users
|
||||
|
||||
# And do not have an active trade or any past contract.
|
||||
# And do not have an active trade, any past contract or any reward.
|
||||
deleted_users = []
|
||||
for user in queryset:
|
||||
if user.profile.pending_rewards > 0 or user.profile.earned_rewards > 0 or user.profile.claimed_rewards > 0:
|
||||
continue
|
||||
if not user.profile.total_contracts == 0:
|
||||
continue
|
||||
valid, _, _ = Logics.validate_already_maker_or_taker(user)
|
||||
@ -33,6 +35,28 @@ def users_cleansing():
|
||||
}
|
||||
return results
|
||||
|
||||
@shared_task(name="give_rewards")
|
||||
def users_cleansing():
|
||||
"""
|
||||
Referral rewards go from pending to earned.
|
||||
Happens asynchronously so the referral program cannot be easily used to spy.
|
||||
"""
|
||||
from api.models import Profile
|
||||
|
||||
# Users who's last login has not been in the last 6 hours
|
||||
queryset = Profile.objects.filter(pending_rewards__gt=0)
|
||||
|
||||
# And do not have an active trade, any past contract or any reward.
|
||||
results = {}
|
||||
for profile in queryset:
|
||||
given_reward = profile.pending_rewards
|
||||
profile.earned_rewards += given_reward
|
||||
profile.pending_rewards = 0
|
||||
profile.save()
|
||||
|
||||
results[profile.user.username] = {'given_reward':given_reward,'earned_rewards':profile.earned_rewards}
|
||||
|
||||
return results
|
||||
|
||||
@shared_task(name="follow_send_payment")
|
||||
def follow_send_payment(lnpayment):
|
||||
@ -45,6 +69,7 @@ def follow_send_payment(lnpayment):
|
||||
|
||||
from api.lightning.node import LNNode, MACAROON
|
||||
from api.models import LNPayment, Order
|
||||
from api.logics import Logics
|
||||
|
||||
fee_limit_sat = int(
|
||||
max(
|
||||
|
@ -1,11 +1,9 @@
|
||||
from django.urls import path
|
||||
from .views import MakerView, OrderView, UserView, BookView, InfoView
|
||||
from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView
|
||||
|
||||
urlpatterns = [
|
||||
path("make/", MakerView.as_view()),
|
||||
path(
|
||||
"order/",
|
||||
OrderView.as_view({
|
||||
path("order/",OrderView.as_view({
|
||||
"get": "get",
|
||||
"post": "take_update_confirm_dispute_cancel"
|
||||
}),
|
||||
@ -14,4 +12,5 @@ urlpatterns = [
|
||||
path("book/", BookView.as_view()),
|
||||
# path('robot/') # Profile Info
|
||||
path("info/", InfoView.as_view()),
|
||||
path("reward/", RewardView.as_view()),
|
||||
]
|
||||
|
67
api/views.py
67
api/views.py
@ -1,6 +1,6 @@
|
||||
import os
|
||||
from re import T
|
||||
from django.db.models import query
|
||||
from django.db.models import Sum
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.generics import CreateAPIView, ListAPIView
|
||||
from rest_framework.views import APIView
|
||||
@ -9,10 +9,11 @@ from rest_framework.response import Response
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
|
||||
from api.models import LNPayment, MarketTick, Order, Currency
|
||||
from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer
|
||||
from api.models import LNPayment, MarketTick, Order, Currency, Profile
|
||||
from api.logics import Logics
|
||||
from api.messages import Telegram
|
||||
from secrets import token_urlsafe
|
||||
from api.utils import get_lnd_version, get_commit_robosats, compute_premium_percentile
|
||||
|
||||
from .nick_generator.nick_generator import NickGenerator
|
||||
@ -445,7 +446,6 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
|
||||
class UserView(APIView):
|
||||
lookup_url_kwarg = "token"
|
||||
NickGen = NickGenerator(lang="English",
|
||||
use_adv=False,
|
||||
use_adj=True,
|
||||
@ -475,12 +475,8 @@ class UserView(APIView):
|
||||
"bad_request"] = f"You are already logged in as {request.user} and have an active order"
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Does not allow this 'mistake' if the last login was sometime ago (5 minutes)
|
||||
# if request.user.last_login < timezone.now() - timedelta(minutes=5):
|
||||
# context['bad_request'] = f'You are already logged in as {request.user}'
|
||||
# return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
token = request.GET.get(self.lookup_url_kwarg)
|
||||
token = request.GET.get("token")
|
||||
ref_code = request.GET.get("ref_code")
|
||||
|
||||
# Compute token entropy
|
||||
value, counts = np.unique(list(token), return_counts=True)
|
||||
@ -514,14 +510,27 @@ class UserView(APIView):
|
||||
with open(image_path, "wb") as f:
|
||||
rh.img.save(f, format="png")
|
||||
|
||||
|
||||
|
||||
# Create new credentials and login if nickname is new
|
||||
if len(User.objects.filter(username=nickname)) == 0:
|
||||
User.objects.create_user(username=nickname,
|
||||
password=token,
|
||||
is_staff=False)
|
||||
user = authenticate(request, username=nickname, password=token)
|
||||
user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
|
||||
login(request, user)
|
||||
|
||||
context['referral_code'] = token_urlsafe(8)
|
||||
user.profile.referral_code = context['referral_code']
|
||||
user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
|
||||
|
||||
# If the ref_code was created by another robot, this robot was referred.
|
||||
queryset = Profile.objects.filter(referral_code=ref_code)
|
||||
if len(queryset) == 1:
|
||||
user.profile.is_referred = True
|
||||
user.profile.referred_by = queryset[0]
|
||||
|
||||
user.profile.save()
|
||||
return Response(context, status=status.HTTP_201_CREATED)
|
||||
|
||||
else:
|
||||
@ -635,6 +644,8 @@ class InfoView(ListAPIView):
|
||||
context["num_public_sell_orders"] = len(
|
||||
Order.objects.filter(type=Order.Types.SELL,
|
||||
status=Order.Status.PUB))
|
||||
context["book_liquidity"] = Order.objects.filter(status=Order.Status.PUB).aggregate(Sum('last_satoshis'))['last_satoshis__sum']
|
||||
context["book_liquidity"] = 0 if context["book_liquidity"] == None else context["book_liquidity"]
|
||||
|
||||
# Number of active users (logged in in last 30 minutes)
|
||||
today = datetime.today()
|
||||
@ -679,11 +690,45 @@ class InfoView(ListAPIView):
|
||||
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"))
|
||||
|
||||
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["earned_rewards"] = request.user.profile.earned_rewards
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
if not has_no_active_order:
|
||||
context["active_order_id"] = order.id
|
||||
|
||||
return Response(context, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class RewardView(CreateAPIView):
|
||||
serializer_class = ClaimRewardSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Woops! It seems you do not have a robot avatar"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
invoice = serializer.data.get("invoice")
|
||||
|
||||
valid, context = Logics.withdraw_rewards(request.user, invoice)
|
||||
|
||||
if not valid:
|
||||
context['successful_withdrawal'] = False
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
|
||||
return Response({"successful_withdrawal": True}, status.HTTP_200_OK)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import {Badge, Tooltip, TextField, ListItemAvatar, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
|
||||
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 MediaQuery from 'react-responsive'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
@ -21,14 +21,37 @@ import PasswordIcon from '@mui/icons-material/Password';
|
||||
import ContentCopy from "@mui/icons-material/ContentCopy";
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
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';
|
||||
|
||||
// pretty numbers
|
||||
function pn(x) {
|
||||
var parts = x.toString().split(".");
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return parts.join(".");
|
||||
if(x == null){
|
||||
return 'null'
|
||||
}else{
|
||||
var parts = x.toString().split(".");
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return parts.join(".");
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
export default class BottomBar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -36,8 +59,10 @@ export default class BottomBar extends Component {
|
||||
openStatsForNerds: false,
|
||||
openCommuniy: false,
|
||||
openExchangeSummary:false,
|
||||
openClaimRewards: false,
|
||||
num_public_buy_orders: 0,
|
||||
num_public_sell_orders: 0,
|
||||
book_liquidity: 0,
|
||||
active_robots_today: 0,
|
||||
maker_fee: 0,
|
||||
taker_fee: 0,
|
||||
@ -49,6 +74,12 @@ export default class BottomBar extends Component {
|
||||
profileShown: false,
|
||||
alternative_site: 'robosats...',
|
||||
node_id: '00000000',
|
||||
referral_link: 'No referral link',
|
||||
earned_rewards: 0,
|
||||
rewardInvoice: null,
|
||||
badInvoice: false,
|
||||
showRewardsSpinner: false,
|
||||
withdrawn: false,
|
||||
};
|
||||
this.getInfo();
|
||||
}
|
||||
@ -215,6 +246,30 @@ export default class BottomBar extends Component {
|
||||
this.setState({openProfile: false});
|
||||
};
|
||||
|
||||
handleSubmitInvoiceClicked=()=>{
|
||||
this.setState({
|
||||
badInvoice:false,
|
||||
showRewardsSpinner: true,
|
||||
});
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'invoice': this.state.rewardInvoice,
|
||||
}),
|
||||
};
|
||||
fetch('/api/reward/', requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => console.log(data) & this.setState({
|
||||
badInvoice:data.bad_invoice,
|
||||
openClaimRewards: data.successful_withdrawal ? false : true,
|
||||
earned_rewards: data.successful_withdrawal ? 0 : this.state.earned_rewards,
|
||||
withdrawn: data.successful_withdrawal ? true : false,
|
||||
showRewardsSpinner: false,
|
||||
}));
|
||||
}
|
||||
|
||||
dialogProfile =() =>{
|
||||
return(
|
||||
<Dialog
|
||||
@ -241,6 +296,7 @@ export default class BottomBar extends Component {
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
{this.state.active_order_id ?
|
||||
// TODO Link to router and do this.props.history.push
|
||||
@ -258,6 +314,7 @@ export default class BottomBar extends Component {
|
||||
<ListItemText primary="No active orders" secondary="Your current order"/>
|
||||
</ListItem>
|
||||
}
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PasswordIcon/>
|
||||
@ -266,21 +323,97 @@ export default class BottomBar extends Component {
|
||||
{this.props.token ?
|
||||
<TextField
|
||||
disabled
|
||||
label='Store safely'
|
||||
label='Store Safely'
|
||||
value={this.props.token }
|
||||
variant='filled'
|
||||
size='small'
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.props.token)}>
|
||||
<ContentCopy />
|
||||
</IconButton>,
|
||||
<Tooltip disableHoverListener enterTouchDelay="0" title="Copied!">
|
||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.props.token)}>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
</Tooltip>,
|
||||
}}
|
||||
/>
|
||||
:
|
||||
'Cannot remember'}
|
||||
</ListItemText>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider><Chip label='Earn Sats'/></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>,
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</ListItemText>
|
||||
:
|
||||
<form style={{maxWidth: 270}}>
|
||||
<Grid containter 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>
|
||||
</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>
|
||||
:""}
|
||||
|
||||
</List>
|
||||
</DialogContent>
|
||||
@ -465,6 +598,18 @@ bottomBarDesktop =()=>{
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
|
||||
<ListItem >
|
||||
<ListItemIcon size="small">
|
||||
<BookIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={pn(this.state.book_liquidity)+" Sats"}
|
||||
secondary="Book liquidity" />
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
|
||||
<ListItem >
|
||||
<ListItemIcon size="small">
|
||||
<SmartToyIcon/>
|
||||
|
@ -30,7 +30,8 @@ export default class HomePage extends Component {
|
||||
<Router >
|
||||
<div className='appCenter'>
|
||||
<Switch>
|
||||
<Route exact path='/' render={(props) => <UserGenPage {...this.state} setAppState={this.setAppState}/>}/>
|
||||
<Route exact path='/' render={(props) => <UserGenPage {...props} {...this.state} setAppState={this.setAppState}/>}/>
|
||||
<Route path='/ref/:refCode' render={(props) => <UserGenPage {...props} {...this.state} setAppState={this.setAppState}/>}/>
|
||||
<Route path='/make' component={MakerPage}/>
|
||||
<Route path='/book' component={BookPage}/>
|
||||
<Route path="/order/:orderId" component={OrderPage}/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, dividerClasses} from "@mui/material"
|
||||
import { Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
|
||||
import { Link } from 'react-router-dom'
|
||||
import getFlags from './getFlags'
|
||||
|
||||
|
@ -815,17 +815,17 @@ handleRatingRobosatsChange=(e)=>{
|
||||
🎉Trade finished!🥳
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
{/* <Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
What do you think of ⚡<b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>⚡?
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingUserChange} />
|
||||
</Grid>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
What do you think of 🤖<b>RoboSats</b>🤖?
|
||||
What do you think of 🤖<b>RoboSats</b>⚡?
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
@ -834,7 +834,7 @@ handleRatingRobosatsChange=(e)=>{
|
||||
{this.state.rating_platform==5 ?
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
<p>Thank you! RoboSats loves you too ❤️</p>
|
||||
<p><b>Thank you! RoboSats loves you too ❤️</b></p>
|
||||
<p>RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!</p>
|
||||
</Typography>
|
||||
</Grid>
|
||||
@ -842,8 +842,9 @@ handleRatingRobosatsChange=(e)=>{
|
||||
{this.state.rating_platform!=5 & this.state.rating_platform!=null ?
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Thank you for using Robosats! Let us know what you did not like and 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><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>
|
||||
</Typography>
|
||||
</Grid>
|
||||
: null}
|
||||
@ -902,7 +903,9 @@ handleRatingRobosatsChange=(e)=>{
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Your invoice has expires or more than 3 payments attempts have been made.
|
||||
Your invoice has expired or more than 3 payment attempts have been made.
|
||||
Muun is not recommended, <a href="https://github.com/Reckless-Satoshi/robosats/issues/44">check the list of
|
||||
compatible wallets</a>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
|
@ -32,7 +32,7 @@ export default class UserGenPage extends Component {
|
||||
tokenHasChanged: false,
|
||||
};
|
||||
|
||||
//this.props.setAppState({avatarLoaded: false, nickname: null, token: null});
|
||||
this.refCode = this.props.match.params.refCode;
|
||||
|
||||
// Checks in parent HomePage if there is already a nick and token
|
||||
// Displays the existing one
|
||||
@ -40,7 +40,7 @@ export default class UserGenPage extends Component {
|
||||
this.state = {
|
||||
nickname: this.props.nickname,
|
||||
token: this.props.token? this.props.token : null,
|
||||
avatar_url: 'static/assets/avatars/' + this.props.nickname + '.png',
|
||||
avatar_url: '/static/assets/avatars/' + this.props.nickname + '.png',
|
||||
loadingRobot: false
|
||||
}
|
||||
}
|
||||
@ -65,13 +65,13 @@ export default class UserGenPage extends Component {
|
||||
}
|
||||
|
||||
getGeneratedUser=(token)=>{
|
||||
fetch('/api/user' + '?token=' + token)
|
||||
fetch('/api/user' + '?token=' + token + '&ref_code=' + this.refCode)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.setState({
|
||||
nickname: data.nickname,
|
||||
bit_entropy: data.token_bits_entropy,
|
||||
avatar_url: 'static/assets/avatars/' + data.nickname + '.png',
|
||||
avatar_url: '/static/assets/avatars/' + data.nickname + '.png',
|
||||
shannon_entropy: data.token_shannon_entropy,
|
||||
bad_request: data.bad_request,
|
||||
found: data.found,
|
||||
|
File diff suppressed because one or more lines are too long
@ -758,6 +758,10 @@
|
||||
!*** ./node_modules/@mui/icons-material/ContentCopy.js ***!
|
||||
\*********************************************************/
|
||||
|
||||
/*!*********************************************************!*\
|
||||
!*** ./node_modules/@mui/icons-material/EmojiEvents.js ***!
|
||||
\*********************************************************/
|
||||
|
||||
/*!*********************************************************!*\
|
||||
!*** ./node_modules/@mui/icons-material/PriceChange.js ***!
|
||||
\*********************************************************/
|
||||
@ -838,6 +842,10 @@
|
||||
!*** ./node_modules/@mui/base/utils/appendOwnerState.js ***!
|
||||
\**********************************************************/
|
||||
|
||||
/*!**********************************************************!*\
|
||||
!*** ./node_modules/@mui/icons-material/PersonAddAlt.js ***!
|
||||
\**********************************************************/
|
||||
|
||||
/*!**********************************************************!*\
|
||||
!*** ./node_modules/@mui/material/Alert/alertClasses.js ***!
|
||||
\**********************************************************/
|
||||
|
@ -2,11 +2,9 @@ from django.urls import path
|
||||
from .views import index
|
||||
|
||||
urlpatterns = [
|
||||
path("", index),
|
||||
path("info/", index),
|
||||
path("login/", index),
|
||||
path("make/", index),
|
||||
path("book/", index),
|
||||
path("order/<int:orderId>", index),
|
||||
path("wait/", index),
|
||||
path("", index),
|
||||
path("ref/<refCode>", index),
|
||||
]
|
||||
|
@ -31,9 +31,13 @@ app.conf.beat_scheduler = "django_celery_beat.schedulers:DatabaseScheduler"
|
||||
|
||||
# Configure the periodic tasks
|
||||
app.conf.beat_schedule = {
|
||||
"users-cleansing": { # Cleans abandoned users every 6 hours
|
||||
"users-cleansing": { # Cleans abandoned users at midnight
|
||||
"task": "users_cleansing",
|
||||
"schedule": timedelta(hours=6),
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"give-rewards": { # Referral rewards go from 'pending' to 'earned' at midnight
|
||||
"task": "give_rewards",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"cache-market-prices": { # Cache market prices every minutes for now.
|
||||
"task": "cache_external_market_prices",
|
||||
|
Loading…
Reference in New Issue
Block a user