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 { {this.state.earned_rewards+" Sats"} - Claim + this.setState({openClaimRewards:true})} variant="contained" size="small">Claim : -
Thank you! RoboSats loves you too ❤️
RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!
Thank you for using Robosats!
Let us know how the platform could improve + (Telegram / Github)