mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-31 02:21:35 +00:00
Implement withdraw rewards (backend & frontend)
This commit is contained in:
parent
794d1e8f1b
commit
255dae188d
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
@ -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()),
|
||||
]
|
||||
|
33
api/views.py
33
api/views.py
@ -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)
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user