Add admin background task: follow all active hold invoices

This commit is contained in:
Reckless_Satoshi
2022-01-17 08:41:55 -08:00
parent 9d883ccc4d
commit eddd4674f6
10 changed files with 178 additions and 52 deletions

View File

@ -28,6 +28,10 @@ class LNNode():
invoicesstub = invoicesstub.InvoicesStub(channel) invoicesstub = invoicesstub.InvoicesStub(channel)
routerstub = routerstub.RouterStub(channel) routerstub = routerstub.RouterStub(channel)
lnrpc = lnrpc
invoicesrpc = invoicesrpc
routerrpc = routerrpc
payment_failure_context = { payment_failure_context = {
0: "Payment isn't failed (yet)", 0: "Payment isn't failed (yet)",
1: "There are more routes to try, but the payment timeout was exceeded.", 1: "There are more routes to try, but the payment timeout was exceeded.",

View File

@ -53,7 +53,7 @@ class Logics():
else: else:
order.taker = user order.taker = user
order.status = Order.Status.TAK order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta(minutes=EXP_TAKER_BOND_INVOICE) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
order.save() order.save()
return True, None return True, None
@ -234,18 +234,21 @@ class Logics():
return True, None return True, None
def dispute_statement(order, user, statement): def dispute_statement(order, user, statement):
''' Updates the dispute statements in DB''' ''' Updates the dispute statements in DB'''
if not order.status == Order.Status.DIS:
return False, {'bad_request':'Only orders in dispute accept a dispute statements'}
if len(statement) > 5000: if len(statement) > 5000:
return False, {'bad_statement':'The statement is longer than 5000 characters'} return False, {'bad_statement':'The statement is longer than 5000 characters'}
if order.maker == user: if order.maker == user:
order.maker_statement = statement order.maker_statement = statement
else: else:
order.taker_statement = statement order.taker_statement = statement
# If both statements are in, move to wait for dispute resolution # If both statements are in, move status to wait for dispute resolution
if order.maker_statement != None and order.taker_statement != None: if order.maker_statement != None and order.taker_statement != None:
order.status = Order.Status.WFR order.status = Order.Status.WFR
order.expires_at = timezone.now() + Order.t_to_expire[Order.Status.WFR] order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WFR])
order.save() order.save()
return True, None return True, None
@ -296,14 +299,14 @@ class Logics():
# If the order status is 'Waiting for invoice'. Move forward to 'chat' # If the order status is 'Waiting for invoice'. Move forward to 'chat'
if order.status == Order.Status.WFI: if order.status == Order.Status.WFI:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2: if order.status == Order.Status.WF2:
# If the escrow is lock move to Chat. # If the escrow is lock move to Chat.
if order.trade_escrow.status == LNPayment.Status.LOCKED: if order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
else: else:
order.status = Order.Status.WFE order.status = Order.Status.WFE
@ -413,7 +416,7 @@ class Logics():
order.maker_bond.save() order.maker_bond.save()
order.status = Order.Status.PUB order.status = Order.Status.PUB
# With the bond confirmation the order is extended 'public_order_duration' hours # With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION) order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
order.save() order.save()
return True return True
return False return False
@ -461,6 +464,9 @@ class Logics():
@classmethod @classmethod
def is_taker_bond_locked(cls, order): def is_taker_bond_locked(cls, order):
if order.taker_bond.status == LNPayment.Status.LOCKED:
return True
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash): if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND! # THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
# (This is the last update to "last_satoshis", it becomes the escrow amount next!) # (This is the last update to "last_satoshis", it becomes the escrow amount next!)
@ -468,9 +474,9 @@ class Logics():
order.taker_bond.status = LNPayment.Status.LOCKED order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save() order.taker_bond.save()
# Both users profiles are added one more contract # Both users profiles are added one more contract // Unsafe can add more than once.
order.maker.profile.total_contracts = order.maker.profile.total_contracts + 1 order.maker.profile.total_contracts += 1
order.taker.profile.total_contracts = order.taker.profile.total_contracts + 1 order.taker.profile.total_contracts += 1
order.maker.profile.save() order.maker.profile.save()
order.taker.profile.save() order.taker.profile.save()
@ -478,7 +484,7 @@ class Logics():
MarketTick.log_a_tick(order) MarketTick.log_a_tick(order)
# With the bond confirmation the order is extended 'public_order_duration' hours # With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(minutes=INVOICE_AND_ESCROW_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WF2])
order.status = Order.Status.WF2 order.status = Order.Status.WF2
order.save() order.save()
return True return True
@ -524,7 +530,7 @@ class Logics():
created_at = hold_payment['created_at'], created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at']) expires_at = hold_payment['expires_at'])
order.expires_at = timezone.now() + timedelta(seconds=EXP_TAKER_BOND_INVOICE) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
order.save() order.save()
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis} return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
@ -540,7 +546,7 @@ class Logics():
# If status is 'Waiting for invoice' move to Chat # If status is 'Waiting for invoice' move to Chat
elif order.status == Order.Status.WFE: elif order.status == Order.Status.WFE:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
order.save() order.save()
return True return True
return False return False
@ -649,7 +655,7 @@ class Logics():
if is_payed: if is_payed:
order.status = Order.Status.SUC order.status = Order.Status.SUC
order.buyer_invoice.status = LNPayment.Status.SUCCED order.buyer_invoice.status = LNPayment.Status.SUCCED
order.expires_at = timezone.now() + timedelta(days=1) # One day to rate / see this order. order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
order.save() order.save()
# RETURN THE BONDS # RETURN THE BONDS

