Improve failure logics. Add platform rating.

This commit is contained in:
Reckless_Satoshi 2022-02-04 10:07:09 -08:00
parent 3f6731d3e0
commit 1a6aa8a9ee
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
7 changed files with 99 additions and 16 deletions

View File

@ -41,7 +41,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@admin.register(Profile) @admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): 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') list_display_links = ('avatar_tag','id')
change_links =['user'] change_links =['user']
readonly_fields = ['avatar_tag'] readonly_fields = ['avatar_tag']

View File

@ -302,7 +302,7 @@ class Logics():
return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'} return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
if not order.taker_bond: if not order.taker_bond:
return False, {'bad_request':'Wait for your order to be taken.'} 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.'} return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'}
num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount'] 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 the order status is 'Failed Routing'. Retry payment.
if order.status == Order.Status.FAI: if order.status == Order.Status.FAI:
# Double check the escrow is settled.
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): 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() order.save()
return True, None return True, None
@ -526,7 +529,7 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order) order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE) 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 # Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
@ -607,7 +610,7 @@ class Logics():
bond_satoshis = int(order.last_satoshis * BOND_SIZE) bond_satoshis = int(order.last_satoshis * BOND_SIZE)
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling' 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)]}" 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 # Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
@ -672,7 +675,7 @@ class Logics():
# If there was no taker_bond object yet, generate one # If there was no taker_bond object yet, generate one
escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked 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 # Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
@ -835,3 +838,9 @@ class Logics():
return False, {'bad_request':'You cannot rate your counterparty yet.'} return False, {'bad_request':'You cannot rate your counterparty yet.'}
return True, None return True, None
@classmethod
def rate_platform(cls, user, rating):
user.profile.platform_rating = rating
user.profile.save()
return True, None

View File

@ -137,6 +137,10 @@ class Command(BaseCommand):
for lnpayment in queryset: for lnpayment in queryset:
success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency. 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 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: if not success and lnpayment.routing_attempts == 3:
lnpayment.status = LNPayment.Status.EXPIRE lnpayment.status = LNPayment.Status.EXPIRE

View File

@ -169,6 +169,8 @@ class Order(models.Model):
# ratings # ratings
maker_rated = models.BooleanField(default=False, null=False) maker_rated = models.BooleanField(default=False, null=False)
taker_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 = { t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond' 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 (only used then taking/cancelling repeatedly orders in the book before comitting bond)
penalty_expiration = models.DateTimeField(null=True,default=None, blank=True) 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) @receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs): def create_user_profile(sender, instance, created, **kwargs):
if created: if created:

View File

@ -14,5 +14,5 @@ class MakeOrderSerializer(serializers.ModelSerializer):
class UpdateOrderSerializer(serializers.Serializer): class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None) 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) 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) rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)

View File

@ -275,7 +275,7 @@ class OrderView(viewsets.ViewSet):
order = Order.objects.get(id=order_id) order = Order.objects.get(id=order_id)
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' # 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') action = serializer.data.get('action')
invoice = serializer.data.get('invoice') invoice = serializer.data.get('invoice')
statement = serializer.data.get('statement') statement = serializer.data.get('statement')
@ -323,10 +323,15 @@ class OrderView(viewsets.ViewSet):
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate # 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) valid, context = Logics.rate_counterparty(order,request.user, rating)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) 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! # If nothing of the above... something else is going on. Probably not allowed!
else: else:
return Response( return Response(

View File

@ -1,7 +1,7 @@
import React, { Component } from "react"; 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 QRCode from "react-qr-code";
import Countdown from 'react-countdown'; import Countdown, { zeroPad} from 'react-countdown';
import Chat from "./Chat" import Chat from "./Chat"
import MediaQuery from 'react-responsive' import MediaQuery from 'react-responsive'
import QrReader from 'react-qr-reader' import QrReader from 'react-qr-reader'
@ -549,12 +549,31 @@ export default class TradeBox extends Component {
.then((data) => this.props.completeSetState(data)); .then((data) => this.props.completeSetState(data));
} }
handleRatingChange=(e)=>{ handleRatingUserChange=(e)=>{
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({ 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, 'rating': e.target.value,
}), }),
}; };
@ -672,12 +691,36 @@ handleRatingChange=(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">
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={2} size="large" onChange={this.handleRatingChange} /> <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>🤖?
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingRobosatsChange} />
</Grid>
{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>RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!</p>
</Typography>
</Grid>
: null}
{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>)
</Typography>
</Grid>
: null}
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button color='primary' onClick={() => {this.props.push('/')}}>Start Again</Button> <Button color='primary' onClick={() => {this.props.push('/')}}>Start Again</Button>
</Grid> </Grid>
@ -698,11 +741,28 @@ handleRatingChange=(e)=>{
RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must
be online in order to receive payments. be online in order to receive payments.
</Typography> </Typography>
<br/>
<Grid item xs={12} align="center">
<CircularProgress/>
</Grid>
</Grid> </Grid>
</Grid> </Grid>
) )
} }
// Countdown Renderer callback with condition
countdownRenderer = ({ minutes, seconds, completed }) => {
if (completed) {
// Render a completed state
return (<div align="center"><span> Retrying! </span><br/><CircularProgress/></div> );
} else {
return (
<span>{zeroPad(minutes)}m {zeroPad(seconds)}s </span>
);
}
};
showRoutingFailed=()=>{ showRoutingFailed=()=>{
// TODO If it has failed 3 times, ask for a new invoice. // TODO If it has failed 3 times, ask for a new invoice.
if(this.props.data.invoice_expired){ if(this.props.data.invoice_expired){