From 1a6aa8a9eeb58c272541c7d1853be52938f2b7c5 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 4 Feb 2022 10:07:09 -0800 Subject: [PATCH] Improve failure logics. Add platform rating. --- api/admin.py | 2 +- api/logics.py | 21 +++++-- api/management/commands/follow_invoices.py | 4 ++ api/models.py | 5 ++ api/serializers.py | 2 +- api/views.py | 9 ++- frontend/src/components/TradeBox.js | 72 ++++++++++++++++++++-- 7 files changed, 99 insertions(+), 16 deletions(-) diff --git a/api/admin.py b/api/admin.py index 0e0d1793..5f99723f 100644 --- a/api/admin.py +++ b/api/admin.py @@ -41,7 +41,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): @admin.register(Profile) class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('avatar_tag','id','user_link','total_contracts','total_ratings','avg_rating','num_disputes','lost_disputes') + list_display = ('avatar_tag','id','user_link','total_contracts','platform_rating','total_ratings','avg_rating','num_disputes','lost_disputes') list_display_links = ('avatar_tag','id') change_links =['user'] readonly_fields = ['avatar_tag'] diff --git a/api/logics.py b/api/logics.py index f88ce41d..c71f0c5d 100644 --- a/api/logics.py +++ b/api/logics.py @@ -302,7 +302,7 @@ class Logics(): return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'} if not order.taker_bond: return False, {'bad_request':'Wait for your order to be taken.'} - if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED): + if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED) and not order.status == Order.Status.FAI: return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'} num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount'] @@ -347,9 +347,12 @@ class Logics(): # If the order status is 'Failed Routing'. Retry payment. if order.status == Order.Status.FAI: - # Double check the escrow is settled. if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): - follow_send_payment(order.payout) + order.status = Order.Status.PAY + order.payout.status = LNPayment.Status.FLIGHT + order.payout.routing_attempts = 0 + order.payout.save() + order.save() order.save() return True, None @@ -526,7 +529,7 @@ class Logics(): order.last_satoshis = cls.satoshis_now(order) bond_satoshis = int(order.last_satoshis * BOND_SIZE) - description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel." + description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally." # Gen hold Invoice hold_payment = LNNode.gen_hold_invoice(bond_satoshis, @@ -607,7 +610,7 @@ class Logics(): bond_satoshis = int(order.last_satoshis * BOND_SIZE) pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling' description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}" - + " - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel.") + + " - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally.") # Gen hold Invoice hold_payment = LNNode.gen_hold_invoice(bond_satoshis, @@ -672,7 +675,7 @@ class Logics(): # If there was no taker_bond object yet, generate one escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked - description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment." + description = f"RoboSats - Escrow amount for '{str(order)}' - It WILL FREEZE IN YOUR WALLET. It will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment." # Gen hold Invoice hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, @@ -834,4 +837,10 @@ class Logics(): else: return False, {'bad_request':'You cannot rate your counterparty yet.'} + return True, None + + @classmethod + def rate_platform(cls, user, rating): + user.profile.platform_rating = rating + user.profile.save() return True, None \ No newline at end of file diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py index 2a7e1abf..0fb8025a 100644 --- a/api/management/commands/follow_invoices.py +++ b/api/management/commands/follow_invoices.py @@ -137,6 +137,10 @@ class Command(BaseCommand): for lnpayment in queryset: success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency. + # If failed, reset mision control. (This won't scale well, just a temporary fix) + if not success: + LNNode.resetmc() + # If already 3 attempts and last failed. Make it expire (ask for a new invoice) an reset attempts. if not success and lnpayment.routing_attempts == 3: lnpayment.status = LNPayment.Status.EXPIRE diff --git a/api/models.py b/api/models.py index e45707e6..aff5e9ec 100644 --- a/api/models.py +++ b/api/models.py @@ -169,6 +169,8 @@ class Order(models.Model): # ratings maker_rated = models.BooleanField(default=False, null=False) taker_rated = models.BooleanField(default=False, null=False) + maker_platform_rated = models.BooleanField(default=False, null=False) + taker_platform_rated = models.BooleanField(default=False, null=False) t_to_expire = { 0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond' @@ -230,6 +232,9 @@ class Profile(models.Model): # Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond) penalty_expiration = models.DateTimeField(null=True,default=None, blank=True) + # Platform rate + platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True) + @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: diff --git a/api/serializers.py b/api/serializers.py index 88997f3d..aa569c26 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -14,5 +14,5 @@ class MakeOrderSerializer(serializers.ModelSerializer): class UpdateOrderSerializer(serializers.Serializer): invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None) statement = serializers.CharField(max_length=10000, allow_null=True, allow_blank=True, default=None) - action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate'), allow_null=False) + action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate_user','rate_platform'), allow_null=False) rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None) \ No newline at end of file diff --git a/api/views.py b/api/views.py index b0b8e1d2..2329707f 100644 --- a/api/views.py +++ b/api/views.py @@ -275,7 +275,7 @@ class OrderView(viewsets.ViewSet): order = Order.objects.get(id=order_id) # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' - # 6)'submit_statement' (in dispute), 7)'rate' (counterparty) + # 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform' action = serializer.data.get('action') invoice = serializer.data.get('invoice') statement = serializer.data.get('statement') @@ -323,10 +323,15 @@ class OrderView(viewsets.ViewSet): if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 6) If action is rate - elif action == 'rate' and rating: + elif action == 'rate_user' and rating: valid, context = Logics.rate_counterparty(order,request.user, rating) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) + # 6) If action is rate_platform + elif action == 'rate_platform' and rating: + valid, context = Logics.rate_platform(request.user, rating) + if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) + # If nothing of the above... something else is going on. Probably not allowed! else: return Response( diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 074ad77b..59048557 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -1,7 +1,7 @@ import React, { Component } from "react"; -import { IconButton, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material" +import { IconButton, Paper, Rating, Button, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material" import QRCode from "react-qr-code"; -import Countdown from 'react-countdown'; +import Countdown, { zeroPad} from 'react-countdown'; import Chat from "./Chat" import MediaQuery from 'react-responsive' import QrReader from 'react-qr-reader' @@ -549,12 +549,31 @@ export default class TradeBox extends Component { .then((data) => this.props.completeSetState(data)); } -handleRatingChange=(e)=>{ +handleRatingUserChange=(e)=>{ const requestOptions = { method: 'POST', headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, body: JSON.stringify({ - 'action': "rate", + 'action': "rate_user", + 'rating': e.target.value, + }), + }; + fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions) + .then((response) => response.json()) + .then((data) => this.props.completeSetState(data)); +} + +handleRatingRobosatsChange=(e)=>{ + if (this.state.rating_platform != null){ + return null + } + this.setState({rating_platform:e.target.value}); + + const requestOptions = { + method: 'POST', + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, + body: JSON.stringify({ + 'action': "rate_platform", 'rating': e.target.value, }), }; @@ -672,12 +691,36 @@ handleRatingChange=(e)=>{ - What do you think of {this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}? + What do you think of ⚡{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}⚡? - + + + + What do you think of 🤖RoboSats🤖? + + + + + + {this.state.rating_platform==5 ? + + +

Thank you! RoboSats loves you too ❤️

+

RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!

+
+
+ : null} + {this.state.rating_platform!=5 & this.state.rating_platform!=null ? + + + Thank you for using Robosats! Let us know what you did not like and how the platform could improve + (Telegram / Github) + + + : null} @@ -698,11 +741,28 @@ handleRatingChange=(e)=>{ RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must be online in order to receive payments. +
+ + + ) } + // Countdown Renderer callback with condition + countdownRenderer = ({ minutes, seconds, completed }) => { + if (completed) { + // Render a completed state + return (
Retrying!
); + + } else { + return ( + {zeroPad(minutes)}m {zeroPad(seconds)}s + ); + } + }; + showRoutingFailed=()=>{ // TODO If it has failed 3 times, ask for a new invoice. if(this.props.data.invoice_expired){