diff --git a/.env-sample b/.env-sample index 77f06ce3..6054e345 100644 --- a/.env-sample +++ b/.env-sample @@ -1,3 +1,9 @@ +# base64 ~/.lnd/tls.cert | tr -d '\n' +LND_CERT_BASE64='' +# base64 ~/.lnd/data/chain/bitcoin/testnet/admin.macaroon | tr -d '\n' +LND_MACAROON_BASE64='' +LND_GRPC_HOST='127.0.0.1:10009' + # Market price public API MARKET_PRICE_API = 'https://blockchain.info/ticker' @@ -5,8 +11,8 @@ MARKET_PRICE_API = 'https://blockchain.info/ticker' FEE = 0.002 # Bond size in percentage % BOND_SIZE = 0.01 -# Time out penalty for canceling takers in MINUTES -PENALTY_TIMEOUT = 2 +# Time out penalty for canceling takers in SECONDS +PENALTY_TIMEOUT = 60 # Trade limits in satoshis MIN_TRADE = 10000 diff --git a/.gitignore b/.gitignore index bb835b12..51480282 100755 --- a/.gitignore +++ b/.gitignore @@ -642,4 +642,5 @@ frontend/static/frontend/main* # robosats frontend/static/assets/avatars* api/lightning/lightning* +api/lightning/invoices* api/lightning/googleapis* diff --git a/api/lightning/node.py b/api/lightning/node.py index 324e250e..0b050c73 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -1,37 +1,83 @@ -import codecs, grpc, os -from . import lightning_pb2 as ln -from . import lightning_pb2_grpc as lnrpc +import grpc, os, hashlib, secrets, json +import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub +import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub -from datetime import timedelta +from decouple import config +from base64 import b64decode + +from datetime import timedelta, datetime from django.utils import timezone -import random -import string - ####### -# Placeholder functions -# Should work with LND (maybe c-lightning in the future) +# Should work with LND (c-lightning in the future if there are features that deserve the work) +####### + +CERT = b64decode(config('LND_CERT_BASE64')) +MACAROON = b64decode(config('LND_MACAROON_BASE64')) +LND_GRPC_HOST = config('LND_GRPC_HOST') class LNNode(): - ''' - Place holder functions to interact with Lightning Node - ''' - # macaroon = codecs.encode(open('LND_DIR/data/chain/bitcoin/simnet/admin.macaroon', 'rb').read(), 'hex') - # os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA' - # cert = open('LND_DIR/tls.cert', 'rb').read() - # ssl_creds = grpc.ssl_channel_credentials(cert) - # channel = grpc.secure_channel('localhost:10009', ssl_creds) - # stub = lightningstub.LightningStub(channel) - - def gen_hold_invoice(num_satoshis, description, expiry): - '''Generates hold invoice to publish an order''' - # TODO - invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX - payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX - expires_at = timezone.now() + timedelta(hours=8) ##FIX + os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA' - return invoice, payment_hash, expires_at + creds = grpc.ssl_channel_credentials(CERT) + channel = grpc.secure_channel(LND_GRPC_HOST, creds) + + lightningstub = lightningstub.LightningStub(channel) + invoicesstub = invoicesstub.InvoicesStub(channel) + + def decode_payreq(invoice): + '''Decodes a lightning payment request (invoice)''' + request = lnrpc.PayReqString(pay_req=invoice) + response = lightningstub.DecodePayReq(request, metadata=[('macaroon', MACAROON.hex())]) + return response + + def cancel_return_hold_invoice(payment_hash): + '''Cancels or returns a hold invoice''' + request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash)) + response = invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())]) + + # Fix this: tricky because canceling sucessfully an invoice has no response. TODO + if response == None: + return True + else: + return False + + def settle_hold_invoice(preimage): + # SETTLING A HODL INVOICE + request = invoicesrpc.SettleInvoiceMsg(preimage=preimage) + response = invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())]) + # Fix this: tricky because canceling sucessfully an invoice has no response. TODO + if response == None: + return True + else: + return False + + @classmethod + def gen_hold_invoice(cls, num_satoshis, description, expiry): + '''Generates hold invoice''' + + # The preimage will be 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() + + request = invoicesrpc.AddHoldInvoiceRequest( + memo=description, + value=num_satoshis, + hash=preimage_hash, + expiry=expiry) + response = invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())]) + + invoice = response.payment_request + + payreq_decoded = cls.decode_payreq(invoice) + 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 def validate_hold_invoice_locked(payment_hash): '''Checks if hodl invoice is locked''' @@ -43,43 +89,30 @@ class LNNode(): return True - def validate_ln_invoice(invoice, num_satoshis): - '''Checks if the submited LN invoice is as expected''' + @classmethod + def validate_ln_invoice(cls, invoice, num_satoshis): + '''Checks if the submited LN invoice comforms to expectations''' - # request = lnrpc.PayReqString(pay_req=invoice) - # response = stub.DecodePayReq(request, metadata=[('macaroon', macaroon)]) + try: + payreq_decoded = cls.decode_payreq(invoice) + except: + return False, {'bad_invoice':'Does not look like a valid lightning invoice'} - # # { - # # "destination": , - # # "payment_hash": , - # # "num_satoshis": , - # # "timestamp": , - # # "expiry": , - # # "description": , - # # "description_hash": , - # # "fallback_addr": , - # # "cltv_expiry": , - # # "route_hints": , - # # "payment_addr": , - # # "num_msat": , - # # "features": , - # # } - - # if not response['num_satoshis'] == num_satoshis: - # return False, {'bad_invoice':f'The invoice provided is not for {num_satoshis}. '}, None, None, None - # description = response['description'] - # payment_hash = response['payment_hash'] - # expires_at = timezone(response['expiry']) - # if payment_hash and expires_at > timezone.now(): - # return True, None, description, payment_hash, expires_at + if not payreq_decoded.num_satoshis == num_satoshis: + context = {'bad_invoice':f'The invoice provided is not for {num_satoshis}'} + return False, context, None, None, None, None - valid = True - context = None - description = 'Placeholder desc' # TODO decrypt from LN invoice - payment_hash = '567&*GIHU126' # TODO decrypt - expires_at = timezone.now() # TODO decrypt + created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) + expires_at = created_at + timedelta(seconds=payreq_decoded.expiry) - return valid, context, description, payment_hash, expires_at + if expires_at < timezone.now(): + context = {'bad_invoice':f'The invoice provided has already expired'} + return False, context, None, None, None, None + + description = payreq_decoded.expiry.description + payment_hash = payreq_decoded.payment_hash + + return True, None, description, payment_hash, created_at, expires_at def pay_invoice(invoice): '''Sends sats to buyer, or cancelinvoices''' @@ -92,14 +125,6 @@ class LNNode(): return True - def settle_hold_htlcs(payment_hash): - '''Charges a LN hold invoice''' - return True - - def return_hold_htlcs(payment_hash): - '''Returns sats''' - return True - def double_check_htlc_is_settled(payment_hash): ''' Just as it sounds. Better safe than sorry!''' return True diff --git a/api/logics.py b/api/logics.py index 7025d671..95c1ef5c 100644 --- a/api/logics.py +++ b/api/logics.py @@ -254,7 +254,7 @@ class Logics(): description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.' # Gen hold Invoice - invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + invoice, preimage, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.maker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.MAKEBOND, @@ -262,6 +262,7 @@ class Logics(): sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), invoice = invoice, + preimage = preimage, status = LNPayment.Status.INVGEN, num_satoshis = bond_satoshis, description = description, diff --git a/api/models.py b/api/models.py index 36b926de..dafa883e 100644 --- a/api/models.py +++ b/api/models.py @@ -46,6 +46,7 @@ class LNPayment(models.Model): # payment info invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) payment_hash = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + preimage = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) description = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() diff --git a/setup.md b/setup.md index e601ec9d..67865b4d 100644 --- a/setup.md +++ b/setup.md @@ -58,6 +58,11 @@ git clone https://github.com/googleapis/googleapis.git curl -o lightning.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/lightning.proto python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. lightning.proto ``` +We also use the *Invoices* subservice for invoice validation. +``` +curl -o invoices.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/invoicesrpc/invoices.proto +python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. invoices.proto +``` ## React development environment ### Install npm