Rework lightning module. Add version info

This commit is contained in:
Reckless_Satoshi 2022-01-11 06:36:43 -08:00
parent 55c5f62078
commit 17df987630
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
5 changed files with 195 additions and 86 deletions

View File

@ -45,7 +45,7 @@ class LNNode():
@classmethod @classmethod
def settle_hold_invoice(cls, preimage): def settle_hold_invoice(cls, preimage):
# SETTLING A HODL INVOICE '''settles a hold invoice'''
request = invoicesrpc.SettleInvoiceMsg(preimage=preimage) request = invoicesrpc.SettleInvoiceMsg(preimage=preimage)
response = invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())]) response = invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
# Fix this: tricky because settling sucessfully an invoice has no response. TODO # 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): def gen_hold_invoice(cls, num_satoshis, description, expiry):
'''Generates hold invoice''' '''Generates hold invoice'''
hold_payment = {}
# The preimage is a random hash of 256 bits entropy # The preimage is a random hash of 256 bits entropy
preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest() preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
# Its hash is used to generate the hold invoice # Its hash is used to generate the hold invoice
preimage_hash = hashlib.sha256(preimage).digest() r_hash = hashlib.sha256(preimage).digest()
request = invoicesrpc.AddHoldInvoiceRequest( request = invoicesrpc.AddHoldInvoiceRequest(
memo=description, memo=description,
value=num_satoshis, value=num_satoshis,
hash=preimage_hash, hash=r_hash,
expiry=expiry) expiry=expiry)
response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())]) response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])
invoice = response.payment_request hold_payment['invoice'] = response.payment_request
payreq_decoded = cls.decode_payreq(invoice) 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)
preimage = preimage.hex() return hold_payment
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)
return invoice, preimage, payment_hash, created_at, expires_at
@classmethod @classmethod
def validate_hold_invoice_locked(cls, payment_hash): 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 @classmethod
def check_until_invoice_locked(cls, payment_hash, expiration): 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.
# request = ln.InvoiceSubscription() If time expires, return False.'''
# When invoice is settled, return true. If time expires, return False.
# for invoice in stub.SubscribeInvoices(request):
# print(invoice)
request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
for invoice in invoicesstub.SubscribeSingleInvoice(request):
if timezone.now > expiration:
break
if invoice.state == 'LOCKED':
return True return True
return False
@classmethod @classmethod
def validate_ln_invoice(cls, invoice, num_satoshis): def validate_ln_invoice(cls, invoice, num_satoshis):
'''Checks if the submited LN invoice comforms to expectations''' '''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: try:
payreq_decoded = cls.decode_payreq(invoice) payreq_decoded = cls.decode_payreq(invoice)
except: 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: if not payreq_decoded.num_satoshis == num_satoshis:
context = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'} buyer_invoice['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
return False, context, None, None, None, None return buyer_invoice
created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) buyer_invoice['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
expires_at = created_at + timedelta(seconds=payreq_decoded.expiry) buyer_invoice['expires_at'] = buyer_invoice['created_at'] + timedelta(seconds=payreq_decoded.expiry)
if expires_at < timezone.now(): if buyer_invoice['expires_at'] < timezone.now():
context = {'bad_invoice':f'The invoice provided has already expired'} buyer_invoice['context'] = {'bad_invoice':f'The invoice provided has already expired'}
return False, context, None, None, None, None return buyer_invoice
description = payreq_decoded.description buyer_invoice['valid'] = True
payment_hash = payreq_decoded.payment_hash 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 @classmethod
def pay_invoice(cls, invoice): def pay_invoice(cls, invoice):
'''Sends sats to buyer, or cancelinvoices''' '''Sends sats to buyer'''
return True
@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 return True
@classmethod @classmethod
def double_check_htlc_is_settled(cls, payment_hash): def double_check_htlc_is_settled(cls, payment_hash):
''' Just as it sounds. Better safe than sorry!''' ''' Just as it sounds. Better safe than sorry!'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=payment_hash)
response = invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
if response.state == 'SETTLED':
return True return True
else:
return False

View File

