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 PROPORTIONAL_ROUTING_FEE_LIMIT = 0.0002
# Base flat limit fee for routing in Sats (used only when proportional is lower than this) # 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 = 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 # Username for HTLCs escrows
ESCROW_USERNAME = 'admin' ESCROW_USERNAME = 'admin'

View File

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

View File

@ -222,15 +222,15 @@ class LNNode:
return payout return payout
@classmethod @classmethod
def pay_invoice(cls, invoice, num_satoshis): def pay_invoice(cls, lnpayment):
"""Sends sats to buyer""" """Sends sats. Used for rewards payouts"""
fee_limit_sat = int( fee_limit_sat = int(
max( max(
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
)) # 200 ppm or 10 sats )) # 200 ppm or 10 sats
request = routerrpc.SendPaymentRequest(payment_request=invoice, request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat, fee_limit_sat=fee_limit_sat,
timeout_seconds=60) timeout_seconds=60)
@ -238,15 +238,15 @@ class LNNode:
metadata=[("macaroon", metadata=[("macaroon",
MACAROON.hex()) MACAROON.hex())
]): ]):
print(response)
print(response.status)
# TODO ERROR HANDLING
if response.status == 0: # Status 0 'UNKNOWN' if response.status == 0: # Status 0 'UNKNOWN'
# Not sure when this status happens
pass pass
if response.status == 1: # Status 1 'IN_FLIGHT' if response.status == 1: # Status 1 'IN_FLIGHT'
return True, "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). """0 Payment isn't failed (yet).
1 There are more routes to try, but the payment timeout was exceeded. 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. 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] context = cls.payment_failure_context[response.failure_reason]
return False, context return False, context
if response.status == 2: # STATUS 'SUCCEEDED' if response.status == 2: # STATUS 'SUCCEEDED'
return True, None 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 return False
@classmethod @classmethod

View File

