Implement withdraw rewards (backend & frontend)

This commit is contained in:
Reckless_Satoshi 2022-03-06 08:08:28 -08:00
parent 794d1e8f1b
commit 255dae188d
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
9 changed files with 157 additions and 29 deletions

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

@ -1112,3 +1112,38 @@ class Logics:
return 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"

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

@ -1,5 +1,5 @@
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()),
@ -12,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

@ -9,7 +9,7 @@ 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, Profile 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
@ -701,3 +701,34 @@ class InfoView(ListAPIView):
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 {Chip, Badge, Tooltip, TextField, ListItemAvatar, Button, 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'
@ -36,6 +36,22 @@ function pn(x) {
} }
} }
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);
@ -61,6 +77,9 @@ export default class BottomBar extends Component {
referral_link: 'No referral link', referral_link: 'No referral link',
earned_rewards: 0, earned_rewards: 0,
rewardInvoice: null, rewardInvoice: null,
badInvoice: false,
showRewardsSpinner: false,
withdrawn: false,
}; };
this.getInfo(); this.getInfo();
} }
@ -227,8 +246,28 @@ export default class BottomBar extends Component {
this.setState({openProfile: false}); this.setState({openProfile: false});
}; };
handleClickClaimRewards = () => { handleSubmitInvoiceClicked=()=>{
this.setState({openClaimRewards:true}); 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 =() =>{
@ -336,15 +375,17 @@ export default class BottomBar extends Component {
<Typography>{this.state.earned_rewards+" Sats"}</Typography> <Typography>{this.state.earned_rewards+" Sats"}</Typography>
</Grid> </Grid>
<Grid item xs={3}> <Grid item xs={3}>
<Button disabled={this.state.earned_rewards==0? true : false} onCLick={this.handleClickClaimRewards} variant="contained" size="small">Claim</Button> <Button disabled={this.state.earned_rewards==0? true : false} onClick={() => this.setState({openClaimRewards:true})} variant="contained" size="small">Claim</Button>
</Grid> </Grid>
</Grid> </Grid>
</ListItemText> </ListItemText>
: :
<form onSubmit={this.submitRewardInvoice}> <form style={{maxWidth: 270}}>
<Grid containter alignItems="stretch" style={{ display: "flex" }} align="center"> <Grid containter alignItems="stretch" style={{ display: "flex"}} align="center">
<Grid item alignItems="stretch" style={{ display: "flex" }} align="center"> <Grid item alignItems="stretch" style={{ display: "flex" }} align="center">
<TextField <TextField
error={this.state.badInvoice}
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
label={"Invoice for " + this.state.earned_rewards + " Sats"} label={"Invoice for " + this.state.earned_rewards + " Sats"}
//variant="standard" //variant="standard"
size="small" size="small"
@ -355,13 +396,25 @@ export default class BottomBar extends Component {
/> />
</Grid> </Grid>
<Grid item alignItems="stretch" style={{ display: "flex" }}> <Grid item alignItems="stretch" style={{ display: "flex" }}>
<Button type="submit" variant="contained" color="primary" size="small" > Send </Button> <Button sx={{maxHeight:38}} onClick={this.handleSubmitInvoiceClicked} variant="contained" color="primary" size="small" > Submit </Button>
</Grid> </Grid>
</Grid> </Grid>
</form> </form>
} }
</ListItem> </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>

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

File diff suppressed because one or more lines are too long