@ -6,6 +6,8 @@ from .models import Order, LNPayment, MarketTick, User
from decouple import config from decouple import config
from .utils import get_exchange_rate from .utils import get_exchange_rate
import math
FEE = float(config('FEE')) FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE')) BOND_SIZE = float(config('BOND_SIZE'))
MARKET_PRICE_API = config('MARKET_PRICE_API') MARKET_PRICE_API = config('MARKET_PRICE_API')
@ -80,7 +82,7 @@ class Logics():
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
if not order.is_explicit: if not order.is_explicit:
premium = order.premium premium = order.premium
price = exchange_rate price = exchange_rate * (1+float(premium)/100)
else: else:
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
order_rate = float(order.amount) / (float(order.satoshis) / 100000000) order_rate = float(order.amount) / (float(order.satoshis) / 100000000)
@ -88,6 +90,9 @@ class Logics():
premium = int(premium*100) # 2 decimals left premium = int(premium*100) # 2 decimals left
price = order_rate price = order_rate
significant_digits = 6
price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1)
return price, premium return price, premium
def order_expires(order): def order_expires(order):
@ -118,10 +123,10 @@ class Logics():
return False, {'bad_request':'You cannot a invoice while bonds are not posted.'} return False, {'bad_request':'You cannot a invoice while bonds are not posted.'}
num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount'] 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: if not buyer_invoice['valid']:
return False, context return False, buyer_invoice['context']
order.buyer_invoice, _ = LNPayment.objects.update_or_create( order.buyer_invoice, _ = LNPayment.objects.update_or_create(
concept = LNPayment.Concepts.PAYBUYER, concept = LNPayment.Concepts.PAYBUYER,
@ -133,10 +138,10 @@ class Logics():
'invoice' : invoice, 'invoice' : invoice,
'status' : LNPayment.Status.VALIDI, 'status' : LNPayment.Status.VALIDI,
'num_satoshis' : num_satoshis, 'num_satoshis' : num_satoshis,
'description' : description, 'description' : buyer_invoice['description'],
'payment_hash' : payment_hash, 'payment_hash' : buyer_invoice['payment_hash'],
'created_at' : created_at, 'created_at' : buyer_invoice['created_at'],
'expires_at' : expires_at} 'expires_at' : buyer_invoice['expires_at']}
) )
# If the order status is 'Waiting for escrow'. Move forward to 'chat' # 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 Maker is charged the bond to prevent DDOS
on the LN node and order book. TODO Only charge a small part on the LN node and order book. TODO Only charge a small part
of the bond (requires maker submitting an invoice)''' 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 # 3) When taker cancels before bond
''' The order goes back to the book as public. ''' 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. The order goes into the public book if taker cancels.
In both cases there is a small fee.''' 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) # 5) When trade collateral has been posted (after escrow)
'''Always goes to cancelled status. Collaboration is needed. '''Always goes to cancelled status. Collaboration is needed.
When a user asks for cancel, 'order.is_pending_cancel' goes True. 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) order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE) 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 # 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( order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND, concept = LNPayment.Concepts.MAKEBOND,
type = LNPayment.Types.hold, type = LNPayment.Types.HOLD,
sender = user, sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME), receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice, invoice = hold_payment['invoice'],
preimage = preimage, preimage = hold_payment['preimage'],
status = LNPayment.Status.INVGEN, status = LNPayment.Status.INVGEN,
num_satoshis = bond_satoshis, num_satoshis = bond_satoshis,
description = description, description = description,
payment_hash = payment_hash, payment_hash = hold_payment['payment_hash'],
created_at = created_at, created_at = hold_payment['created_at'],
expires_at = expires_at) expires_at = hold_payment['expires_at'])
order.save() order.save()
return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis} return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
@classmethod @classmethod
def gen_taker_hold_invoice(cls, order, user): 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 order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
bond_satoshis = int(order.last_satoshis * BOND_SIZE) 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 # 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( order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TAKEBOND, concept = LNPayment.Concepts.TAKEBOND,
type = LNPayment.Types.hold, type = LNPayment.Types.HOLD,
sender = user, sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME), receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice, invoice = hold_payment['invoice'],
preimage = hold_payment['preimage'],
status = LNPayment.Status.INVGEN, status = LNPayment.Status.INVGEN,
num_satoshis = bond_satoshis, num_satoshis = bond_satoshis,
description = description, description = description,
payment_hash = payment_hash, payment_hash = hold_payment['payment_hash'],
expires_at = expires_at) created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at'])
# Extend expiry time to allow for escrow deposit # 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) ## Not here, on func for confirming taker collar. order.expires_at = timezone.now() + timedelta(minutes=EXP_TRADE_ESCR_INVOICE)
order.save() order.save()
return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis} return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
@classmethod @classmethod
def gen_escrow_hold_invoice(cls, order, user): 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 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) 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 # 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( order.trade_escrow = LNPayment.objects.create(
concept = LNPayment.Concepts.TRESCROW, concept = LNPayment.Concepts.TRESCROW,
type = LNPayment.Types.hold, type = LNPayment.Types.HOLD,
sender = user, sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME), receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice, invoice = hold_payment['invoice'],
preimage = hold_payment['preimage'],
status = LNPayment.Status.INVGEN, status = LNPayment.Status.INVGEN,
num_satoshis = escrow_satoshis, num_satoshis = escrow_satoshis,
description = description, description = description,
payment_hash = payment_hash, payment_hash = hold_payment['payment_hash'],
expires_at = expires_at) created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at'])
order.save() 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): def settle_escrow(order):
''' Settles the trade escrow HTLC''' ''' Settles the trade escrow hold invoice'''
# TODO ERROR HANDLING # 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 return valid
def pay_buyer_invoice(order): def pay_buyer_invoice(order):
''' Settles the trade escrow HTLC''' ''' Pay buyer invoice'''
# TODO ERROR HANDLING # TODO ERROR HANDLING
valid = LNNode.pay_invoice(order.buyer_invoice.payment_hash) valid = LNNode.pay_invoice(order.buyer_invoice.invoice)
return valid return valid
@classmethod @classmethod

