diff --git a/api/lightning/node.py b/api/lightning/node.py index 76d6acb3..4f6cb348 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -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.", diff --git a/api/logics.py b/api/logics.py index dae70ab0..c0a0383a 100644 --- a/api/logics.py +++ b/api/logics.py @@ -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 diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py new file mode 100644 index 00000000..abee1eac --- /dev/null +++ b/api/management/commands/follow_invoices.py @@ -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)) + + + \ No newline at end of file diff --git a/api/models.py b/api/models.py index a2d04adc..8debaf5c 100644 --- a/api/models.py +++ b/api/models.py @@ -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 diff --git a/api/tasks.py b/api/tasks.py index 16219ac8..a17c8de9 100644 --- a/api/tasks.py +++ b/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: diff --git a/api/utils.py b/api/utils.py index b7ea9459..467753b4 100644 --- a/api/utils.py +++ b/api/utils.py @@ -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. diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index a0800075..5516ee36 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -115,10 +115,10 @@ export default class BottomBar extends Component { Community

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!

diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index b5a26bf7..453867ed 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -237,7 +237,7 @@ export default class OrderPage extends Component { - {this.state.type ? "Sell " : "Buy "} Order Details + Order Details diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index fb359736..17b3dc26 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -499,7 +499,7 @@ handleRatingChange=(e)=>{ - TradeBox + Contract Box {/* Maker and taker Bond request */} diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py index df462750..00eb5af7 100644 --- a/robosats/celery/__init__.py +++ b/robosats/celery/__init__.py @@ -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),