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