mirror of
https://github.com/RoboSats/robosats.git
synced 2025-07-22 02:33:17 +00:00
Add validating LN invoices and generaing hold invoices
This commit is contained in:
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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={{
|
||||||
|
10
setup.md
10
setup.md
@ -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
|
||||||
|
Reference in New Issue
Block a user