mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-26 22:06:08 +03:00
Add logics for payment retry, first iteration.
This commit is contained in:
parent
25ab5fdf2e
commit
2d1a2e4c5c
@ -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={
|
||||
@ -320,6 +321,10 @@ class Logics():
|
||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
|
||||
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()
|
||||
|
@ -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,25 +14,36 @@ import time
|
||||
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||
|
||||
class Command(BaseCommand):
|
||||
'''
|
||||
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).
|
||||
Therefore, this thread (follow_invoices) will iterate over all LNpayment
|
||||
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 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)
|
||||
|
||||
def follow_invoices(self, *args, **options):
|
||||
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'''
|
||||
|
||||
# TODO handle 'database is locked'
|
||||
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).
|
||||
Therefore, this thread (follow_invoices) will iterate over all LNpayment
|
||||
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
|
||||
'''
|
||||
|
||||
lnd_state_to_lnpayment_status = {
|
||||
0: LNPayment.Status.INVGEN, # OPEN
|
||||
@ -41,64 +54,76 @@ 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])
|
||||
|
||||
# time it for debugging
|
||||
t0 = time.time()
|
||||
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
|
||||
debug = {}
|
||||
debug['num_active_invoices'] = len(queryset)
|
||||
debug['invoices'] = []
|
||||
at_least_one_changed = False
|
||||
|
||||
debug = {}
|
||||
debug['num_active_invoices'] = len(queryset)
|
||||
debug['invoices'] = []
|
||||
at_least_one_changed = False
|
||||
|
||||
for idx, hold_lnpayment in enumerate(queryset):
|
||||
old_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
|
||||
try:
|
||||
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
||||
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
|
||||
|
||||
except Exception as e:
|
||||
# If it fails at finding the invoice: it has been canceled.
|
||||
# In RoboSats DB we make a distinction between cancelled and returned (LND does not)
|
||||
if 'unable to locate invoice' in str(e):
|
||||
self.stdout.write(str(e))
|
||||
hold_lnpayment.status = LNPayment.Status.CANCEL
|
||||
# LND restarted.
|
||||
if 'wallet locked, unlock it' in str(e):
|
||||
self.stdout.write(str(timezone.now())+':: Wallet Locked')
|
||||
# Other write to logs
|
||||
else:
|
||||
self.stdout.write(str(e))
|
||||
|
||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
|
||||
# Only save the hold_payments that change (otherwise this function does not scale)
|
||||
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)
|
||||
|
||||
# Report for debugging
|
||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
debug['invoices'].append({idx:{
|
||||
'payment_hash': str(hold_lnpayment.payment_hash),
|
||||
'old_status': old_status,
|
||||
'new_status': new_status,
|
||||
}})
|
||||
|
||||
at_least_one_changed = at_least_one_changed or changed
|
||||
for idx, hold_lnpayment in enumerate(queryset):
|
||||
old_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
|
||||
debug['time']=time.time()-t0
|
||||
try:
|
||||
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
||||
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
|
||||
|
||||
if at_least_one_changed:
|
||||
self.stdout.write(str(timezone.now()))
|
||||
self.stdout.write(str(debug))
|
||||
except Exception as e:
|
||||
# If it fails at finding the invoice: it has been canceled.
|
||||
# In RoboSats DB we make a distinction between cancelled and returned (LND does not)
|
||||
if 'unable to locate invoice' in str(e):
|
||||
self.stdout.write(str(e))
|
||||
hold_lnpayment.status = LNPayment.Status.CANCEL
|
||||
# LND restarted.
|
||||
if 'wallet locked, unlock it' in str(e):
|
||||
self.stdout.write(str(timezone.now())+':: Wallet Locked')
|
||||
# Other write to logs
|
||||
else:
|
||||
self.stdout.write(str(e))
|
||||
|
||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
|
||||
# Only save the hold_payments that change (otherwise this function does not scale)
|
||||
changed = not old_status==new_status
|
||||
if changed:
|
||||
# self.handle_status_change(hold_lnpayment, old_status)
|
||||
self.update_order_status(hold_lnpayment)
|
||||
hold_lnpayment.save()
|
||||
|
||||
# Report for debugging
|
||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
debug['invoices'].append({idx:{
|
||||
'payment_hash': str(hold_lnpayment.payment_hash),
|
||||
'old_status': old_status,
|
||||
'new_status': new_status,
|
||||
}})
|
||||
|
||||
at_least_one_changed = at_least_one_changed or changed
|
||||
|
||||
debug['time']=time.time()-t0
|
||||
|
||||
if at_least_one_changed:
|
||||
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
|
||||
# halt the order
|
||||
if lnpayment.status == LNPayment.Status.LOCKED:
|
||||
pass
|
||||
# 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
|
||||
|
||||
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))
|
||||
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.INVGEN:
|
||||
pass
|
66
api/tasks.py
66
api/tasks.py
@ -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,36 +53,54 @@ def follow_send_payment(lnpayment):
|
||||
timeout_seconds=60) # time out payment in 60 seconds
|
||||
|
||||
order = lnpayment.order_paid
|
||||
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
|
||||
if response.status == 0 : # Status 0 'UNKNOWN'
|
||||
# Not sure when this status happens
|
||||
pass
|
||||
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
|
||||
pass
|
||||
|
||||
if response.status == 1 : # Status 1 'IN_FLIGHT'
|
||||
print('IN_FLIGHT')
|
||||
lnpayment.status = LNPayment.Status.FLIGHT
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.PAY
|
||||
order.save()
|
||||
if response.status == 1 : # Status 1 'IN_FLIGHT'
|
||||
print('IN_FLIGHT')
|
||||
lnpayment.status = LNPayment.Status.FLIGHT
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.PAY
|
||||
order.save()
|
||||
|
||||
if response.status == 3 : # Status 3 'FAILED'
|
||||
print('FAILED')
|
||||
lnpayment.status = LNPayment.Status.FAILRO
|
||||
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 = {'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'
|
||||
print('SUCCEEDED')
|
||||
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 = LNNode.payment_failure_context[response.failure_reason]
|
||||
# Call a retry here?
|
||||
context = {'routing_failed':'The payout invoice has expired'}
|
||||
return False, context
|
||||
|
||||
if response.status == 2 : # Status 2 'SUCCEEDED'
|
||||
print('SUCCEEDED')
|
||||
lnpayment.status = LNPayment.Status.SUCCED
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.SUC
|
||||
order.save()
|
||||
return True, None
|
||||
|
||||
@shared_task(name="cache_external_market_prices", ignore_result=True)
|
||||
def cache_market():
|
||||
|
||||
|
@ -236,6 +236,11 @@ class OrderView(viewsets.ViewSet):
|
||||
elif order.status == Order.Status.FAI:
|
||||
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)
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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,45 @@ 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">
|
||||
<Typography component="h6" variant="h6">
|
||||
Lightning Routing Failed
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
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">
|
||||
@ -675,18 +710,19 @@ 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
|
||||
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user