Merge branch '38-a-privacy-friendly-referral-program' into main

This commit is contained in:
Reckless_Satoshi 2022-03-06 10:26:07 -08:00
commit aa89fa603e
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
18 changed files with 390 additions and 61 deletions

View File

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

View File

@ -103,6 +103,7 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"avatar_tag",
"id",
"user_link",
"is_referred",
"telegram_enabled",
"total_contracts",
"platform_rating",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ***!
\**********************************************************/

View File

@ -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),
]

View File

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