mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 19:06:26 +00:00
Add admin background task: follow all active hold invoices
This commit is contained in:
parent
9d883ccc4d
commit
eddd4674f6
@ -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.",
|
||||
|
@ -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
|
||||
|
84
api/management/commands/follow_invoices.py
Normal file
84
api/management/commands/follow_invoices.py
Normal 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))
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
86
api/tasks.py
86
api/tasks.py
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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 */}
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user