Add logics for payment retry, first iteration.

This commit is contained in:
Reckless_Satoshi 2022-01-24 14:53:55 -08:00
parent 25ab5fdf2e
commit 2d1a2e4c5c
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
6 changed files with 210 additions and 115 deletions

View File

@ -295,6 +295,7 @@ class Logics():
concept = LNPayment.Concepts.PAYBUYER,
type = LNPayment.Types.NORM,
sender = User.objects.get(username=ESCROW_USERNAME),
order_paid = order, # In case this user has other buyer_invoices, update the one related to this order.
receiver= user,
# if there is a LNPayment matching these above, it updates that one with defaults below.
defaults={
@ -321,6 +322,10 @@ class Logics():
else:
order.status = Order.Status.WFE
# If the order status is 'Failed Routing'. Retry payment.
if order.status == Order.Status.FAI:
follow_send_payment(order.buyer_invoice)
order.save()
return True, None
@ -766,10 +771,7 @@ class Logics():
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
is_payed, context = follow_send_payment(order.buyer_invoice) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
if is_payed:
order.status = Order.Status.SUC
order.buyer_invoice.status = LNPayment.Status.SUCCED
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
# RETURN THE BONDS
# RETURN THE BONDS // Probably best also do it even if payment failed
cls.return_bond(order.taker_bond)
cls.return_bond(order.maker_bond)
order.save()

View File

@ -1,10 +1,12 @@
from django.core.management.base import BaseCommand, CommandError
from api.lightning.node import LNNode
from api.tasks import follow_send_payment
from api.models import LNPayment, Order
from api.logics import Logics
from django.utils import timezone
from datetime import timedelta
from decouple import config
from base64 import b64decode
import time
@ -12,7 +14,30 @@ import time
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
class Command(BaseCommand):
'''
help = 'Follows all active hold invoices'
rest = 5 # seconds between consecutive checks for invoice updates
def handle(self, *args, **options):
''' Infinite loop to check invoices and retry payments.
ever mind database locked error, keep going, print out'''
while True:
time.sleep(self.rest)
try:
self.follow_hold_invoices()
self.retry_payments()
except Exception as e:
if 'database is locked' in str(e):
self.stdout.write('database is locked')
self.stdout.write(str(e))
def follow_hold_invoices(self):
''' Follows and updates LNpayment objects
until settled or canceled
Background: SubscribeInvoices stub iterator would be great to use here.
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
We are very interested on the other two states (CANCELLED and ACCEPTED).
@ -20,18 +45,6 @@ class Command(BaseCommand):
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
'''
help = 'Follows all active hold invoices'
rest = 5 # seconds between consecutive checks for invoice updates
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
def follow_invoices(self, *args, **options):
''' Follows and updates LNpayment objects
until settled or canceled'''
# TODO handle 'database is locked'
lnd_state_to_lnpayment_status = {
0: LNPayment.Status.INVGEN, # OPEN
1: LNPayment.Status.SETLED, # SETTLED
@ -41,9 +54,6 @@ class Command(BaseCommand):
stub = LNNode.invoicesstub
while True:
time.sleep(self.rest)
# time it for debugging
t0 = time.time()
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
@ -80,8 +90,8 @@ class Command(BaseCommand):
changed = not old_status==new_status
if changed:
# self.handle_status_change(hold_lnpayment, old_status)
hold_lnpayment.save()
self.update_order_status(hold_lnpayment)
hold_lnpayment.save()
# Report for debugging
new_status = LNPayment.Status(hold_lnpayment.status).label
@ -99,6 +109,21 @@ class Command(BaseCommand):
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
def retry_payments(self):
''' Checks if any payment is due for retry, and tries to pay it'''
queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
routing_attempts__lt=4,
last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
for lnpayment in queryset:
success, _ = follow_send_payment(lnpayment)
# 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
lnpayment.routing_attempts = 0
lnpayment.save()
def update_order_status(self, lnpayment):
''' Background process following LND hold invoices
@ -123,21 +148,27 @@ class Command(BaseCommand):
elif hasattr(lnpayment, 'order_escrow' ):
Logics.trade_escrow_received(lnpayment.order_escrow)
return
except Exception as e:
self.stdout.write(str(e))
# TODO If a lnpayment goes from LOCKED to INVGED. Totally weird
# If the LNPayment goes to CANCEL from INVGEN, the invoice had expired
# If it goes to CANCEL from LOCKED the bond was relased. Order had expired in both cases.
# Testing needed for end of time trades!
if lnpayment.status == LNPayment.Status.CANCEL :
if hasattr(lnpayment, 'order_made' ):
Logics.order_expires(lnpayment.order_made)
return
elif hasattr(lnpayment, 'order_taken' ):
Logics.order_expires(lnpayment.order_taken)
return
elif hasattr(lnpayment, 'order_escrow' ):
Logics.order_expires(lnpayment.order_escrow)
return
# TODO If a lnpayment goes from LOCKED to INVGEN. Totally weird
# halt the order
if lnpayment.status == LNPayment.Status.LOCKED:
if lnpayment.status == LNPayment.Status.INVGEN:
pass
def handle(self, *args, **options):
''' Never mind database locked error, keep going, print them out'''
try:
self.follow_invoices()
except Exception as e:
if 'database is locked' in str(e):
self.stdout.write('database is locked')
self.stdout.write(str(e))

View File

@ -38,6 +38,8 @@ def follow_send_payment(lnpayment):
from decouple import config
from base64 import b64decode
from django.utils import timezone
from datetime import timedelta
from api.lightning.node import LNNode
from api.models import LNPayment, Order
@ -51,6 +53,7 @@ def follow_send_payment(lnpayment):
timeout_seconds=60) # time out payment in 60 seconds
order = lnpayment.order_paid
try:
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
if response.status == 0 : # Status 0 'UNKNOWN'
# Not sure when this status happens
@ -66,11 +69,15 @@ def follow_send_payment(lnpayment):
if response.status == 3 : # Status 3 'FAILED'
print('FAILED')
lnpayment.status = LNPayment.Status.FAILRO
lnpayment.last_routing_time = timezone.now()
lnpayment.routing_attempts += 1
lnpayment.save()
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
order.save()
context = LNNode.payment_failure_context[response.failure_reason]
# Call a retry here?
context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
print(context)
# Call a retry in 5 mins here?
return False, context
if response.status == 2 : # Status 2 'SUCCEEDED'
@ -78,9 +85,22 @@ def follow_send_payment(lnpayment):
lnpayment.status = LNPayment.Status.SUCCED
lnpayment.save()
order.status = Order.Status.SUC
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
order.save()
return True, None
except Exception as e:
if "invoice expired" in str(e):
print('INVOICE EXPIRED')
lnpayment.status = LNPayment.Status.EXPIRE
lnpayment.last_routing_time = timezone.now()
lnpayment.save()
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
order.save()
context = {'routing_failed':'The payout invoice has expired'}
return False, context
@shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market():

