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

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

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

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

@ -9,7 +9,7 @@ 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.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
@ -701,3 +701,34 @@ class InfoView(ListAPIView):
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 {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 { 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 {
constructor(props) {
super(props);
@ -61,6 +77,9 @@ export default class BottomBar extends Component {
referral_link: 'No referral link',
earned_rewards: 0,
rewardInvoice: null,
badInvoice: false,
showRewardsSpinner: false,
withdrawn: false,
};
this.getInfo();
}
@ -227,8 +246,28 @@ export default class BottomBar extends Component {
this.setState({openProfile: false});
};
handleClickClaimRewards = () => {
this.setState({openClaimRewards:true});
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 =() =>{
@ -336,15 +375,17 @@ export default class BottomBar extends Component {
<Typography>{this.state.earned_rewards+" Sats"}</Typography>
</Grid>
<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>
</ListItemText>
:
<form onSubmit={this.submitRewardInvoice}>
<Grid containter alignItems="stretch" style={{ display: "flex" }} align="center">
<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"
@ -355,13 +396,25 @@ export default class BottomBar extends Component {
/>
</Grid>
<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>
</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>

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

File diff suppressed because one or more lines are too long