From 9f65a5adb651cc2acfa411c7f13a7b2ae47a351a Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Mon, 10 Jan 2022 17:02:06 -0800 Subject: [PATCH] Add validating LN invoices and generaing hold invoices --- api/lightning/node.py | 52 ++++++++++++++++++----------- api/logics.py | 15 +++++---- api/models.py | 12 +++---- frontend/src/components/TradeBox.js | 13 ++++++-- setup.md | 10 ++++++ 5 files changed, 68 insertions(+), 34 deletions(-) diff --git a/api/lightning/node.py b/api/lightning/node.py index 0b050c73..45870041 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -1,6 +1,6 @@ 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 . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub +from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub from decouple import config from base64 import b64decode @@ -19,23 +19,23 @@ LND_GRPC_HOST = config('LND_GRPC_HOST') class LNNode(): os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA' - 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): + @classmethod + def decode_payreq(cls, invoice): '''Decodes a lightning payment request (invoice)''' request = lnrpc.PayReqString(pay_req=invoice) - response = lightningstub.DecodePayReq(request, metadata=[('macaroon', MACAROON.hex())]) + response = cls.lightningstub.DecodePayReq(request, metadata=[('macaroon', MACAROON.hex())]) return response - def cancel_return_hold_invoice(payment_hash): + @classmethod + def cancel_return_hold_invoice(cls, 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())]) + response = cls.invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())]) # Fix this: tricky because canceling sucessfully an invoice has no response. TODO if response == None: @@ -43,11 +43,12 @@ class LNNode(): else: return False - def settle_hold_invoice(preimage): + @classmethod + def settle_hold_invoice(cls, 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 + # Fix this: tricky because settling sucessfully an invoice has no response. TODO if response == None: return True else: @@ -57,7 +58,7 @@ class LNNode(): def gen_hold_invoice(cls, num_satoshis, description, expiry): '''Generates hold invoice''' - # The preimage will be 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() # Its hash is used to generate the hold invoice @@ -68,20 +69,28 @@ class LNNode(): value=num_satoshis, hash=preimage_hash, expiry=expiry) - response = invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())]) + 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) return invoice, preimage, payment_hash, created_at, expires_at - def validate_hold_invoice_locked(payment_hash): + @classmethod + def validate_hold_invoice_locked(cls, payment_hash): '''Checks if hodl invoice is locked''' + return True + + @classmethod + def check_until_invoice_locked(cls, payment_hash, expiration): + '''Checks until hodl invoice is locked''' + # request = ln.InvoiceSubscription() # When invoice is settled, return true. If time expires, return False. # for invoice in stub.SubscribeInvoices(request): @@ -96,10 +105,10 @@ class LNNode(): try: payreq_decoded = cls.decode_payreq(invoice) except: - return False, {'bad_invoice':'Does not look like a valid lightning invoice'} + return False, {'bad_invoice':'Does not look like a valid lightning invoice'}, None, None, None, None if not payreq_decoded.num_satoshis == num_satoshis: - context = {'bad_invoice':f'The invoice provided is not for {num_satoshis}'} + context = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'} return False, context, None, None, None, None created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) @@ -109,23 +118,26 @@ class LNNode(): context = {'bad_invoice':f'The invoice provided has already expired'} return False, context, None, None, None, None - description = payreq_decoded.expiry.description + description = payreq_decoded.description payment_hash = payreq_decoded.payment_hash return True, None, description, payment_hash, created_at, expires_at - def pay_invoice(invoice): + @classmethod + def pay_invoice(cls, invoice): '''Sends sats to buyer, or cancelinvoices''' return True - def check_if_hold_invoice_is_locked(payment_hash): + @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 - def double_check_htlc_is_settled(payment_hash): + @classmethod + def double_check_htlc_is_settled(cls, payment_hash): ''' Just as it sounds. Better safe than sorry!''' return True diff --git a/api/logics.py b/api/logics.py index 95c1ef5c..02d4b1f4 100644 --- a/api/logics.py +++ b/api/logics.py @@ -118,7 +118,8 @@ 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, expires_at = LNNode.validate_ln_invoice(invoice, num_satoshis) + valid, context, description, payment_hash, created_at, expires_at = LNNode.validate_ln_invoice(invoice, num_satoshis) + if not valid: return False, context @@ -134,13 +135,14 @@ class Logics(): 'num_satoshis' : num_satoshis, 'description' : description, 'payment_hash' : payment_hash, + 'created_at' : created_at, 'expires_at' : expires_at} ) - # If the order status is 'Waiting for invoice'. Move forward to 'waiting for invoice' + # If the order status is 'Waiting for escrow'. Move forward to 'chat' if order.status == Order.Status.WFE: order.status = Order.Status.CHA - # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' or to 'chat' + # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' if order.status == Order.Status.WF2: print(order.trade_escrow) if order.trade_escrow: @@ -251,10 +253,10 @@ 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.' + description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat or unilaterally cancel' # Gen hold Invoice - invoice, preimage, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + invoice, preimage, payment_hash, created_at, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.maker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.MAKEBOND, @@ -267,6 +269,7 @@ class Logics(): num_satoshis = bond_satoshis, description = description, payment_hash = payment_hash, + created_at = created_at, expires_at = expires_at) order.save() @@ -291,7 +294,7 @@ 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.' + description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat or unilaterally cancel' # Gen hold Invoice invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) diff --git a/api/models.py b/api/models.py index dafa883e..4de3ce03 100644 --- a/api/models.py +++ b/api/models.py @@ -44,13 +44,13 @@ class LNPayment(models.Model): routing_retries = models.PositiveSmallIntegerField(null=False, default=0) # 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() + invoice = models.CharField(max_length=500, unique=True, null=True, default=None, blank=True) + payment_hash = models.CharField(max_length=100, unique=True, null=True, default=None, blank=True) + preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True) + description = models.CharField(max_length=150, unique=False, null=True, default=None, blank=True) num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) + created_at = models.DateTimeField() + expires_at = models.DateTimeField() # involved parties sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None) diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 229f09f2..a7ec4e88 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -27,6 +27,9 @@ function pn(x) { export default class TradeBox extends Component { constructor(props) { super(props); + this.state = { + badInvoice: false, + } } showQRInvoice=()=>{ @@ -164,13 +167,16 @@ export default class TradeBox extends Component { handleInputInvoiceChanged=(e)=>{ this.setState({ - invoice: e.target.value, + invoice: e.target.value, + badInvoice: false, }); } // Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage. handleClickSubmitInvoiceButton=()=>{ + this.setState({badInvoice:false}); + const requestOptions = { method: 'POST', headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, @@ -181,7 +187,8 @@ export default class TradeBox extends Component { }; fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions) .then((response) => response.json()) - .then((data) => (this.props.data = data)); + .then((data) => this.setState({badInvoice:data.bad_invoice}) + & console.log(data)); } showInputInvoice(){ @@ -204,6 +211,8 @@ export default class TradeBox extends Component {