diff --git a/api/lightning/node.py b/api/lightning/node.py
index c6214eb2..b0da223c 100644
--- a/api/lightning/node.py
+++ b/api/lightning/node.py
@@ -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
diff --git a/api/logics.py b/api/logics.py
index 2356551a..8d0d22d0 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -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
+
diff --git a/api/models.py b/api/models.py
index 11047c8e..797cbbcd 100644
--- a/api/models.py
+++ b/api/models.py
@@ -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"
diff --git a/api/serializers.py b/api/serializers.py
index 18b13319..c8e97e19 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -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)
\ No newline at end of file
diff --git a/api/urls.py b/api/urls.py
index 70a5e2e2..50197bd4 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -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()),
]
diff --git a/api/views.py b/api/views.py
index 5f735f4a..4a5c95e8 100644
--- a/api/views.py
+++ b/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)
diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js
index 006c338a..30587f6a 100644
--- a/frontend/src/components/BottomBar.js
+++ b/frontend/src/components/BottomBar.js
@@ -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 {