@ -384,8 +384,10 @@ class Logics:
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_buyer(order, user): 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} return True, {"invoice_amount": invoice_amount}
@ -399,10 +401,12 @@ class Logics:
elif user == order.taker: elif user == order.taker:
fee_fraction = FEE * (1 - MAKER_FEE_SPLIT) 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): 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} return True, {"escrow_amount": escrow_amount}
@ -1029,12 +1033,19 @@ class Logics:
cls.return_bond(order.taker_bond) cls.return_bond(order.taker_bond)
cls.return_bond(order.maker_bond) cls.return_bond(order.maker_bond)
##### !!! KEY LINE - PAYS THE BUYER INVOICE !!! ##### !!! 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.status = Order.Status.PAY
order.payout.status = LNPayment.Status.FLIGHT order.payout.status = LNPayment.Status.FLIGHT
order.payout.save() order.payout.save()
order.save() order.save()
send_message.delay(order.id,'trade_successful') send_message.delay(order.id,'trade_successful')
# Add referral rewards (safe)
try:
Logics.add_rewards(order)
except:
pass
return True, None return True, None
else: else:
@ -1082,3 +1093,57 @@ class Logics:
user.profile.save() user.profile.save()
return True, None 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" TAKEBOND = 1, "Taker bond"
TRESCROW = 2, "Trade escrow" TRESCROW = 2, "Trade escrow"
PAYBUYER = 3, "Payment to buyer" PAYBUYER = 3, "Payment to buyer"
WITHREWA = 4, "Withdraw rewards"
class Status(models.IntegerChoices): class Status(models.IntegerChoices):
INVGEN = 0, "Generated" INVGEN = 0, "Generated"
@ -405,6 +406,32 @@ class Profile(models.Model):
default=False, default=False,
null=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 # Disputes
num_disputes = models.PositiveIntegerField(null=False, default=0) num_disputes = models.PositiveIntegerField(null=False, default=0)
lost_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, allow_blank=True,
default=None, 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 = User.objects.filter(~Q(last_login__range=active_time_range))
queryset = queryset.filter(is_staff=False) # Do not delete staff users 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 = [] deleted_users = []
for user in queryset: 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: if not user.profile.total_contracts == 0:
continue continue
valid, _, _ = Logics.validate_already_maker_or_taker(user) valid, _, _ = Logics.validate_already_maker_or_taker(user)
@ -33,6 +35,28 @@ def users_cleansing():
} }
return results 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") @shared_task(name="follow_send_payment")
def follow_send_payment(lnpayment): def follow_send_payment(lnpayment):
@ -45,6 +69,7 @@ def follow_send_payment(lnpayment):
from api.lightning.node import LNNode, MACAROON from api.lightning.node import LNNode, MACAROON
from api.models import LNPayment, Order from api.models import LNPayment, Order
from api.logics import Logics
fee_limit_sat = int( fee_limit_sat = int(
max( max(

View File

@ -1,11 +1,9 @@
from django.urls import path from django.urls import path
from .views import MakerView, OrderView, UserView, BookView, InfoView from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView
urlpatterns = [ urlpatterns = [
path("make/", MakerView.as_view()), path("make/", MakerView.as_view()),
path( path("order/",OrderView.as_view({
"order/",
OrderView.as_view({
"get": "get", "get": "get",
"post": "take_update_confirm_dispute_cancel" "post": "take_update_confirm_dispute_cancel"
}), }),
@ -14,4 +12,5 @@ urlpatterns = [
path("book/", BookView.as_view()), path("book/", BookView.as_view()),
# path('robot/') # Profile Info # path('robot/') # Profile Info
path("info/", InfoView.as_view()), path("info/", InfoView.as_view()),
path("reward/", RewardView.as_view()),
] ]

View File

@ -1,6 +1,6 @@
import os import os
from re import T 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 import status, viewsets
from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.generics import CreateAPIView, ListAPIView
from rest_framework.views import APIView 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 import authenticate, login, logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer
from api.models import LNPayment, MarketTick, Order, Currency from api.models import LNPayment, MarketTick, Order, Currency, Profile
from api.logics import Logics from api.logics import Logics
from api.messages import Telegram from api.messages import Telegram
from secrets import token_urlsafe
from api.utils import get_lnd_version, get_commit_robosats, compute_premium_percentile from api.utils import get_lnd_version, get_commit_robosats, compute_premium_percentile
from .nick_generator.nick_generator import NickGenerator from .nick_generator.nick_generator import NickGenerator
@ -445,7 +446,6 @@ class OrderView(viewsets.ViewSet):
class UserView(APIView): class UserView(APIView):
lookup_url_kwarg = "token"
NickGen = NickGenerator(lang="English", NickGen = NickGenerator(lang="English",
use_adv=False, use_adv=False,
use_adj=True, 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" "bad_request"] = f"You are already logged in as {request.user} and have an active order"
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
# Does not allow this 'mistake' if the last login was sometime ago (5 minutes) token = request.GET.get("token")
# if request.user.last_login < timezone.now() - timedelta(minutes=5): ref_code = request.GET.get("ref_code")
# 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)
# Compute token entropy # Compute token entropy
value, counts = np.unique(list(token), return_counts=True) value, counts = np.unique(list(token), return_counts=True)
@ -514,14 +510,27 @@ class UserView(APIView):
with open(image_path, "wb") as f: with open(image_path, "wb") as f:
rh.img.save(f, format="png") rh.img.save(f, format="png")
# Create new credentials and login if nickname is new # Create new credentials and login if nickname is new
if len(User.objects.filter(username=nickname)) == 0: if len(User.objects.filter(username=nickname)) == 0:
User.objects.create_user(username=nickname, User.objects.create_user(username=nickname,
password=token, password=token,
is_staff=False) is_staff=False)
user = authenticate(request, username=nickname, password=token) user = authenticate(request, username=nickname, password=token)
user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
login(request, user) 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) return Response(context, status=status.HTTP_201_CREATED)
else: else:
@ -635,6 +644,8 @@ class InfoView(ListAPIView):
context["num_public_sell_orders"] = len( context["num_public_sell_orders"] = len(
Order.objects.filter(type=Order.Types.SELL, Order.objects.filter(type=Order.Types.SELL,
status=Order.Status.PUB)) 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) # Number of active users (logged in in last 30 minutes)
today = datetime.today() today = datetime.today()
@ -679,11 +690,45 @@ class InfoView(ListAPIView):
context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT")) context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT"))
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT"))) context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
context["bond_size"] = float(config("BOND_SIZE")) context["bond_size"] = float(config("BOND_SIZE"))
if request.user.is_authenticated: if request.user.is_authenticated:
context["nickname"] = request.user.username context["nickname"] = request.user.username
context["referral_link"] = str(config('HOST_NAME'))+'/ref/'+str(request.user.profile.referral_code)
context["earned_rewards"] = request.user.profile.earned_rewards
has_no_active_order, _, order = Logics.validate_already_maker_or_taker( has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user) request.user)
if not has_no_active_order: if not has_no_active_order:
context["active_order_id"] = order.id context["active_order_id"] = order.id
return Response(context, status.HTTP_200_OK) 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 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 MediaQuery from 'react-responsive'
import { Link } from 'react-router-dom' 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 ContentCopy from "@mui/icons-material/ContentCopy";
import DnsIcon from '@mui/icons-material/Dns'; import DnsIcon from '@mui/icons-material/Dns';
import WebIcon from '@mui/icons-material/Web'; 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 // pretty numbers
function pn(x) { function pn(x) {
var parts = x.toString().split("."); if(x == null){
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); return 'null'
return parts.join("."); }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 { export default class BottomBar extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -36,8 +59,10 @@ export default class BottomBar extends Component {
openStatsForNerds: false, openStatsForNerds: false,
openCommuniy: false, openCommuniy: false,
openExchangeSummary:false, openExchangeSummary:false,
openClaimRewards: false,
num_public_buy_orders: 0, num_public_buy_orders: 0,
num_public_sell_orders: 0, num_public_sell_orders: 0,
book_liquidity: 0,
active_robots_today: 0, active_robots_today: 0,
maker_fee: 0, maker_fee: 0,
taker_fee: 0, taker_fee: 0,
@ -49,6 +74,12 @@ export default class BottomBar extends Component {
profileShown: false, profileShown: false,
alternative_site: 'robosats...', alternative_site: 'robosats...',
node_id: '00000000', node_id: '00000000',
referral_link: 'No referral link',
earned_rewards: 0,
rewardInvoice: null,
badInvoice: false,
showRewardsSpinner: false,
withdrawn: false,
}; };
this.getInfo(); this.getInfo();
} }
@ -215,6 +246,30 @@ export default class BottomBar extends Component {
this.setState({openProfile: false}); 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 =() =>{ dialogProfile =() =>{
return( return(
<Dialog <Dialog
@ -241,6 +296,7 @@ export default class BottomBar extends Component {
/> />
</ListItemAvatar> </ListItemAvatar>
</ListItem> </ListItem>
<Divider/> <Divider/>
{this.state.active_order_id ? {this.state.active_order_id ?
// TODO Link to router and do this.props.history.push // 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"/> <ListItemText primary="No active orders" secondary="Your current order"/>
</ListItem> </ListItem>
} }
<ListItem> <ListItem>
<ListItemIcon> <ListItemIcon>
<PasswordIcon/> <PasswordIcon/>
@ -266,21 +323,97 @@ export default class BottomBar extends Component {
{this.props.token ? {this.props.token ?
<TextField <TextField
disabled disabled
label='Store safely' label='Store Safely'
value={this.props.token } value={this.props.token }
variant='filled' variant='filled'
size='small' size='small'
InputProps={{ InputProps={{
endAdornment: endAdornment:
<IconButton onClick= {()=>navigator.clipboard.writeText(this.props.token)}> <Tooltip disableHoverListener enterTouchDelay="0" title="Copied!">
<ContentCopy /> <IconButton onClick= {()=>navigator.clipboard.writeText(this.props.token)}>
</IconButton>, <ContentCopy />
</IconButton>
</Tooltip>,
}} }}
/> />
: :
'Cannot remember'} 'Cannot remember'}
</ListItemText> </ListItemText>
</ListItem> </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> </List>
</DialogContent> </DialogContent>
@ -465,6 +598,18 @@ bottomBarDesktop =()=>{
</ListItem> </ListItem>
<Divider/> <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 > <ListItem >
<ListItemIcon size="small"> <ListItemIcon size="small">
<SmartToyIcon/> <SmartToyIcon/>

View File

@ -30,7 +30,8 @@ export default class HomePage extends Component {
<Router > <Router >
<div className='appCenter'> <div className='appCenter'>
<Switch> <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='/make' component={MakerPage}/>
<Route path='/book' component={BookPage}/> <Route path='/book' component={BookPage}/>
<Route path="/order/:orderId" component={OrderPage}/> <Route path="/order/:orderId" component={OrderPage}/>

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; 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 { Link } from 'react-router-dom'
import getFlags from './getFlags' import getFlags from './getFlags'

View File

@ -815,17 +815,17 @@ handleRatingRobosatsChange=(e)=>{
🎉Trade finished!🥳 🎉Trade finished!🥳
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> {/* <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
What do you think of <b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>? What do you think of <b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>?
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingUserChange} /> <Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingUserChange} />
</Grid> </Grid> */}
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
What do you think of 🤖<b>RoboSats</b>🤖? What do you think of 🤖<b>RoboSats</b>?
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -834,7 +834,7 @@ handleRatingRobosatsChange=(e)=>{
{this.state.rating_platform==5 ? {this.state.rating_platform==5 ?
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
<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> <p>RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!</p>
</Typography> </Typography>
</Grid> </Grid>
@ -842,8 +842,9 @@ handleRatingRobosatsChange=(e)=>{
{this.state.rating_platform!=5 & this.state.rating_platform!=null ? {this.state.rating_platform!=5 & this.state.rating_platform!=null ?
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
Thank you for using Robosats! Let us know what you did not like and how the platform could improve <p><b>Thank you for using Robosats!</b></p>
(<a href="https://t.me/robosats">Telegram</a> / <a href="https://github.com/Reckless-Satoshi/robosats/issues">Github</a>) <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> </Typography>
</Grid> </Grid>
: null} : null}
@ -902,7 +903,9 @@ handleRatingRobosatsChange=(e)=>{
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
Your invoice has 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> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">

View File

@ -32,7 +32,7 @@ export default class UserGenPage extends Component {
tokenHasChanged: false, 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 // Checks in parent HomePage if there is already a nick and token
// Displays the existing one // Displays the existing one
@ -40,7 +40,7 @@ export default class UserGenPage extends Component {
this.state = { this.state = {
nickname: this.props.nickname, nickname: this.props.nickname,
token: this.props.token? this.props.token : null, 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 loadingRobot: false
} }
} }
@ -65,13 +65,13 @@ export default class UserGenPage extends Component {
} }
getGeneratedUser=(token)=>{ getGeneratedUser=(token)=>{
fetch('/api/user' + '?token=' + token) fetch('/api/user' + '?token=' + token + '&ref_code=' + this.refCode)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
this.setState({ this.setState({
nickname: data.nickname, nickname: data.nickname,
bit_entropy: data.token_bits_entropy, 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, shannon_entropy: data.token_shannon_entropy,
bad_request: data.bad_request, bad_request: data.bad_request,
found: data.found, 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/ContentCopy.js ***!
\*********************************************************/ \*********************************************************/
/*!*********************************************************!*\
!*** ./node_modules/@mui/icons-material/EmojiEvents.js ***!
\*********************************************************/
/*!*********************************************************!*\ /*!*********************************************************!*\
!*** ./node_modules/@mui/icons-material/PriceChange.js ***! !*** ./node_modules/@mui/icons-material/PriceChange.js ***!
\*********************************************************/ \*********************************************************/
@ -838,6 +842,10 @@
!*** ./node_modules/@mui/base/utils/appendOwnerState.js ***! !*** ./node_modules/@mui/base/utils/appendOwnerState.js ***!
\**********************************************************/ \**********************************************************/
/*!**********************************************************!*\
!*** ./node_modules/@mui/icons-material/PersonAddAlt.js ***!
\**********************************************************/
/*!**********************************************************!*\ /*!**********************************************************!*\
!*** ./node_modules/@mui/material/Alert/alertClasses.js ***! !*** ./node_modules/@mui/material/Alert/alertClasses.js ***!
\**********************************************************/ \**********************************************************/

View File

@ -2,11 +2,9 @@ from django.urls import path
from .views import index from .views import index
urlpatterns = [ urlpatterns = [
path("", index),
path("info/", index),
path("login/", index),
path("make/", index), path("make/", index),
path("book/", index), path("book/", index),
path("order/<int:orderId>", 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 # Configure the periodic tasks
app.conf.beat_schedule = { app.conf.beat_schedule = {
"users-cleansing": { # Cleans abandoned users every 6 hours "users-cleansing": { # Cleans abandoned users at midnight
"task": "users_cleansing", "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. "cache-market-prices": { # Cache market prices every minutes for now.
"task": "cache_external_market_prices", "task": "cache_external_market_prices",