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
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
10 changed files with 178 additions and 52 deletions

View File

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

View File

@ -53,7 +53,7 @@ class Logics():
else:
order.taker = user
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()
return True, None
@ -234,18 +234,21 @@ class Logics():
return True, None
def dispute_statement(order, user, statement):
''' 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:
return False, {'bad_statement':'The statement is longer than 5000 characters'}
if order.maker == user:
order.maker_statement = statement
else:
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:
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()
return True, None
@ -296,14 +299,14 @@ class Logics():
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
if order.status == Order.Status.WFI:
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 order.status == Order.Status.WF2:
# If the escrow is lock move to Chat.
if order.trade_escrow.status == LNPayment.Status.LOCKED:
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:
order.status = Order.Status.WFE
@ -413,7 +416,7 @@ class Logics():
order.maker_bond.save()
order.status = Order.Status.PUB
# 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()
return True
return False
@ -461,6 +464,9 @@ class Logics():
@classmethod
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):
# 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!)
@ -468,9 +474,9 @@ class Logics():
order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save()
# Both users profiles are added one more contract
order.maker.profile.total_contracts = order.maker.profile.total_contracts + 1
order.taker.profile.total_contracts = order.taker.profile.total_contracts + 1
# Both users profiles are added one more contract // Unsafe can add more than once.
order.maker.profile.total_contracts += 1
order.taker.profile.total_contracts += 1
order.maker.profile.save()
order.taker.profile.save()
@ -478,7 +484,7 @@ class Logics():
MarketTick.log_a_tick(order)
# 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.save()
return True
@ -524,7 +530,7 @@ class Logics():
created_at = hold_payment['created_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()
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
elif order.status == Order.Status.WFE:
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()
return True
return False
@ -649,7 +655,7 @@ class Logics():
if is_payed:
order.status = Order.Status.SUC
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()
# 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'
SETLED = 2, 'Settled'
RETNED = 3, 'Returned'
EXPIRE = 4, 'Expired'
VALIDI = 5, 'Valid'
FLIGHT = 6, 'In flight'
SUCCED = 7, 'Succeeded'
FAILRO = 8, 'Routing failed'
CANCEL = 4, 'Cancelled'
EXPIRE = 5, 'Expired'
VALIDI = 6, 'Valid'
FLIGHT = 7, 'In flight'
SUCCED = 8, 'Succeeded'
FAILRO = 9, 'Routing failed'
# payment use details

View File

@ -1,26 +1,20 @@
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")
def users_cleansing():
'''
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
active_time_range = (timezone.now() - timedelta(hours=12), timezone.now())
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.
deleted_users = []
@ -46,8 +40,14 @@ def orders_expire(rest_secs):
Continuously checks order expiration times for 1 hour. If order
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()
end_time = now + timedelta(hours=1)
end_time = now + timedelta(minutes=60)
context = []
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
for order in queryset:
if Logics.order_expires(order): # Order send to expire here
context.append(str(order)+ " was "+ Order.Status(order.status).label)
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
context.append(info)
except:
pass
# Allow for some thread rest.
time.sleep(rest_secs)
@ -72,23 +76,51 @@ def orders_expire(rest_secs):
return results
@shared_task
def follow_lnd_payment():
''' Makes a payment and follows it.
Updates the LNpayment object, and retries
until payment is done'''
@shared_task(name='follow_send_payment')
def follow_send_payment(lnpayment):
'''Sends sats to buyer, continuous update'''
pass
from decouple import config
from base64 import b64decode
@shared_task
def follow_lnd_hold_invoice():
''' Follows and updates LNpayment object
until settled or canceled'''
from api.lightning.node import LNNode
from api.models import LNPayment
pass
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
if response.status == 1 : # Status 1 'IN_FLIGHT'
lnpayment.status = LNPayment.Status.FLIGHT
lnpayment.save()
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)
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()))
results = {}
for val in Currency.currency_dict:

View File

@ -5,7 +5,7 @@ import numpy as np
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):
'''
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="body2" variant="body2">
<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.
If you find a bug or want to see new features, use
the Github Issues page.
Please, use our Github Issues if you find a bug or want
to see new features!
</p>
</Typography>
<List>

View File

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

View File

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

View File

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