View File

@ -18,8 +18,8 @@ BOND_SIZE = float(config('BOND_SIZE'))
class LNPayment(models.Model): class LNPayment(models.Model):
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hold) NORM = 0, 'Regular invoice' # Only outgoing buyer payment will be a regular invoice (Non-hold)
hold = 1, 'hold invoice' HOLD = 1, 'hold invoice'
class Concepts(models.IntegerChoices): class Concepts(models.IntegerChoices):
MAKEBOND = 0, 'Maker bond' MAKEBOND = 0, 'Maker bond'
@ -38,7 +38,7 @@ class LNPayment(models.Model):
FAILRO = 7, 'Failed routing' FAILRO = 7, 'Failed routing'
# payment use details # 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) concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
routing_retries = models.PositiveSmallIntegerField(null=False, default=0) routing_retries = models.PositiveSmallIntegerField(null=False, default=0)

View File

@ -1,11 +1,10 @@
import requests, ring, os
from decouple import config 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): def get_exchange_rate(currency):
# TODO Add fallback Public APIs and error handling # TODO Add fallback Public APIs and error handling
# Think about polling price data in a different way (e.g. store locally every t seconds) # Think about polling price data in a different way (e.g. store locally every t seconds)
@ -14,3 +13,24 @@ def get_exchange_rate(currency):
exchange_rate = float(market_prices[currency]['last']) exchange_rate = float(market_prices[currency]['last'])
return exchange_rate 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

View File

@ -11,6 +11,7 @@ from django.contrib.auth.models import User
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
from .models import LNPayment, MarketTick, Order from .models import LNPayment, MarketTick, Order
from .logics import Logics from .logics import Logics
from .utils import get_lnd_version, get_commit_robosats
from .nick_generator.nick_generator import NickGenerator from .nick_generator.nick_generator import NickGenerator
from robohash import Robohash from robohash import Robohash
@ -415,8 +416,10 @@ class InfoView(ListAPIView):
avg_premium = None avg_premium = None
total_volume = None total_volume = None
context['last_day_avg_btc_premium'] = avg_premium context['today_avg_nonkyc_btc_premium'] = avg_premium
context['total_volume_today'] = total_volume 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) return Response(context, status.HTTP_200_OK)