View File

@ -0,0 +1,84 @@
from distutils.log import debug
from re import L
from xmlrpc.client import boolean
from django.core.management.base import BaseCommand, CommandError
from api.lightning.node import LNNode
from decouple import config
from base64 import b64decode
from api.models import LNPayment
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 to update their state 'live' '''
help = 'Follows all active hold invoices'
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
def handle(self, *args, **options):
''' Follows and updates LNpayment objects
until settled or canceled'''
lnd_state_to_lnpayment_status = {
0: LNPayment.Status.INVGEN,
1: LNPayment.Status.SETLED,
2: LNPayment.Status.CANCEL,
3: LNPayment.Status.LOCKED
}
stub = LNNode.invoicesstub
while True:
time.sleep(5)
# 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'] = []
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]
# If it fails at finding the invoice it has definetely been canceled.
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
except:
hold_lnpayment.status = LNPayment.Status.CANCEL
continue
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:
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),
'status_changed': not old_status==new_status,
'old_status': old_status,
'new_status': new_status,
}})
debug['time']=time.time()-t0
self.stdout.write(str(debug))

View File

@ -50,11 +50,12 @@ class LNPayment(models.Model):
LOCKED = 1, 'Locked' LOCKED = 1, 'Locked'
SETLED = 2, 'Settled' SETLED = 2, 'Settled'
RETNED = 3, 'Returned' RETNED = 3, 'Returned'
EXPIRE = 4, 'Expired' CANCEL = 4, 'Cancelled'
VALIDI = 5, 'Valid' EXPIRE = 5, 'Expired'
FLIGHT = 6, 'In flight' VALIDI = 6, 'Valid'
SUCCED = 7, 'Succeeded' FLIGHT = 7, 'In flight'
FAILRO = 8, 'Routing failed' SUCCED = 8, 'Succeeded'
FAILRO = 9, 'Routing failed'
# payment use details # payment use details

View File

@ -1,26 +1,20 @@
from celery import shared_task from celery import shared_task
from .lightning.node import LNNode
from django.contrib.auth.models import User
from .models import LNPayment, Order, Currency
from .logics import Logics
from .utils import get_exchange_rates
from django.db.models import Q
from datetime import timedelta
from django.utils import timezone
import time
@shared_task(name="users_cleansing") @shared_task(name="users_cleansing")
def users_cleansing(): def users_cleansing():
''' '''
Deletes users never used 12 hours after creation Deletes users never used 12 hours after creation
''' '''
from django.contrib.auth.models import User
from django.db.models import Q
from .logics import Logics
from datetime import timedelta
from django.utils import timezone
# Users who's last login has not been in the last 12 hours # Users who's last login has not been in the last 12 hours
active_time_range = (timezone.now() - timedelta(hours=12), timezone.now()) active_time_range = (timezone.now() - timedelta(hours=12), timezone.now())
queryset = User.objects.filter(~Q(last_login__range=active_time_range)) queryset = User.objects.filter(~Q(last_login__range=active_time_range))
queryset = queryset(is_staff=False) # Do not delete staff users queryset = queryset.filter(is_staff=False) # Do not delete staff users
# And do not have an active trade or any pass finished trade. # And do not have an active trade or any pass finished trade.
deleted_users = [] deleted_users = []
@ -46,8 +40,14 @@ def orders_expire(rest_secs):
Continuously checks order expiration times for 1 hour. If order Continuously checks order expiration times for 1 hour. If order
has expires, it calls the logics module for expiration handling. has expires, it calls the logics module for expiration handling.
''' '''
import time
from .models import Order
from .logics import Logics
from datetime import timedelta
from django.utils import timezone
now = timezone.now() now = timezone.now()
end_time = now + timedelta(hours=1) end_time = now + timedelta(minutes=60)
context = [] context = []
while now < end_time: while now < end_time:
@ -55,8 +55,12 @@ def orders_expire(rest_secs):
queryset = queryset.filter(expires_at__lt=now) # expires at lower than now queryset = queryset.filter(expires_at__lt=now) # expires at lower than now
for order in queryset: for order in queryset:
try: # TODO Fix, it might fail if returning an already returned bond.
info = str(order)+ " was "+ Order.Status(order.status).label
if Logics.order_expires(order): # Order send to expire here if Logics.order_expires(order): # Order send to expire here
context.append(str(order)+ " was "+ Order.Status(order.status).label) context.append(info)
except:
pass
# Allow for some thread rest. # Allow for some thread rest.
time.sleep(rest_secs) time.sleep(rest_secs)
@ -72,23 +76,51 @@ def orders_expire(rest_secs):
return results return results
@shared_task @shared_task(name='follow_send_payment')
def follow_lnd_payment(): def follow_send_payment(lnpayment):
''' Makes a payment and follows it. '''Sends sats to buyer, continuous update'''
Updates the LNpayment object, and retries
until payment is done'''
from decouple import config
from base64 import b64decode
from api.lightning.node import LNNode
from api.models import LNPayment
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
fee_limit_sat = max(lnpayment.num_satoshis * 0.0002, 10) # 200 ppm or 10 sats max
request = LNNode.routerrpc.SendPaymentRequest(
payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat,
timeout_seconds=60)
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
if response.status == 0 : # Status 0 'UNKNOWN'
pass pass
@shared_task if response.status == 1 : # Status 1 'IN_FLIGHT'
def follow_lnd_hold_invoice(): lnpayment.status = LNPayment.Status.FLIGHT
''' Follows and updates LNpayment object lnpayment.save()
until settled or canceled'''
pass if response.status == 3 : # Status 3 'FAILED'
lnpayment.status = LNPayment.Status.FAILRO
lnpayment.save()
context = LNNode.payment_failure_context[response.failure_reason]
return False, context
if response.status == 2 : # Status 2 'SUCCEEDED'
lnpayment.status = LNPayment.Status.SUCCED
lnpayment.save()
return True, None
@shared_task(name="cache_external_market_prices", ignore_result=True) @shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market(): def cache_market():
from .models import Currency
from .utils import get_exchange_rates
from django.utils import timezone
exchange_rates = get_exchange_rates(list(Currency.currency_dict.values())) exchange_rates = get_exchange_rates(list(Currency.currency_dict.values()))
results = {} results = {}
for val in Currency.currency_dict: for val in Currency.currency_dict:

View File

@ -5,7 +5,7 @@ import numpy as np
market_cache = {} market_cache = {}
# @ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds # @ring.dict(market_cache, expire=5) #keeps in cache for 5 seconds
def get_exchange_rates(currencies): def get_exchange_rates(currencies):
''' '''
Params: list of currency codes. Params: list of currency codes.

View File

@ -115,10 +115,10 @@ export default class BottomBar extends Component {
<Typography component="h5" variant="h5">Community</Typography> <Typography component="h5" variant="h5">Community</Typography>
<Typography component="body2" variant="body2"> <Typography component="body2" variant="body2">
<p> Support is only offered via public channels. <p> Support is only offered via public channels.
Writte us on our Telegram community if you have Join our Telegram community if you have
questions or want to hang out with other cool robots. questions or want to hang out with other cool robots.
If you find a bug or want to see new features, use Please, use our Github Issues if you find a bug or want
the Github Issues page. to see new features!
</p> </p>
</Typography> </Typography>
<List> <List>

View File

@ -237,7 +237,7 @@ export default class OrderPage extends Component {
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
{this.state.type ? "Sell " : "Buy "} Order Details Order Details
</Typography> </Typography>
<Paper elevation={12} style={{ padding: 8,}}> <Paper elevation={12} style={{ padding: 8,}}>
<List dense="true"> <List dense="true">

View File

@ -499,7 +499,7 @@ handleRatingChange=(e)=>{
<Grid container spacing={1} style={{ width:330}}> <Grid container spacing={1} style={{ width:330}}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
TradeBox Contract Box
</Typography> </Typography>
<Paper elevation={12} style={{ padding: 8,}}> <Paper elevation={12} style={{ padding: 8,}}>
{/* Maker and taker Bond request */} {/* Maker and taker Bond request */}

View File

@ -31,7 +31,6 @@ app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
# Configure the periodic tasks # Configure the periodic tasks
app.conf.beat_schedule = { app.conf.beat_schedule = {
# User cleansing every 6 hours
'users-cleansing': { # Cleans abandoned users every 6 hours 'users-cleansing': { # Cleans abandoned users every 6 hours
'task': 'users_cleansing', 'task': 'users_cleansing',
'schedule': timedelta(hours=6), 'schedule': timedelta(hours=6),