Add validating LN invoices and generaing hold invoices

This commit is contained in:
Reckless_Satoshi
2022-01-10 17:02:06 -08:00
parent 9c2f50dacf
commit 9f65a5adb6
5 changed files with 68 additions and 34 deletions

View File

@ -1,6 +1,6 @@
import grpc, os, hashlib, secrets, json import grpc, os, hashlib, secrets, json
import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
from decouple import config from decouple import config
from base64 import b64decode from base64 import b64decode
@ -19,23 +19,23 @@ LND_GRPC_HOST = config('LND_GRPC_HOST')
class LNNode(): class LNNode():
os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA' os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA'
creds = grpc.ssl_channel_credentials(CERT) creds = grpc.ssl_channel_credentials(CERT)
channel = grpc.secure_channel(LND_GRPC_HOST, creds) channel = grpc.secure_channel(LND_GRPC_HOST, creds)
lightningstub = lightningstub.LightningStub(channel) lightningstub = lightningstub.LightningStub(channel)
invoicesstub = invoicesstub.InvoicesStub(channel) invoicesstub = invoicesstub.InvoicesStub(channel)
def decode_payreq(invoice): @classmethod
def decode_payreq(cls, invoice):
'''Decodes a lightning payment request (invoice)''' '''Decodes a lightning payment request (invoice)'''
request = lnrpc.PayReqString(pay_req=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 return response
def cancel_return_hold_invoice(payment_hash): @classmethod
def cancel_return_hold_invoice(cls, payment_hash):
'''Cancels or returns a hold invoice''' '''Cancels or returns a hold invoice'''
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash)) 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 # Fix this: tricky because canceling sucessfully an invoice has no response. TODO
if response == None: if response == None:
@ -43,11 +43,12 @@ class LNNode():
else: else:
return False return False
def settle_hold_invoice(preimage): @classmethod
def settle_hold_invoice(cls, preimage):
# SETTLING A HODL INVOICE # SETTLING A HODL 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 canceling sucessfully an invoice has no response. TODO # Fix this: tricky because settling sucessfully an invoice has no response. TODO
if response == None: if response == None:
return True return True
else: else:
@ -57,7 +58,7 @@ 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'''
# 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() 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
@ -68,20 +69,28 @@ class LNNode():
value=num_satoshis, value=num_satoshis,
hash=preimage_hash, hash=preimage_hash,
expiry=expiry) expiry=expiry)
response = invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())]) response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])
invoice = response.payment_request invoice = response.payment_request
payreq_decoded = cls.decode_payreq(invoice) payreq_decoded = cls.decode_payreq(invoice)
preimage = preimage.hex()
payment_hash = payreq_decoded.payment_hash payment_hash = payreq_decoded.payment_hash
created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
expires_at = created_at + timedelta(seconds=payreq_decoded.expiry) expires_at = created_at + timedelta(seconds=payreq_decoded.expiry)
return invoice, preimage, payment_hash, created_at, expires_at 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''' '''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() # request = ln.InvoiceSubscription()
# When invoice is settled, return true. If time expires, return False. # When invoice is settled, return true. If time expires, return False.
# for invoice in stub.SubscribeInvoices(request): # for invoice in stub.SubscribeInvoices(request):
@ -96,10 +105,10 @@ class LNNode():
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'} return False, {'bad_invoice':'Does not look like a valid lightning invoice'}, None, None, None, None
if not payreq_decoded.num_satoshis == num_satoshis: 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 return False, context, None, None, None, None
created_at = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) 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'} context = {'bad_invoice':f'The invoice provided has already expired'}
return False, context, None, None, None, None return False, context, None, None, None, None
description = payreq_decoded.expiry.description description = payreq_decoded.description
payment_hash = payreq_decoded.payment_hash payment_hash = payreq_decoded.payment_hash
return True, None, description, payment_hash, created_at, expires_at 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''' '''Sends sats to buyer, or cancelinvoices'''
return True 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 '''Every hodl invoice that is in state INVGEN
Has to be checked for payment received until Has to be checked for payment received until
the window expires''' the window expires'''
return True 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!''' ''' Just as it sounds. Better safe than sorry!'''
return True return True

View File

@ -118,7 +118,8 @@ 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, 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: if not valid:
return False, context return False, context
@ -134,13 +135,14 @@ class Logics():
'num_satoshis' : num_satoshis, 'num_satoshis' : num_satoshis,
'description' : description, 'description' : description,
'payment_hash' : payment_hash, 'payment_hash' : payment_hash,
'created_at' : created_at,
'expires_at' : expires_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 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: if order.status == Order.Status.WF2:
print(order.trade_escrow) print(order.trade_escrow)
if order.trade_escrow: if order.trade_escrow:
@ -251,10 +253,10 @@ 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.' description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat or unilaterally cancel'
# Gen hold Invoice # 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( order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND, concept = LNPayment.Concepts.MAKEBOND,
@ -267,6 +269,7 @@ class Logics():
num_satoshis = bond_satoshis, num_satoshis = bond_satoshis,
description = description, description = description,
payment_hash = payment_hash, payment_hash = payment_hash,
created_at = created_at,
expires_at = expires_at) expires_at = expires_at)
order.save() order.save()
@ -291,7 +294,7 @@ 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.' description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat or unilaterally cancel'
# Gen hold Invoice # Gen hold Invoice
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)

View File

@ -44,13 +44,13 @@ class LNPayment(models.Model):
routing_retries = models.PositiveSmallIntegerField(null=False, default=0) routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
# payment info # payment info
invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) invoice = models.CharField(max_length=500, unique=True, 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=100, unique=True, null=True, default=None, blank=True)
preimage = models.CharField(max_length=300, unique=False, 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=300, unique=False, null=True, default=None, blank=True) description = models.CharField(max_length=150, unique=False, null=True, default=None, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) 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 # involved parties
sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None) sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)

View File

@ -27,6 +27,9 @@ function pn(x) {
export default class TradeBox extends Component { export default class TradeBox extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
badInvoice: false,
}
} }
showQRInvoice=()=>{ showQRInvoice=()=>{
@ -165,12 +168,15 @@ export default class TradeBox extends Component {
handleInputInvoiceChanged=(e)=>{ handleInputInvoiceChanged=(e)=>{
this.setState({ 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. // Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
handleClickSubmitInvoiceButton=()=>{ handleClickSubmitInvoiceButton=()=>{
this.setState({badInvoice:false});
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, 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) fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => (this.props.data = data)); .then((data) => this.setState({badInvoice:data.bad_invoice})
& console.log(data));
} }
showInputInvoice(){ showInputInvoice(){
@ -204,6 +211,8 @@ export default class TradeBox extends Component {
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<TextField <TextField
error={this.state.badInvoice}
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
label={"Payout Lightning Invoice"} label={"Payout Lightning Invoice"}
required required
inputProps={{ inputProps={{

View File

@ -63,6 +63,16 @@ 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 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 python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. invoices.proto
``` ```
Relative imports are not working at the moment, so some editing is needed in
`api/lightning` files `lightning_pb2_grpc.py`, `invoices_pb2_grpc.py` and `invoices_pb2.py`.
Example, change line :
`import lightning_pb2 as lightning__pb2`
to
`from . import lightning_pb2 as lightning__pb2`
## React development environment ## React development environment
### Install npm ### Install npm