View File

@ -237,6 +237,11 @@ class OrderView(viewsets.ViewSet):
data['retries'] = order.buyer_invoice.routing_attempts
data['next_retry_time'] = order.buyer_invoice.last_routing_time + timedelta(minutes=RETRY_TIME)
if order.buyer_invoice.status == LNPayment.Status.EXPIRE:
data['invoice_expired'] = True
# Add invoice amount once again if invoice was expired.
data['invoice_amount'] = int(order.last_satoshis * (1-FEE))
return Response(data, status.HTTP_200_OK)
def take_update_confirm_dispute_cancel(self, request, format=None):

View File

@ -87,6 +87,7 @@ export default class OrderPage extends Component {
delay: this.setDelay(newStateVars.status),
currencyCode: this.getCurrencyCode(newStateVars.currency),
penalty: newStateVars.penalty, // in case penalty time has finished, it goes back to null
invoice_expired: newStateVars.invoice_expired // in case invoice had expired, it goes back to null when it is valid again
};
var completeStateVars = Object.assign({}, newStateVars, otherStateVars);

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import { Paper, Rating, Button, 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 Chat from "./Chat"
// Icons
@ -349,7 +349,7 @@ export default class TradeBox extends Component {
showInputInvoice(){
return (
// TODO Option to upload files and images
// TODO Option to upload using QR from camera
<Grid container spacing={1}>
{/* In case the taker was very fast to scan the bond, make the taker found alarm sound again */}
@ -662,10 +662,9 @@ handleRatingChange=(e)=>{
)
}
showRoutingFailed(){
showRoutingFailed=()=>{
// TODO If it has failed 3 times, ask for a new invoice.
if(this.props.data.invoice_expired){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
@ -675,18 +674,55 @@ handleRatingChange=(e)=>{
</Grid>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
RoboSats will retry pay your invoice 3 times every 5 minutes. If it keeps failing, you
Your invoice has expires or more than 3 payments have been attempted.
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b> Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats </b>
</Typography>
</Grid>
<Grid item xs={12} align="center">
<TextField
error={this.state.badInvoice}
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
label={"Payout Lightning Invoice"}
required
inputProps={{
style: {textAlign:"center"}
}}
multiline
onChange={this.handleInputInvoiceChanged}
/>
</Grid>
<Grid item xs={12} align="center">
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
</Grid>
</Grid>
)
}else{
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="h6" variant="h6">
Lightning Routing Failed
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
RoboSats will try to pay your invoice 3 times every 5 minutes. If it keeps failing, you
will be able to submit a new invoice. Check whether you have enough inboud liquidity.
Remember that lightning nodes must be online in order to receive payments.
</Typography>
<List>
<Divider/>
<ListItemText secondary="Next attempt in">
<Countdown date={new Date(this.props.data.next_retry_time)} renderer={this.countdownRenderer} />
</ListItemText>
</List>
</Grid>
</Grid>
)
)}
}
render() {