diff --git a/api/lightning/node.py b/api/lightning/node.py index 45870041..11ec9b34 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -45,7 +45,7 @@ class LNNode(): @classmethod def settle_hold_invoice(cls, preimage): - # SETTLING A HODL INVOICE + '''settles a hold invoice''' request = invoicesrpc.SettleInvoiceMsg(preimage=preimage) response = invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())]) # Fix this: tricky because settling sucessfully an invoice has no response. TODO @@ -58,88 +58,111 @@ class LNNode(): def gen_hold_invoice(cls, num_satoshis, description, expiry): '''Generates hold invoice''' + hold_payment = {} # The preimage is a random hash of 256 bits entropy preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest() # Its hash is used to generate the hold invoice - preimage_hash = hashlib.sha256(preimage).digest() + r_hash = hashlib.sha256(preimage).digest() request = invoicesrpc.AddHoldInvoiceRequest( memo=description, value=num_satoshis, - hash=preimage_hash, + hash=r_hash, expiry=expiry) response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())]) - invoice = response.payment_request - payreq_decoded = cls.decode_payreq(invoice) - - preimage = preimage.hex() - payment_hash = payreq_decoded.payment_hash - created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) - expires_at = created_at + timedelta(seconds=payreq_decoded.expiry) + hold_payment['invoice'] = response.payment_request + payreq_decoded = cls.decode_payreq(hold_payment['invoice']) + hold_payment['preimage'] = preimage.hex() + hold_payment['payment_hash'] = payreq_decoded.payment_hash + hold_payment['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) + hold_payment['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry) - return invoice, preimage, payment_hash, created_at, expires_at + return hold_payment @classmethod def validate_hold_invoice_locked(cls, payment_hash): - '''Checks if hodl invoice is locked''' + '''Checks if hold invoice is locked''' + + request = invoicesrpc.LookupInvoiceMsg(payment_hash=payment_hash) + response = invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())]) + + # What is the state for locked ??? + if response.state == 'OPEN' or response.state == 'SETTLED': + return False + else: + return True - return True @classmethod def check_until_invoice_locked(cls, payment_hash, expiration): - '''Checks until hodl invoice is locked''' + '''Checks until hold invoice is locked. + When invoice is locked, returns true. + If time expires, return False.''' + + request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash) + for invoice in invoicesstub.SubscribeSingleInvoice(request): + if timezone.now > expiration: + break + if invoice.state == 'LOCKED': + return True - # request = ln.InvoiceSubscription() - # When invoice is settled, return true. If time expires, return False. - # for invoice in stub.SubscribeInvoices(request): - # print(invoice) - - return True + return False @classmethod def validate_ln_invoice(cls, invoice, num_satoshis): '''Checks if the submited LN invoice comforms to expectations''' + buyer_invoice = { + 'valid': False, + 'context': None, + 'description': None, + 'payment_hash': None, + 'created_at': None, + 'expires_at': None, + } + try: payreq_decoded = cls.decode_payreq(invoice) except: - return False, {'bad_invoice':'Does not look like a valid lightning invoice'}, None, None, None, None + buyer_invoice['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'} + return buyer_invoice if not payreq_decoded.num_satoshis == num_satoshis: - context = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'} - return False, context, None, None, None, None + buyer_invoice['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'} + return buyer_invoice - created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) - expires_at = created_at + timedelta(seconds=payreq_decoded.expiry) + buyer_invoice['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) + buyer_invoice['expires_at'] = buyer_invoice['created_at'] + timedelta(seconds=payreq_decoded.expiry) - if expires_at < timezone.now(): - context = {'bad_invoice':f'The invoice provided has already expired'} - return False, context, None, None, None, None + if buyer_invoice['expires_at'] < timezone.now(): + buyer_invoice['context'] = {'bad_invoice':f'The invoice provided has already expired'} + return buyer_invoice - description = payreq_decoded.description - payment_hash = payreq_decoded.payment_hash + buyer_invoice['valid'] = True + buyer_invoice['description'] = payreq_decoded.description + buyer_invoice['payment_hash'] = payreq_decoded.payment_hash - return True, None, description, payment_hash, created_at, expires_at + return buyer_invoice @classmethod def pay_invoice(cls, invoice): - '''Sends sats to buyer, or cancelinvoices''' - return True + '''Sends sats to buyer''' + - @classmethod - def check_if_hold_invoice_is_locked(cls, payment_hash): - '''Every hodl invoice that is in state INVGEN - Has to be checked for payment received until - the window expires''' - return True @classmethod def double_check_htlc_is_settled(cls, payment_hash): ''' Just as it sounds. Better safe than sorry!''' - return True + request = invoicesrpc.LookupInvoiceMsg(payment_hash=payment_hash) + response = invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())]) + + if response.state == 'SETTLED': + return True + else: + return False diff --git a/api/logics.py b/api/logics.py index 02d4b1f4..1041f502 100644 --- a/api/logics.py +++ b/api/logics.py @@ -6,6 +6,8 @@ from .models import Order, LNPayment, MarketTick, User from decouple import config from .utils import get_exchange_rate +import math + FEE = float(config('FEE')) BOND_SIZE = float(config('BOND_SIZE')) MARKET_PRICE_API = config('MARKET_PRICE_API') @@ -80,13 +82,16 @@ class Logics(): exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) if not order.is_explicit: premium = order.premium - price = exchange_rate + price = exchange_rate * (1+float(premium)/100) else: exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) order_rate = float(order.amount) / (float(order.satoshis) / 100000000) premium = order_rate / exchange_rate - 1 premium = int(premium*100) # 2 decimals left price = order_rate + + significant_digits = 6 + price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1) return price, premium @@ -118,10 +123,10 @@ class Logics(): return False, {'bad_request':'You cannot a invoice while bonds are not posted.'} num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount'] - valid, context, description, payment_hash, created_at, expires_at = LNNode.validate_ln_invoice(invoice, num_satoshis) + buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis) - if not valid: - return False, context + if not buyer_invoice['valid']: + return False, buyer_invoice['context'] order.buyer_invoice, _ = LNPayment.objects.update_or_create( concept = LNPayment.Concepts.PAYBUYER, @@ -133,10 +138,10 @@ class Logics(): 'invoice' : invoice, 'status' : LNPayment.Status.VALIDI, 'num_satoshis' : num_satoshis, - 'description' : description, - 'payment_hash' : payment_hash, - 'created_at' : created_at, - 'expires_at' : expires_at} + 'description' : buyer_invoice['description'], + 'payment_hash' : buyer_invoice['payment_hash'], + 'created_at' : buyer_invoice['created_at'], + 'expires_at' : buyer_invoice['expires_at']} ) # If the order status is 'Waiting for escrow'. Move forward to 'chat' @@ -206,7 +211,14 @@ class Logics(): Maker is charged the bond to prevent DDOS on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)''' - + elif order.status == Order.Status.PUB and order.maker == user: + #Settle the maker bond (Maker loses the bond for a public order) + valid = cls.settle_maker_bond(order) + if valid: + order.maker = None + order.status = Order.Status.UCA + order.save() + return True, None # 3) When taker cancels before bond ''' The order goes back to the book as public. @@ -227,6 +239,29 @@ class Logics(): The order goes into the public book if taker cancels. In both cases there is a small fee.''' + # 4.a) When maker cancel after bond (before escrow) + '''The order into cancelled status if maker cancels.''' + elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user: + #Settle the maker bond (Maker loses the bond for canceling an ongoing trade) + valid = cls.settle_maker_bond(order) + if valid: + order.maker = None + order.status = Order.Status.UCA + order.save() + return True, None + + # 4.b) When taker cancel after bond (before escrow) + '''The order into cancelled status if maker cancels.''' + elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user: + #Settle the maker bond (Maker loses the bond for canceling an ongoing trade) + valid = cls.settle_taker_bond(order) + if valid: + order.taker = None + order.status = Order.Status.PUB + # order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard. + order.save() + return True, None + # 5) When trade collateral has been posted (after escrow) '''Always goes to cancelled status. Collaboration is needed. When a user asks for cancel, 'order.is_pending_cancel' goes True. @@ -253,27 +288,28 @@ class Logics(): order.last_satoshis = cls.satoshis_now(order) bond_satoshis = int(order.last_satoshis * BOND_SIZE) - description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat or unilaterally cancel' + + description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond. It will automatically return if you do not cancel or cheat" # Gen hold Invoice - invoice, preimage, payment_hash, created_at, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.maker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.MAKEBOND, - type = LNPayment.Types.hold, + type = LNPayment.Types.HOLD, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), - invoice = invoice, - preimage = preimage, + invoice = hold_payment['invoice'], + preimage = hold_payment['preimage'], status = LNPayment.Status.INVGEN, num_satoshis = bond_satoshis, description = description, - payment_hash = payment_hash, - created_at = created_at, - expires_at = expires_at) + payment_hash = hold_payment['payment_hash'], + created_at = hold_payment['created_at'], + expires_at = hold_payment['expires_at']) order.save() - return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis} + return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis} @classmethod def gen_taker_hold_invoice(cls, order, user): @@ -294,28 +330,30 @@ class Logics(): order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE bond_satoshis = int(order.last_satoshis * BOND_SIZE) - description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat or unilaterally cancel' + description = f"RoboSats - Taking '{str(order)}' - This is a taker bond. It will automatically return if you do not cancel or cheat" # Gen hold Invoice - invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.taker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.TAKEBOND, - type = LNPayment.Types.hold, + type = LNPayment.Types.HOLD, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), - invoice = invoice, + invoice = hold_payment['invoice'], + preimage = hold_payment['preimage'], status = LNPayment.Status.INVGEN, num_satoshis = bond_satoshis, description = description, - payment_hash = payment_hash, - expires_at = expires_at) + payment_hash = hold_payment['payment_hash'], + created_at = hold_payment['created_at'], + expires_at = hold_payment['expires_at']) # Extend expiry time to allow for escrow deposit ## Not here, on func for confirming taker collar. order.expires_at = timezone.now() + timedelta(minutes=EXP_TRADE_ESCR_INVOICE) order.save() - return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis} + return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis} @classmethod def gen_escrow_hold_invoice(cls, order, user): @@ -334,38 +372,63 @@ class Logics(): return False, None # Does not return any context of a healthy locked escrow escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis) - description = f'RoboSats - Escrow amount for {str(order)} - This escrow will be released to the buyer once you confirm you received the fiat.' + description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not pay." # Gen hold Invoice - invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600) + hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600) order.trade_escrow = LNPayment.objects.create( concept = LNPayment.Concepts.TRESCROW, - type = LNPayment.Types.hold, + type = LNPayment.Types.HOLD, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), - invoice = invoice, + invoice = hold_payment['invoice'], + preimage = hold_payment['preimage'], status = LNPayment.Status.INVGEN, num_satoshis = escrow_satoshis, description = description, - payment_hash = payment_hash, - expires_at = expires_at) + payment_hash = hold_payment['payment_hash'], + created_at = hold_payment['created_at'], + expires_at = hold_payment['expires_at']) order.save() - return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis} + return True, {'escrow_invoice':hold_payment['invoice'],'escrow_satoshis': escrow_satoshis} def settle_escrow(order): - ''' Settles the trade escrow HTLC''' + ''' Settles the trade escrow hold invoice''' # TODO ERROR HANDLING + valid = LNNode.settle_hold_invoice(order.trade_escrow.preimage) + if valid: + order.trade_escrow.status = LNPayment.Status.SETLED + order.save() + + return valid + + def settle_maker_bond(order): + ''' Settles the maker bond hold invoice''' + # TODO ERROR HANDLING + valid = LNNode.settle_hold_invoice(order.maker_bond.preimage) + if valid: + order.maker_bond.status = LNPayment.Status.SETLED + order.save() + + return valid + + def settle_taker_bond(order): + ''' Settles the taker bond hold invoice''' + # TODO ERROR HANDLING + valid = LNNode.settle_hold_invoice(order.taker_bond.preimage) + if valid: + order.taker_bond.status = LNPayment.Status.SETLED + order.save() - valid = LNNode.settle_hold_htlcs(order.trade_escrow.payment_hash) return valid def pay_buyer_invoice(order): - ''' Settles the trade escrow HTLC''' + ''' Pay buyer invoice''' # TODO ERROR HANDLING - valid = LNNode.pay_invoice(order.buyer_invoice.payment_hash) + valid = LNNode.pay_invoice(order.buyer_invoice.invoice) return valid @classmethod diff --git a/api/models.py b/api/models.py index 4de3ce03..e1707561 100644 --- a/api/models.py +++ b/api/models.py @@ -18,8 +18,8 @@ BOND_SIZE = float(config('BOND_SIZE')) class LNPayment(models.Model): class Types(models.IntegerChoices): - NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hold) - hold = 1, 'hold invoice' + NORM = 0, 'Regular invoice' # Only outgoing buyer payment will be a regular invoice (Non-hold) + HOLD = 1, 'hold invoice' class Concepts(models.IntegerChoices): MAKEBOND = 0, 'Maker bond' @@ -38,7 +38,7 @@ class LNPayment(models.Model): FAILRO = 7, 'Failed routing' # payment use details - type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.hold) + type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD) concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) routing_retries = models.PositiveSmallIntegerField(null=False, default=0) diff --git a/api/utils.py b/api/utils.py index 3cf948c5..6c23d924 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,11 +1,10 @@ +import requests, ring, os from decouple import config -import requests -import ring -storage = {} +market_cache = {} -@ring.dict(storage, expire=30) #keeps in cache for 30 seconds +@ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds def get_exchange_rate(currency): # TODO Add fallback Public APIs and error handling # Think about polling price data in a different way (e.g. store locally every t seconds) @@ -13,4 +12,25 @@ def get_exchange_rate(currency): market_prices = requests.get(config('MARKET_PRICE_API')).json() exchange_rate = float(market_prices[currency]['last']) - return exchange_rate \ No newline at end of file + return exchange_rate + +lnd_v_cache = {} + +@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds +def get_lnd_version(): + + stream = os.popen('lnd --version') + lnd_version = stream.read()[:-1] + + return lnd_version + +robosats_commit_cache = {} + +@ring.dict(robosats_commit_cache, expire=3600) +def get_commit_robosats(): + + stream = os.popen('git log -n 1 --pretty=format:"%H"') + lnd_version = stream.read() + + return lnd_version + diff --git a/api/views.py b/api/views.py index 926115c4..ae891419 100644 --- a/api/views.py +++ b/api/views.py @@ -11,6 +11,7 @@ from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from .models import LNPayment, MarketTick, Order from .logics import Logics +from .utils import get_lnd_version, get_commit_robosats from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -415,8 +416,10 @@ class InfoView(ListAPIView): avg_premium = None total_volume = None - context['last_day_avg_btc_premium'] = avg_premium - context['total_volume_today'] = total_volume + context['today_avg_nonkyc_btc_premium'] = avg_premium + context['today_total_volume'] = total_volume + context['lnd_version'] = get_lnd_version() + context['robosats_running_commit_hash'] = get_commit_robosats() return Response(context, status.HTTP_200_OK)