Stage 4 - Webapp Integration
In this stage, we will integrate Lightning micropayments into our server-side web application.
Starting Afresh
It’s time to recreate the micropayments ourselves. Get the starter code:
# Clear any local changes first and checkout the start tag
git checkout -- .
git checkout start
The starter code has stripped out all micropayments code, and we just also just reset our database state to the default.
The models
First, let’s examine coindesk/models.py
to understand our web app a little
better.
coindesk/models.py
(cleaned up a little bit):
class Profile(models.Model):
user = models.OneToOneField(User)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
identity_pubkey = models.CharField(max_length=80, unique=True)
# bitcoin_address = BitcoinAddressField()
class Article(models.Model):
ARTICLE_STATUS_CHOICES = (
('visible', 'Visible'),
('deleted by admin', 'Deleted by admin'),
)
title = models.CharField(max_length=191)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=50, default='visible', choices=ARTICLE_STATUS_CHOICES)
@property
def views(self):
return self.payments.filter(status='complete', purpose='view').count()
class Payment(models.Model):
PAYMENT_STATUS_CHOICES = (
('pending_invoice', 'Pending Invoice'), # Should be atomic
('pending_payment', 'Pending Payment'),
('complete', 'Complete'),
('error', 'Error'),
)
PAYMENT_PURPOSE_CHOICES = (
('view', 'View'),
('upvote', 'Upvote')
)
user = models.ForeignKey(User)
article = models.ForeignKey(Article, related_name='payments')
purpose = models.CharField(max_length=10, choices=PAYMENT_PURPOSE_CHOICES)
satoshi_amount = models.IntegerField()
r_hash = models.CharField(max_length=64)
payment_request = models.CharField(max_length=1000)
status = models.CharField(max_length=50, default='pending_invoice', choices=PAYMENT_STATUS_CHOICES)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
def generate_invoice(self, user, article):
"""
Generates a new invoice
"""
pass
def check_payment(self):
"""
Checks if the Lightning payment has been received for this invoice
"""
pass
Each Profile
object is associated with one User
object, and stores the
identity pubkey of that user.
The Article
class has a title, text, a timestamp and some basic moderation
functionality. Notice that the number of views for an article as counted as the
number of completed payments associated with it.
The Payment
class represents a Lightning micropayment, and is going to be
where we implement the bulk of our work. Each payment is associated with a particular user and a article.
We have the satoshi_amount
, r_hash
and payment_request
fields - the basic technical information
required to conduct a Lightning payment.
Each Payment
goes through the
following lifecycle, represented by the status
field:
- The payment is initially pending an invoice. Calling
generate_invoice
will generate an invoice for a user and a particular article they want to view. - After the invoice is generated, it is now pending payment.
check_payment
will check if a payment has indeed been received by Bob node. Afterwards, thisPayment
counts as complete, and the user should be able to view the article.
It will soon be up to us to implement the generate_invoice
and
check_payment
functions. But first, let’s add in some security.
The views
We also need to understand some of the wiring behind the app.
Open coindesk/views.py
. For our purposes, the article
view is the most
important since it is executed when we view an article and handles what to do
with the different payment states.
def article(request, pk):
try:
article = Article.objects.filter(status='visible').get(id=pk)
except Article.DoesNotExist:
raise Exception("Article with id {} does not exist or is not visible".format(pk))
context = {'article': article}
if not request.user.is_authenticated():
return render(request,
template_name='article.html',
context=context)
qs = article.payments.filter(user=request.user, purpose='view')
if qs.count() == 0:
# Generate a new payment
payment, _ = Payment.objects.get_or_create(user=request.user,
article=article,
purpose='view',
satoshi_amount=settings.MIN_VIEW_AMOUNT,
status='pending_invoice')
payment.save()
elif qs.count() == 1:
payment = qs.last()
else:
# This should not happen because there should never be more than one view payment per article per person
raise Exception("Multiple payments detected")
# User client requests that we check if the payment has been made
if request.GET.get('check'):
print "Checking for payment {}".format(payment.payment_request)
if payment.check_payment():
print "Payment succeeded!"
else:
print "Payment not received"
if payment.status == 'pending_invoice':
payment.generate_invoice(request.user, article)
elif payment.status == 'pending_payment':
# Do nothing; display the payment page to user
pass
elif payment.status == 'complete':
# Do nothing; display the article to user
pass
elif payment.status == 'error':
# TODO Optionally implement some kind of error resolution
pass
raise Exception("Payment error")
else:
context['payment_status'] = payment.status
context['payment'] = payment
return render(request,
template_name='article.html',
context=context)
A quick breakdown of what’s going on:
- First, we try to find the appropriate
Article
by its id. If successful, we populate thecontext
variable so that the template can work with it. - If the user is not logged in, we can’t check for a payment from them because we don’t know who they are, so we just direct them to the article page.
- We attempt to find a
Payment
for this particularUser
andArticle
. If aPayment
was not found, we generate a new one. The satoshi amount is set bysettings.MIN_VIEW_AMOUNT
, which defaults to 1000 satoshis. This newly createdPayment
has the initial state:pending_invoice
. - If the user provides the
check
query parameter, we call thecheck_payment
function, which will check if the payment was indeed complete and update the state if so. - If the payment is pending an invoice, we call the
generate_invoice
function, which will generate the invoice and update the Payment state. - If the payment is in any other state, we render
article.html
for the user or throw an error.
Adding in a paywall
Right now, all the articles in the app are freely viewable. We all know no one likes free stuff, so let’s fix that.
We will begin by adding some conditionals in the html where we are currently displaying the article content.
In coindesk/templates/article.html
, notice the following code:
<div class="article-body">
<div class="article-text">
{{ article.text }}
</div>
</div>
{{ article.text }}
is the Django template variable
representing the text of the website. We need to hide this for all
non-authenticated users. Let’s show them the pay.html
page instead. And of
course, users who have completed payments must be able to view the article
text.
Make the necessary edits:
<div class="article-body">
{% if not request.user.is_authenticated %}
<h3>Make Payment</h3>
<p>
To view this article, please log in and return to this page to make
a payment.
</p>
<a id="complete_button" href="/login">Log in</a>
{% elif payment.status == "complete" %}
<div class="article-text">
{{ article.text }}
</div>
{% else %}
<div>
{% include "pay.html" %}
</div>
{% endif %}
</div>
As this is a Lightning tutorial, understanding the exact syntax is not important, but a quick clarification may be helpful:
- The
{% if %}
blocks are Django’s way of adding conditionals in html templates. We are using this to determine if users are logged in or if the payment is complete. payment
is a context variable the template needs populated in order to render correctly and access the relevant information. This was done in thearticle
view.- The
{% include %}
tag adds in the template by the name ofpay.html
.
We should now see something like this when we click on an article:
Generating an invoice
Examine coindesk/templates/pay.html
:
<div id="payment" class="ln-dialog">
<div id="submit_box" class="animated fadeInDown delay">
<h3>Make Payment</h3>
<p>To view this article, please pay 1000 satoshis via the request below:</p>
<p class="small">{{ payment.payment_request }}</p><br>
<a id="complete_button" href="/articles/{{ article.id }}/?check=true">Complete</a>
</div>
</div>
This template prompts the user to pay 1000 satoshis, and supplies a payment request.
Log in as Alice. The page should look something like this:
We are missing the payment request, so let’s generate an invoice.
Navigate to generate_invoice
in coindesk/models.py
. Modify it to the
following:
def generate_invoice(self, user, article):
"""
Generates a new invoice
"""
assert self.status == 'pending_invoice', "Already generated invoice"
channel = grpc.insecure_channel(settings.LND_RPCHOST)
stub = lnrpc.LightningStub(channel)
add_invoice_resp = stub.AddInvoice(ln.Invoice(amt=settings.MIN_VIEW_AMOUNT, memo="User '{}' | ArticleId {}".format(user.username, article.id)))
r_hash_base64 = codecs.encode(add_invoice_resp.r_hash, 'base64')
self.r_hash = r_hash_base64.decode('utf-8')
self.payment_request = add_invoice_resp.payment_request
self.status = 'pending_payment'
self.save()
Don’t forget also to include the necessary imports at the top of the file:
from coindesk import rpc_pb2 as ln, rpc_pb2_grpc as lnrpc
import grpc
Walking through the code:
- We first check that this
Payment
is indeed pending an invoice, and throw an error otherwise. - Using gRPC, we add an invoice of the amount set by
MIN_VIEW_AMOUNT
insettings.py
. - We set the
r_hash
andpayment_request
attributes and update the state of thisPayment
to now be pending payment.
Checking for payment receipt
The last piece needed for our fully functional Lightning Coindesk is to
implement the check_payment
function of the Payment
class. Recall from
coindesk.views.
that check_payment()
is called if the user passes along the
check
query parameter. For your convenience, we have already implemented this
for you in the frontend:
In coindesk/templates/pay.html
:
<a id="complete_button" href="/articles//?check=true">Complete</a>
Now, try to implement the check_payment
function yourself. Here’s some
starter code to help you with the annoying and unimportant parts:
def check_payment(self):
"""
Checks if the Lightning payment has been received for this invoice
"""
if self.status == 'pending_invoice':
return False
# Prepare r_hash_bytes, which can be passed into the gRPC client
r_hash_base64 = self.r_hash.encode('utf-8')
r_hash_bytes = str(codecs.decode(r_hash_base64, 'base64'))
# Implement this
payment_settled = False
if payment_settled:
# Payment complete
self.status = 'complete'
self.save()
return True
else:
# Payment not received
return False
If you’re absolutely stuck, you may refer to the tutorial repo for the final solution.
Conclusion
Congratulations! You have now built a working Lightning Coindesk app! If you are looking to take this tutorial to the next level, consider trying one or more of the following:
- Integrate error handling in the webapp; see the TODO in
coindesk.views.article
- Add P2P payments through the app. Instead of paying the central server, distribute your micropayment equally between all the people who upvoted before you. The upvote functionality was intentionally left unimplemented for this purpose.
- Run this application in a more “production” environment, by using
testnet
and deploying your webapp online - Allow the server to accept Litecoin in addition to Bitcoin.
At this point, you’re ready to build with lnd
. Go forth and bring
Bitcoin to the masses!
Next Steps
Read through the LND Overview and Developer Guide to see best practices and guidelines for implementing LND in your own webapp, or for a conceptual brushup of the Lightning Network.
Check out the accessible dev manuals available on our Guides page.
Navigation
Questions
- Join the #dev-help channel on our Community Slack
- Join IRC: