mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 19:06:26 +00:00
Init work on Lightning functionality
This commit is contained in:
parent
db36277456
commit
9c2f50dacf
10
.env-sample
10
.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 public API
|
||||||
MARKET_PRICE_API = 'https://blockchain.info/ticker'
|
MARKET_PRICE_API = 'https://blockchain.info/ticker'
|
||||||
|
|
||||||
@ -5,8 +11,8 @@ MARKET_PRICE_API = 'https://blockchain.info/ticker'
|
|||||||
FEE = 0.002
|
FEE = 0.002
|
||||||
# Bond size in percentage %
|
# Bond size in percentage %
|
||||||
BOND_SIZE = 0.01
|
BOND_SIZE = 0.01
|
||||||
# Time out penalty for canceling takers in MINUTES
|
# Time out penalty for canceling takers in SECONDS
|
||||||
PENALTY_TIMEOUT = 2
|
PENALTY_TIMEOUT = 60
|
||||||
|
|
||||||
# Trade limits in satoshis
|
# Trade limits in satoshis
|
||||||
MIN_TRADE = 10000
|
MIN_TRADE = 10000
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -642,4 +642,5 @@ frontend/static/frontend/main*
|
|||||||
# robosats
|
# robosats
|
||||||
frontend/static/assets/avatars*
|
frontend/static/assets/avatars*
|
||||||
api/lightning/lightning*
|
api/lightning/lightning*
|
||||||
|
api/lightning/invoices*
|
||||||
api/lightning/googleapis*
|
api/lightning/googleapis*
|
||||||
|
@ -1,37 +1,83 @@
|
|||||||
import codecs, grpc, os
|
import grpc, os, hashlib, secrets, json
|
||||||
from . import lightning_pb2 as ln
|
import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
|
||||||
from . import lightning_pb2_grpc as lnrpc
|
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
|
from django.utils import timezone
|
||||||
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
#######
|
#######
|
||||||
# Placeholder functions
|
# Should work with LND (c-lightning in the future if there are features that deserve the work)
|
||||||
# Should work with LND (maybe c-lightning in the future)
|
#######
|
||||||
|
|
||||||
|
CERT = b64decode(config('LND_CERT_BASE64'))
|
||||||
|
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||||
|
LND_GRPC_HOST = config('LND_GRPC_HOST')
|
||||||
|
|
||||||
class LNNode():
|
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'
|
||||||
# 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
|
|
||||||
|
|
||||||
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):
|
def validate_hold_invoice_locked(payment_hash):
|
||||||
'''Checks if hodl invoice is locked'''
|
'''Checks if hodl invoice is locked'''
|
||||||
@ -43,43 +89,30 @@ class LNNode():
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_ln_invoice(invoice, num_satoshis):
|
@classmethod
|
||||||
'''Checks if the submited LN invoice is as expected'''
|
def validate_ln_invoice(cls, invoice, num_satoshis):
|
||||||
|
'''Checks if the submited LN invoice comforms to expectations'''
|
||||||
|
|
||||||
# request = lnrpc.PayReqString(pay_req=invoice)
|
try:
|
||||||
# response = stub.DecodePayReq(request, metadata=[('macaroon', macaroon)])
|
payreq_decoded = cls.decode_payreq(invoice)
|
||||||
|
except:
|
||||||
|
return False, {'bad_invoice':'Does not look like a valid lightning invoice'}
|
||||||
|
|
||||||
# # {
|
if not payreq_decoded.num_satoshis == num_satoshis:
|
||||||
# # "destination": <string>,
|
context = {'bad_invoice':f'The invoice provided is not for {num_satoshis}'}
|
||||||
# # "payment_hash": <string>,
|
return False, context, None, None, None, None
|
||||||
# # "num_satoshis": <int64>,
|
|
||||||
# # "timestamp": <int64>,
|
|
||||||
# # "expiry": <int64>,
|
|
||||||
# # "description": <string>,
|
|
||||||
# # "description_hash": <string>,
|
|
||||||
# # "fallback_addr": <string>,
|
|
||||||
# # "cltv_expiry": <int64>,
|
|
||||||
# # "route_hints": <array RouteHint>,
|
|
||||||
# # "payment_addr": <bytes>,
|
|
||||||
# # "num_msat": <int64>,
|
|
||||||
# # "features": <array FeaturesEntry>,
|
|
||||||
# # }
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
valid = True
|
created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||||
context = None
|
expires_at = created_at + timedelta(seconds=payreq_decoded.expiry)
|
||||||
description = 'Placeholder desc' # TODO decrypt from LN invoice
|
|
||||||
payment_hash = '567&*GIHU126' # TODO decrypt
|
|
||||||
expires_at = timezone.now() # TODO decrypt
|
|
||||||
|
|
||||||
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):
|
def pay_invoice(invoice):
|
||||||
'''Sends sats to buyer, or cancelinvoices'''
|
'''Sends sats to buyer, or cancelinvoices'''
|
||||||
@ -92,14 +125,6 @@ class LNNode():
|
|||||||
|
|
||||||
return True
|
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):
|
def double_check_htlc_is_settled(payment_hash):
|
||||||
''' Just as it sounds. Better safe than sorry!'''
|
''' Just as it sounds. Better safe than sorry!'''
|
||||||
return True
|
return True
|
||||||
|
@ -254,7 +254,7 @@ class Logics():
|
|||||||
description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.'
|
description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.'
|
||||||
|
|
||||||
# Gen hold Invoice
|
# 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(
|
order.maker_bond = LNPayment.objects.create(
|
||||||
concept = LNPayment.Concepts.MAKEBOND,
|
concept = LNPayment.Concepts.MAKEBOND,
|
||||||
@ -262,6 +262,7 @@ class Logics():
|
|||||||
sender = user,
|
sender = user,
|
||||||
receiver = User.objects.get(username=ESCROW_USERNAME),
|
receiver = User.objects.get(username=ESCROW_USERNAME),
|
||||||
invoice = invoice,
|
invoice = invoice,
|
||||||
|
preimage = preimage,
|
||||||
status = LNPayment.Status.INVGEN,
|
status = LNPayment.Status.INVGEN,
|
||||||
num_satoshis = bond_satoshis,
|
num_satoshis = bond_satoshis,
|
||||||
description = description,
|
description = description,
|
||||||
|
@ -46,6 +46,7 @@ class LNPayment(models.Model):
|
|||||||
# payment info
|
# payment info
|
||||||
invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
|
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)
|
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)
|
description = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
expires_at = models.DateTimeField()
|
expires_at = models.DateTimeField()
|
||||||
|
5
setup.md
5
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
|
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
|
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
|
## React development environment
|
||||||
### Install npm
|
### Install npm
|
||||||
|
Loading…
Reference in New Issue
Block a user