LightningJ - Paywall, Enabling Micropayments for Microservices. V 0.1.0, 2019-08-25
1. Introduction
LightningJ Paywall is a project aimed at providing a simple to use framework to implement micro-payment functionality to Java oriented Web Services using the Lightning Network. It is built to support Spring Framework and Spring Boot.
More information and source code can be found on the Github site. There it is possible to report issues and contribute code.
If you want updates on this project, follow https://twitter.com/LightningJ_org on twitter.
Important: This API is still in beta and API methods might change before real production release.
All use of this library is on your own risk.
1.1. Overview
The LightningJ Paywall Framework has a goal to provide micro payment functionality to web services in Java.
-
It contains a Java library with core functionality and a Spring Framework library for use in Spring and Spring Boot applications.
-
REST Services can be annotated with a @PaymentRequired annotation, triggering the need for a payment flow.
-
The library uses a LND Lightning Node in the backend to manage invoices.
-
It provides a JavaScript library extending the standard XMLHttpRequest class with paywall functionality, automatically regenerating a request after settlement.
-
To minimize the latency times after settling an invoice is a WebSocket interface provided to push out settlement data to the browser so it can regenerate the request as soon as possible.
The framework aims to simply cloud deployment by being as stateless as possible (on server side) by utilizing state in encrypted and signed (JWT, Java Web Tokens) authentication tokens. This removes the need for stateful load balancers and complex logic in a clustered environment.
In the initial step is all functionality contained in the same application, i.e connection to Lightning Node and end-point for checking state, but in the future will more distributed configuration be available in order to make the paywalled service to be as minimal as possible.
See Getting Started section for a quick run through of the project.
1.2. License
This library is Open Source and released under the LGPL v3 License. A link to the license agreement can be found here.
1.3. Whats New
-
0.1.0-beta, Inital release.
1.4. Roadmap
The framework will improve overtime with features requested, some ideas of future features are:
-
Support HODL Invoices.
-
Javascript API should be packaged and uploaded to central repository.
-
Support a distributed payment flow other that local where lightning node connection can be managed from a central system.
-
Improved PaymentEventBus, possibility to subscribe to payment related events.
1.5. Release Distribution
All releases of the LightningJ Paywall framework are uploaded to maven central to be used with Maven or Gradle under the group org.lightningj.paywall.
1.5.1. Release Signature Key
All releases are signed with the following GPG Key.
GPG Key Fingerprint:
7C0F 80B8 BD9F E3B8 1388 4BA1 9515 B31D DD9B BCCD
2. Getting Started
In this section we will build a simple REST Service based application that required payment for each call using the Paywall library.
It will be a web service that provides a Bitcoin Price TA ( Technical Analysis ) Price Prediction Service, with a guaranteed correct prediction rate of 50%. We will then charge 10 Satoshis per prediction.
2.1. Prerequisites
Before you start development you need to set the following in your test environment.
-
JDK 8 or JDK 11 installed.
-
Access to a local LND node, version 0.7.0 or up, preferable on Bitcoin Testnet.
Instructions to set up or install an LND node can be found at LND Site. After finished installing the LND node you will also need to find the TLS certificate and the invoice.macaroon file generated after LND startup. You also need to retrieve the connect string by issuing the command:
lncli getinfo
The connection string is one of the uris listed. The port is the one specified using the parameter 'rpclisted' in the LND configuration.
2.2. Generating a Starter Project
In our example we will use Spring Boot to build the service and a good way to start a new project is to go to start.spring.io and generate a skeleton project structure.
In this example is Gradle used with Spring Boot 2.1.7. Enter a group, a project name under artifact and finally add the dependencies: Spring Web Starter, WebSocket and Spring Data JPA to the project before clicking on Generate the Project.
Open up the downloaded ZIP in your favorite IDE and you will get a project structure similar to Figure 2.3.
2.3. Configuring the Project
First we need to add a paywall dependency to the file build.gradle by adding the row:
compile 'org.lightningj.paywall:paywall-spring:0.1.0'
You also need to add database support, in our example we use an in memory database:
implementation 'com.h2database:h2'
Full example of the build.gradle is shown below.
plugins { id 'org.springframework.boot' version '2.1.7.RELEASE' id 'java' } apply plugin: 'io.spring.dependency-management' group = 'org.lightningj.paywall' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { // Add Paywall Spring dependency here compile 'org.lightningj.paywall:paywall-spring:0.1.0' implementation 'com.h2database:h2' // Optionally if you want to use mariadb database instead of in memory, uncomment: //implementation 'org.mariadb.jdbc:mariadb-java-client:2.4.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-websocket' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
Then we need to setup a minimum configuration in the file src/main/resources/application.properties. Here we configure the current paywall profile: paywall_local and connection options to your local LND node. Also required is the location of and password protecting the secret key signing and encrypting the JWT (Java Web Token, used to prove the state of a specific payment in a stateless way).
spring.profiles.active=paywall_local
paywall.lnd.hostname=test3
paywall.lnd.port=10000
paywall.lnd.tlscertpath=/tmp/tlscertpath
paywall.lnd.macaroonpath=/tmp/macroonpath
paywall.lnd.connectstring=8371729292821728191012918129172271827281262611282@10.10.10.1:9735
paywall.keys.keystorepath=~/ta-demo-keys
paywall.keys.password=foobar123
If you want to use mariadb as database instead of the in-memory provided in the example also add the following properties, assuming your mariadb database is called paywallTSDemo (also remember to add the mariadb-java-client dependency in build.gradle):
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mariadb://localhost:3306/paywallTSDemo
spring.datasource.username=paywalltsdemouser
spring.datasource.password=foo124
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
2.4. Creating the REST Service
Next we create our TA Prediction Service that we want to paywall. It is a standard Spring RestController with one method generating a JSON object TADemoResult when called and that is mapped to the URL /tademo.
The magic to require payment for the service is that we annotate it with @PaymentRequired with an article id of tademo and indicates that payment should be done per request. If payPerRequest is set to false it will be possible for the requester to perform multiple requests to the service until the settlement token issued after successful payment is expires. In our example we want payment for every prediction we make.
@RestController
public class TADemoRestController {
private static final String template = "Bitcoin number is probably going %s.";
private final AtomicLong counter = new AtomicLong();
private final SecureRandom taEngine = new SecureRandom();
@PaymentRequired(articleId = "tademo1", payPerRequest = true)
@RequestMapping("/tademo")
public TADemoResult tademo() {
boolean goingUp = taEngine.nextBoolean();
return new TADemoResult(counter.incrementAndGet(),
String.format(template, (goingUp ? "up":"down")),
goingUp);
}
}
The JSON result we return from our service contains an id of this object, a prediction of future price and a boolean, indicating up or down, that can be used in css styling of the HTML. This class has no Paywall specific in it.
public class TADemoResult {
private long id;
private String prediction;
private boolean goingUp;
public TADemoResult(long id, String prediction, boolean goingUp) {
this.id = id;
this.prediction = prediction;
this.goingUp = goingUp;
}
public long getId() {
return id;
}
public String getPrediction() {
return prediction;
}
public void setId(long id) {
this.id = id;
}
public void setPrediction(String prediction) {
this.prediction = prediction;
}
public boolean isGoingUp() {
return goingUp;
}
public void setGoingUp(boolean goingUp) {
this.goingUp = goingUp;
}
}
2.5. The Required PaymentHandler
The Paywall Framework requires one component to be implemented by the target application. And that is a PaymentHandler. It is in charge of creating and maintaining PaymentData, i.e. value objects about a payment that goes through the payment flow (order, invoice, settlement) and persist them.
The PaymentHandler we will implement will use two database tables. One is ArticleData, containing an article id to price relation in order to avoid hard coding the price for a given service. The other is table is if type PaymentData that support pay per request calls. We call this class DemoPerRequestPaymentData.
Finally we will implement the actual PaymentHandler by extending the Spring Framework specific base version of PaymentHandlers.
2.5.1. The ArticleData Table
First we create the ArticleData object that is mapped to a database table using Spring Data JPA framework.
It’s a very simple table, It contains an unique id, an articleId used in @PaymentRequired annotations and a price used in generated orders.
@Entity
public class ArticleData {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
@Column(nullable = false, unique = true)
private String articleId;
private long price;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getArticleId() {
return articleId;
}
public void setArticleId(String articleId) {
this.articleId = articleId;
}
public long getPrice() {
return price;
}
public void setPrice(long price) {
this.price = price;
}
}
Then we create a CrudRepository for the class that have one method findByArticleId used to fetch ArticleData by it’s articleId.
public interface ArticleDataRepository extends CrudRepository<ArticleData,Integer> {
ArticleData findByArticleId(String articleId);
}
2.5.2. PaymentData Table
Next is to create the PaymentData table. We will create a payment data containing minimal information to support pay per request payment flows. It contains a unique identifier of the payment flow (preImageHash) the amount invoiced and flags indicating if payment have been settled and executed.
@Entity
public class DemoPerRequestPaymentData implements PerRequestPaymentData {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
@Column(nullable = false)
private String preImageHash;
private long orderAmount;
private boolean settled = false;
private boolean payPerRequest = false;
private boolean executed = false;
/**
* @return Unique Id of database row.
*/
public Integer getId() {
return id;
}
/**
* @param id Unique Id of database row.
*/
public void setId(Integer id) {
this.id = id;
}
/**
* Unique identifier of a payment in the system and also used in LightningHandler
* to identify an invoice. Should be generated by TokenGenerator when
* creating an order and not set manually.
*
* @return the unique identifier of a payment.
*/
@Override
public byte[] getPreImageHash() {
return Base58.decode(this.preImageHash);
}
/**
* @param preImageHash unique identifier of a payment in the system and also used in LightningHandler
* to identify an invoice. Should be generated by TokenGenerator when
* creating an order and not set manually.
*/
@Override
public void setPreImageHash(byte[] preImageHash) {
this.preImageHash = Base58.encodeToString(preImageHash);
}
/**
* @return the requested amount for payment. This can be either a FiatAmount or CryptoAmount but
* always make sure the systems configured CurrencyConverter supports this currency when converting
* into a currency accepted by the LightningHandler later in the payment flow.
*/
@Override
public Amount getOrderAmount() {
return new BTC(orderAmount);
}
/**
* @param orderAmount the requested amount for payment. This can be either a FiatAmount or CryptoAmount but
* always make sure the systems configured CurrencyConverter supports this currency when converting
* into a currency accepted by the LightningHandler later in the payment flow.
*/
@Override
public void setOrderAmount(Amount orderAmount) {
assert orderAmount instanceof CryptoAmount;
this.orderAmount = ((CryptoAmount) orderAmount).getValue();
}
/**
* @return true if related invoice have been settled in full.
*/
@Override
public boolean isSettled() {
return this.settled;
}
/**
* @param settled true if related invoice have been settled in full.
*/
@Override
public void setSettled(boolean settled) {
this.settled = settled;
}
/**
* @return flag indicating that this payment is for one request only. The implementation
* can take the payPerRequest flag from the order request as guidance, but it is the PaymentHandler
* that ultimately decides if payPerRequest should be set.
*/
@Override
public boolean isPayPerRequest() {
return payPerRequest;
}
/**
* @param payPerRequest flag indicating that this payment is for one request only. The implementation
* can take the payPerRequest flag from the order request as guidance, but it is the PaymentHandler
* that ultimately decides if payPerRequest should be set.
*/
@Override
public void setPayPerRequest(boolean payPerRequest) {
this.payPerRequest = payPerRequest;
}
/**
* @return true if related request have been executed, is set after successful processing
* if a payed call and used to indicate that it cannot be processed again.
*/
@Override
public boolean isExecuted() {
return executed;
}
/**
* @param executed true if related request have been executed, is set after successful processing
* if a payed call and used to indicate that it cannot be processed again.
*/
@Override
public void setExecuted(boolean executed) {
this.executed = executed;
}
}
We also create a simple CrudRepository finding PaymentData for a given preImageHash.
/**
* Spring Data repository for DemoPerRequestPaymentData.
*/
public interface DemoPerRequestPaymentDataRepository extends CrudRepository<DemoPerRequestPaymentData,Integer> {
DemoPerRequestPaymentData findByPreImageHash(String preImageHash);
}
2.5.3. The PaymentHandler
Finally we create the actual PaymentHandler bean. Below is an example implementation of a component that extends the SpringPaymentHandler and that lookups up a article id and create an PaymentData and maintains it during the payment flow.
- Bean Registration
-
The class is annotated with the @Component("paymentHandler") that register it as a bean with name paymentHandler so other beans withing Paywall Framework can find it. It is also recommended to add the @ComponentScan("org.lightningj.paywall.spring") as a convention to notify the application to scan the package org.lightningj.paywall.spring for bean configurations and it that way initialize the framework.
- After Initialisation
-
The method afterPropertiesSet() is called after the bean in created and in this case used to bootstrap the article database if not configured. This is optional but if used it is important to remember to call super.afterPropertiesSet().
- newPaymentData Method
-
This is one of three required methods to implement. It receives an OrderRequest, looks up the price from the article id and creates a new PaymentData that is persisted to database.
- findPaymentData Method
-
This method should lookup the related payment data from the unique preImageHash from database.
- updatePaymentData
-
This method should persist the state of PaymentData whenever a related payment event is triggered in the payment flow.
Below is the implementation of the PaymentHandler.
@ComponentScan("org.lightningj.paywall.spring")
@Component("paymentHandler")
public class DemoPaymentHandler extends SpringPaymentHandler {
@Autowired
DemoPerRequestPaymentDataRepository demoPaymentDataRepository;
@Autowired
ArticleDataRepository articleDataRepository;
/**
* Method called after initialization of bean.
*
* Contains bootstrap of article database.
*/
@Override
public void afterPropertiesSet() throws Exception {
// Important call afterPropertiesSet from SpringPaymentHandler
super.afterPropertiesSet();
ArticleData articleData1 = articleDataRepository.findByArticleId("tademo1");
if(articleData1 == null){
articleData1 = new ArticleData();
articleData1.setArticleId("tademo1");
articleData1.setPrice(10);
articleDataRepository.save(articleData1);
}
}
/**
* Method that should generate a new PaymentData for a given order request.
* This is the first call in a payment flow and the implementation should
* look up the order amount from the article id, units and other options in
* the order request.
* <p>
* The generated PaymentData should be at least MinimalPaymentData with preImageHash
* and orderedAmount set.
* <p>
* It is recommended that the PaymentData is persisted in this call but could
* be skipped for performance in certain payment flows.
*
* @param preImageHash the unique preImageHash used to identify a payment flow
* withing a lightning payment.
* @param orderRequest the specification of the payment data that should be created calculated
* from data in the PaymentRequired annotation.
* @return a newly generated PaymentData signaling a new payment flow used to
* create an Order value object.
* @throws IOException if communication exception occurred in underlying components.
* @throws InternalErrorException if internal exception occurred generating new payment data.
*/
@Override
protected PaymentData newPaymentData(byte[] preImageHash, OrderRequest orderRequest) throws IOException, InternalErrorException {
try{
DemoPerRequestPaymentData demoPaymentData = new DemoPerRequestPaymentData();
demoPaymentData.setPreImageHash(preImageHash);
demoPaymentData.setPayPerRequest(orderRequest.isPayPerRequest());
long orderPrice = findArticleById(orderRequest.getArticleId()).getPrice() * orderRequest.getUnits(); // Price in satoshis.
demoPaymentData.setOrderAmount(new BTC(orderPrice));
demoPaymentDataRepository.save(demoPaymentData);
return demoPaymentData;
}catch(Exception e){
if(e instanceof InternalErrorException){
throw e;
}
throw new InternalErrorException("Error occurred saving DemoPaymentData to database: " + e.getMessage(),e);
}
}
/**
* Method to lookup a payment data in the payment handler.
*
* @param preImageHash the unique preImageHash used to identify a payment flow
* withing a lightning payment.
* @return return related payment data or null if not found.
* @throws InternalErrorException if internal exception occurred fetching related payment data.
*/
@Override
protected PaymentData findPaymentData(byte[] preImageHash) throws InternalErrorException {
try{
return demoPaymentDataRepository.findByPreImageHash(Base58.encodeToString(preImageHash));
}catch(Exception e){
throw new InternalErrorException("Error occurred fetching DemoPaymentData from database: " + e.getMessage(),e);
}
}
/**
* Method called on update events about a given payment data. This could be when
* the payment is added as invoice in LND and contains complementary data or when
* the invoice was settled and contains settled flag set and settled amount and date
* (depending on the type of PaymentData used in PaymentHandler).
* <p>
* The related payment data (using preImageHash as unique identifier) is automatically
* looked up and the implementing method should at least persist the updated data.
*
* @param type the type of event such as INVOICE_CREATED or INVOICE_SETTLED.
* @param paymentData the payment data to update and persist.
* @param context the latest known state of the lightning handler. Null if no known state exists.
* @throws InternalErrorException if internal exception occurred updating related payment data.
*/
@Override
protected void updatePaymentData(PaymentEventType type, PaymentData paymentData, LightningHandlerContext context) throws InternalErrorException {
try {
assert paymentData instanceof DemoPerRequestPaymentData;
demoPaymentDataRepository.save((DemoPerRequestPaymentData) paymentData);
}catch(Exception e){
throw new InternalErrorException("Error occurred updating DemoPaymentData to database: " + e.getMessage(),e);
}
}
private ArticleData findArticleById(String articleId) throws InternalErrorException{
ArticleData articleData = articleDataRepository.findByArticleId(articleId);
if(articleData == null){
throw new InternalErrorException("Internal error creating payment data, article id " + articleId + " doesn't exist in database.");
}
return articleData;
}
}
2.6. The Javascript Frontend
The final component that needs to be updated in order to support Lightning payments is the web site front end should display an invoice to the user. The TA application it-self is a very simple one-page html page with Bootstrap styling to make it a bit more pretty.
What we want is to add automatic display of invoice when needed and it should close automatically when settled as shown in figure 2.5.
2.6.1. The HTML Page
We start with creating a index.html file that uses the three required Javascript files, sockjs.js, stomp.js and paywall.js. The page also have a welcome section and a prediction display section, that is shown once the prediction is downloaded from the paywalled REST service. There also exists a modal section that will be shown as soon as an invoice is received.
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Lightning J, Paywall TA Demo</title>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<div class="navbar-collapse collapse w-100 order-1 order-md-0 dual-collapse2">
<a class="navbar-brand justify-content-left" href="#">LightningJ Paywall TA Demo</a>
</div>
<div class="navbar-collapse collapse w-100 order-3 dual-collapse2">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="https://paywall.lightningj.org">Project Doc</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/lightningj-org/paywall">GitHub</a>
</li>
</ul>
</div>
</nav>
<div id="welcomeCard" class="card" >
<div class="card-body">
<h5 class="card-title">Bitcoin Price Prediction Service</h5>
<h6 class="card-subtitle mb-2 text-muted"><i>Cost:</i> 10 Satoshis</h6>
<p class="card-text">Click on button below do receive a Bitcoin price indication with guaranteed 50% prediction accuracy.</p>
<button id="welcomeCardBuyButton" type="button" class="btn btn-primary">Buy Prediction</button>
</div>
</div>
<div id="predictionCard" class="card d-none" >
<div class="card-body">
<h5 class="card-title">Bitcoin Prediction Generated</h5>
<p id="predictionText" class="card-text text-white"></p>
<button id="predictionCardBuyButton" type="button" class="btn btn-primary">Buy New Prediction</button>
<button id="predictionCardResetButton" type="button" class="btn btn-primary">Reset</button>
</div>
</div>
<div id="invoiceModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Invoice Received</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div id="invoiceModalBody" class="modal-body">
</div>
<div class="modal-footer justify-content-left">
<button type="button" class="btn btn-secondary mr-auto" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<div class="fixed-bottom bg-secondary text-white"><i>Disclamer:</i> This is not a real TA application. This is just a demo of LightingJ Paywall Framework. Do NOT consider this as financial advice in any way.</div>
<!-- JavaScript Required for Paywall-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.3.0/sockjs.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js" ></script>
<script src="https://github.com/lightningj-org/paywall/releases/download/v0.0.1/paywall.js" ></script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
2.6.2. The Javascript Part
How the TA REST Service would usually be called to fetch JSON is done by XMLHttpRequest as shown in the example below. This is what we need to enhance to support payments.
function requestTA(){
var xmlHttpRequest = new XMLHttpRequest();
// Create onload event handler called after loading of JSON was complete.
xmlHttpRequest.onload = function(){
if(xmlHttpRequest.status === 200) {
// Process the service response as would be done with a regular XMLHttpRequest
var response = JSON.parse(xmlHttpRequest.responseText);
// Update web page using JSON response
}else{
// Error calling the underlying service.
console.log(xmlHttpRequest.responseText)
alert("Error occurred calling the service.")
}
};
// Open up a connection to the paywalled service.
xmlHttpRequest.open("GET","/tademo");
// Send the data to the service that will trigger the payment flow if required.
xmlHttpRequest.send();
}
What we need to do is to replace the XMLHttpRequest with an instance of PaywallHttpRequest (that extends standard XMLHttpRequest with paywall functionality) and add a few new event handlers for handling invoice and settlement.
After that we need to add three event handlers (It is possible to add more, but there are the minimum when creating a pay per request application):
InvoiceListener::An event listener that will recieve the invoice generated by the Paywall API and that contains all information needed to display the invoice. There is also help methods to display amount in different units (Satoshis in this example) and creates an invoice expiration countdown timer. The event handler will also display the invoice modal.
SettledListener::This eventhandler will cloase the invoice modal since PaywallHttpRequest have autoamtically called the REST service again as soon as it recieved payment.
InvoiceExpiredListener::This evenhandler removes all invoice data in the invoice modal and prints Invoice Expired instead.
See example below for replacement for the previous code:
function requestTA(){
var paywallHttpRequest = new PaywallHttpRequest();
var invoiceExpireTimer;
// The invoice expiration that updates the remaining time.
function updateIntervalTime(){
var invoiceRemainingTime = paywallHttpRequest.paywall.getInvoiceExpiration().remaining();
var remainingTime = invoiceRemainingTime.minutes() + ":" + invoiceRemainingTime.seconds();
$('#invoiceTimeRemaining').text(remainingTime);
}
// The Invoice event handler that adds invoice information to the modal and then displays it.
paywallHttpRequest.paywall.addEventListener("InvoiceListener", PaywallEventType.INVOICE, function (type, invoice) {
// Add a Paywall Invoice event Listener that displays the invoice for the user.
var modalBody = $('#invoiceModalBody')
modalBody.empty();
var invoiceExpire = new PaywallTime(invoice.invoiceExpireDate)
modalBody.append("<h6 class='text-center'>Invoice Expires In: <span id='invoiceTimeRemaining'>" + invoiceExpire.remaining().minutes() + ":" + invoiceExpire.remaining().seconds() + "</span></h6>") // Time Left
modalBody.append("<img class='mx-auto d-block' src='" + invoice.qrLink + "'/>"); // QR
var amountInSat = new PaywallAmount(invoice.invoiceAmount).as(BTCUnit.SAT);
modalBody.append("<h6>Price: " + amountInSat + "</h6>") // Time Left
modalBody.append("<div class=\"accordion\" id=\"advancedAccordion\">\n" +
" <div id=\"welcomeCard2\" class=\"card\" >\n" +
" <div class=\"card-header\" id=\"advancedAccordionHeader\">\n" +
" <h6 class=\"mb-0\">\n" +
" <button class=\"btn btn-link\" type=\"button\" data-toggle=\"collapse\" data-target=\"#advancedAccordionBody\" aria-expanded=\"false\" aria-controls=\"advancedAccordionBody\">\n" +
" Advanced\n" +
" </button>\n" +
" </h6>\n" +
" </div>\n" +
" <div id=\"advancedAccordionBody\" class=\"collapse\" aria-labelledby=\"advancedAccordionHeader\" data-parent=\"#advancedAccordion\">\n" +
" <div class=\"card-body\">\n" +
" <div class=\"card\" >\n" +
" <div class=\"card-body\">\n" +
" <h6 class=\"card-subtitle mb-2 text-muted\"><i>Invoice:</i></h6>\n" +
" <p id=\"bolt11Invoice\" class=\"card-text\">" + invoice.bolt11Invoice + "</p>\n" +
" </div>\n" +
" </div>\n" +
" <div class=\"card\" >\n" +
" <div class=\"card-body\">\n" +
" <h6 class=\"card-subtitle mb-2 text-muted\"><i>Node Info:</i></h6>\n" +
" <p id=\"nodeInfo\" class=\"card-text\">" + invoice.nodeInfo.connectString + "</p>\n" +
" </div>\n" +
" </div>\n" +
" </div>\n" +
" </div>\n" +
" </div>\n" +
" </div>"); // Advanced, with invoice and node
// Set the timer
invoiceExpireTimer = setInterval(updateIntervalTime,1000);
// Finally activate the invoice modal.
$('#invoiceModal').modal({});
});
// Event listener that hides the modal upon settlement.
paywallHttpRequest.paywall.addEventListener("SettledListener", PaywallEventType.SETTLED, function (type, settlement) {
$('#invoiceModal').modal('hide');
});
// Event listener that updates the modal with invoice expired information.
paywallHttpRequest.paywall.addEventListener("InvoiceExpiredListener", PaywallEventType.INVOICE_EXPIRED, function (type, invoice) {
var modalBody = $('#invoiceModalBody')
modalBody.empty();
modalBody.append("<h6 class='text-center'>Invoice Expired</h6>") // Time Left
});
// The same onload handler that should have been used without paywall to call the service.
paywallHttpRequest.onload = function(){
if(paywallHttpRequest.status === 200) {
// Process the service response as would be done with a regular XMLHttpRequest
var response = JSON.parse(paywallHttpRequest.responseText);
var predictionText = $('#predictionText')
predictionText.text(response.prediction);
if (response.goingUp) {
predictionText.removeClass("bg-danger");
predictionText.addClass("bg-success");
} else {
predictionText.removeClass("bg-success");
predictionText.addClass("bg-danger");
}
$('#welcomeCard').addClass("d-none");
$('#predictionCard').removeClass("d-none");
}else{
// Error calling the underlying service.
console.log(paywallHttpRequest.responseText)
alert("Error occurred calling the service.")
}
};
// Open up a connection to the paywalled service.
paywallHttpRequest.open("GET","/tademo");
// Send the data to the service that will trigger the payment flow if required.
paywallHttpRequest.send();
}
Finally we also add a event handler specific for the Bootstrap modal that listens to close modal events and releases all underlying resources, such as closing web socket, etc.
$('#invoiceModal').on('hide.bs.modal', function (event) {
console.log("Hidden: " + paywallHttpRequest.paywall.getState());
if(invoiceExpireTimer !== undefined){
clearInterval(invoiceExpireTimer);
}
// Catch event that Invoice Modal is closes and free allocated resources by
// calling the abort() method.
if(paywallHttpRequest.paywall.getState() !== PaywallState.SETTLED &&
paywallHttpRequest.paywall.getState() !== PaywallState.EXECUTED) {
console.log("Aborted");
paywallHttpRequest.abort();
}
});
The full index.html can be found in the example repository here.
Also see Javascript API section for more details on how to use the PaywallHttpRequest class.
2.7. Starting the application.
The final step to get the TA Demo application up an running in your development enviroment is by running the command
./gradlew bootRun
Then open up the browser and go to http://localhost:8080 and you are ready for starting to pay for TA predictions.
3. Paywall Core Components
The Paywall framework is built up of a set of core components controlling the payment flow and most of them are customizable. This chapter goes through the core parts of these components that are not directly Spring Framework specific.
First are the most basic concepts in Paywall Framework, and that are required when using the framework are:
-
PaymentHandler, that creates, updates and persist PaymentData.
-
PaymentData, value object of a given payment state.
-
@PaymentRequired, annotation that is set on API services that should trigger a payment flow.
Then are the concept PaymentFlow described, that is in charge of directing the flow between the different components.
Finally are other core components discussed, such as LightningHandler used to communicate with the lightning node, and CurrencyConverter that is in charge of converting between FIAT and Crypto currency if needed.
3.1. Payment Handler
A PaymentHandler manages Orders, Invoices and Settlements in a payment flow and every application using Paywall framework is required to implement one. The class is responsible to create and persist a value object of type PaymentData.
A PaymentData that can be of different types depending on the level of control of the actual payment flow wanted or the what types of payment related data that needs to be persisted.
3.1.1. Implementing a Payment Handler using Spring Framework
Using the Spring Framework, the easiest way to implement the payment handler is to create a class extending org.lightningj.paywall.spring.SpringPaymentHandler and implement the following three methods:
- newPaymentData
-
Method that should generate a new PaymentData. The method receives a newly generated preImageHash, used to uniquely identify the payment flow, and an OrderRequest, containing data from PaymentRequired annotation. This is the first call in a payment flow and the implementation should for example look up the order amount from the article id, units and other options in the order request. The generated PaymentData should be at least MinimalPaymentData with preImageHash and orderedAmount set. It is recommended that the PaymentData is persisted in this call but could be skipped for performance in certain payment flows.
- findPaymentData
-
Method used by the paywall framework to lookup a PaymentData from a preImageHash.
- updatePaymentData
-
Method called on update events about a given payment data. This could be when the payment is added as invoice in LND and contains complementary data or when the invoice was settled and has the settled flag set along with the settled amount (depending on the type of PaymentData used in PaymentHandler). The implementing method should at least persist the updated payment data. See table Available Payment Event Types for list of different PaymentEventType that might occur.
Type | Description |
---|---|
ORDER_CREATED |
Event signaling a order was created. This event is never sent to updatePaymentData. |
INVOICE_CREATED |
Event signaling that a payments invoice have been created by lightning handler. |
INVOICE_SETTLED |
Event signaling that a invoice have been settled. |
REQUEST_EXECUTED |
Event signaling that a payed request has been executed. Only used for payment flows with payPerRequest flag set. |
Below is an excerpts of the required methods to implement:
/**
* Method that should generate a new PaymentData for a given order request.
* This is the first call in a payment flow and the implementation should
* look up the order amount from the article id, units and other options in
* the order request.
*
* The generated PaymentData should be at least MinimalPaymentData with preImageHash
* and orderedAmount set.
*
* It is recommended that the PaymentData is persisted in this call but could
* be skipped for performance in certain payment flows.
*
* @param preImageHash the unique preImageHash used to identify a payment flow
* withing a lightning payment.
* @param orderRequest the specification of the payment data that should be created calculated
* from data in the PaymentRequired annotation.
* @return a newly generated PaymentData signaling a new payment flow used to
* create an Order value object.
* @throws IOException if communication exception occurred in underlying components.
* @throws InternalErrorException if internal exception occurred generating new payment data.
*/
protected abstract PaymentData newPaymentData(byte[] preImageHash, OrderRequest orderRequest) throws IOException, InternalErrorException;
/**
* Method to lookup a payment data in the payment handler.
*
* @param preImageHash the unique preImageHash used to identify a payment flow
* withing a lightning payment.
* @return return related payment data or null if not found.
* @throws IOException if communication exception occurred in underlying components.
* @throws InternalErrorException if internal exception occurred fetching related payment data.
*/
protected abstract PaymentData findPaymentData(byte[] preImageHash) throws IOException, InternalErrorException;
/**
* Method called on update events about a given payment data. This could be when
* the payment is added as invoice in LND and contains complementary data or when
* the invoice was settled and contains settled flag set and settled amount and date
* (depending on the type of PaymentData used in PaymentHandler).
*
* The related payment data (using preImageHash as unique identifier) is automatically
* looked up and the implementing method should at least persist the updated data.
*
* @param type the type of event such as INVOICE_CREATED or INVOICE_SETTLED.
* @param paymentData the payment data to update and persist.
* @param context the latest known state of the lightning handler. Null if no known state exists.
* @throws IOException if communication exception occurred in underlying components.
* @throws InternalErrorException if internal exception occurred updating related payment data.
*/
protected abstract void updatePaymentData(PaymentEventType type, PaymentData paymentData,
LightningHandlerContext context) throws IOException, InternalErrorException;
Below is an example implementation for Spring Framework, that assumes you have Spring Data Repository for a FullPaymentData implementation (see PaymentData section ), and that supports payPerRequest annotated services. It also assumes there exists a Spring Data Repository for the Article Ids specified in @PaymentRequired annotation to look up price for specified Article Id.
Using Spring it is important to add the class as a component with name paymentHandler using the @Component("paymentHandler") annotation on the class level.
Important: In order for your Spring application to load all beans in the Paywall framework it is a requirement to also add an annotation @ComponentScan("org.lightningj.paywall.spring") above one of the target application components, and it is recommended to do this for the PaymentHandler as a convertion.
/**
* Demo implementation of a Payment Handler extending SpringPaymentHandler.
*
* It creates DemoPaymentData that implements the PerRequestPaymentData interface. (In order to demonstrate
* support for both request that's valid for a period of time and for a specific request.)
* by first looking up the article id order (generated by the @PaymentRequired annotation).
* Then checks the price of the article in ArticleData table to calculate the ordered amount.
*
* It also implements the lookup by preImageHash method and update payment data methods by calling
* related methods in the DemoPaymentDataRepository.
*
*/
@ComponentScan("org.lightningj.paywall.spring")
@Component("paymentHandler")
public class DemoPaymentHandler extends SpringPaymentHandler {
@Autowired
DemoFullPaymentDataRepository demoPaymentDataRepository;
@Autowired
ArticleDataRepository articleDataRepository;
/**
* Method that should generate a new PaymentData for a given order request.
* This is the first call in a payment flow and the implementation should
* look up the order amount from the article id, units and other options in
* the order request.
* <p>
* The generated PaymentData should be at least MinimalPaymentData with preImageHash
* and orderedAmount set.
* <p>
* It is recommended that the PaymentData is persisted in this call but could
* be skipped for performance in certain payment flows.
*
* @param preImageHash the unique preImageHash used to identify a payment flow
* withing a lightning payment.
* @param orderRequest the specification of the payment data that should be created calculated
* from data in the PaymentRequired annotation.
* @return a newly generated PaymentData signaling a new payment flow used to
* create an Order value object.
* @throws IOException if communication exception occurred in underlying components.
* @throws InternalErrorException if internal exception occurred generating new payment data.
*/
@Override
protected PaymentData newPaymentData(byte[] preImageHash, OrderRequest orderRequest) throws IOException, InternalErrorException {
try{
DemoFullPaymentData demoPaymentData = new DemoFullPaymentData();
demoPaymentData.setPreImageHash(preImageHash);
demoPaymentData.setPayPerRequest(orderRequest.isPayPerRequest());
ArticleData articleData = articleDataRepository.findByArticleId(orderRequest.getArticleId());
if(articleData == null){
throw new InternalErrorException("Internal error creating payment data, article id " + orderRequest.getArticleId() + " doesn't exist in database.");
}
long orderPrice = articleData.getPrice() * orderRequest.getUnits(); // Price in satoshis.
demoPaymentData.setOrderAmount(new BTC(orderPrice));
demoPaymentDataRepository.save(demoPaymentData);
return demoPaymentData;
}catch(Exception e){
if(e instanceof InternalErrorException){
throw e;
}
throw new InternalErrorException("Error occurred saving DemoPaymentData to database: " + e.getMessage(),e);
}
}
/**
* Method to lookup a payment data in the payment handler.
*
* @param preImageHash the unique preImageHash used to identify a payment flow
* withing a lightning payment.
* @return return related payment data or null if not found.
* @throws InternalErrorException if internal exception occurred fetching related payment data.
*/
@Override
protected PaymentData findPaymentData(byte[] preImageHash) throws InternalErrorException {
try{
return demoPaymentDataRepository.findByPreImageHash(Base58.encodeToString(preImageHash));
}catch(Exception e){
throw new InternalErrorException("Error occurred fetching DemoPaymentData from database: " + e.getMessage(),e);
}
}
/**
* Method called on update events about a given payment data. This could be when
* the payment is added as invoice in LND and contains complementary data or when
* the invoice was settled and contains settled flag set and settled amount and date
* (depending on the type of PaymentData used in PaymentHandler).
* <p>
* The related payment data (using preImageHash as unique identifier) is automatically
* looked up and the implementing method should at least persist the updated data.
*
* @param type the type of event such as INVOICE_CREATED or INVOICE_SETTLED.
* @param paymentData the payment data to update and persist.
* @param context the latest known state of the lightning handler. Null if no known state exists.
* @throws InternalErrorException if internal exception occurred updating related payment data.
*/
@Override
protected void updatePaymentData(PaymentEventType type, PaymentData paymentData, LightningHandlerContext context) throws InternalErrorException {
try {
assert paymentData instanceof DemoFullPaymentData;
demoPaymentDataRepository.save((DemoFullPaymentData) paymentData);
}catch(Exception e){
throw new InternalErrorException("Error occurred updating DemoPaymentData to database: " + e.getMessage(),e);
}
}
}
Advanced PaymentHandler Features
For production ready implementations of a PaymentHandler it is recommended to also override and implement the method getLightningHandlerContext(). It should return a LightningHandlerContext (i.e. a LNDLightningHandlerContext if LND backend is used) that contains the latest position of subscribed invoices. This will make restarts of the application more reliable since invoices that have been settled during a restart of the application will not be missed.
The LNDLightningHandlerContext contains two values addIndex and settleIndex indicating from which position the LNDLightningHandler should start to subscribe to invoice events. When supporting getLightningHandlerContext() should the PaymentHandler implementation persist the two values from the LNDLightningHandlerContext received in the updatePaymentData call.
/**
* Method to generate the latest LightningHandlerContext of latest
* known state of the lightning node. Used to make sure the PaymentHandler
* starts listening on the correct location of the invoice event queue after
* restart. For LND Nodes should this method return a LNDLightningHandlerContext
* @return the last known state of lightning handler context.
* @throws InternalErrorException if internal exception occurred fetching latest known state of lightning handler.
*/
LightningHandlerContext getLightningHandlerContext() throws InternalErrorException;
3.1.2. Payment Data
PaymentData is a value object, usually stored in a database, that the PaymentHandler manages. There exists several interfaces to choose from depending on required functionality. The simplest interface is MinimalPaymentData and it contains the absolute minimal fields necessary to be able to complete a payment flow. And FullPaymentData where it is possible for the PaymentHandler implementation to control many aspects of the payment flow such as invoice validity, settlement validity.
Each sub-section describes the different types of PaymentData available.
MinimalPaymentData
Contains the minimum fields needed in order to support a payment flow, without possibility to host payPerRequest calls.
Field | Type | Description |
---|---|---|
preImageHash |
byte[] |
Unique identifier of a payment in the system and also used in LightningHandler to identify an invoice. Should be generated by TokenGenerator when creating an order and not set manually. |
orderAmount |
Amount |
The requested amount for payment. This can be either a FiatAmount or CryptoAmount but always make sure the systems configured CurrencyConverter supports this currency when converting into a currency accepted by the LightningHandler later in the payment flow. |
settled |
boolean |
If related invoice have been settled in full. |
For more details see JavaDoc.
PerRequestPaymentData
PerRequestPaymentData is a interface extending MinimalPaymentData required when pay per request functionality is used. It adds two flags that indicate that this payment is payPerRequest and whether the settled call have been executed and cannot be requested again.
Field | Type | Description |
---|---|---|
payPerRequest |
boolean |
Flag indicating that this payment is for one request only. The implementation can take the payPerRequest flag from the order request as guidance, but it is the PaymentHandler that ultimately decides if payPerRequest should be set. |
executed |
boolean |
True if related request have been executed, is set after successful processing of a request and used to indicate that it cannot be processed again. |
For more details see JavaDoc.
StandardPaymentData
The StandardPaymentData extends MinimalPaymentData and adds more information about the invoice and the ability for the PaymentHandler to control invoice validity and settlement validity.
_Note:_In order for a StandardPaymentData to support pay per request flows it has to also implement the PerRequestPaymentData interface.
Field | Type | Description |
---|---|---|
description |
String |
A short description of the payment used in the lightning invoice and might be displayed to the end user. |
invoiceAmount |
CryptoAmount |
The amount set in the lightning invoice, this is the same as orderAmount if the same currency is used in order as in lightning invoice, otherwise is the currency converted before creating the invoice in LightningHandler and the actual invoiced amount is specified here. |
invoiceDate |
Instant |
The date the invoice was created in LightningHandler. |
invoiceExpireDate |
Instant |
The date a generated invoice should expire, this value will be used when creating invoice in LightningHandler. If null will default invoice validity be used to calculate an expire date automatically. |
settledAmount |
CryptoAmount |
The amount that was settled in the LightningHandlers supported crypto currency. Should be equal to invoiceAmount if fully settled. Null if invoice isn’t settled yet. |
settlementDate |
Instant |
The timestamp the invoice was settled in LightningHandler. Null if not settled yet. |
settlementDuration |
Duration |
The settlement duration indicates how long time a generated settlement should be valid. If not set will a default settlement value be used. In FullPaymentData it is also possible to specify an expiration date of an settlement that is used if it’s required to set a fixed time when the settlement should expire, for example if a settlement should be valid the entire day or month. If settlement expire date is set it has precedence over settlementDuration. Important: Data in this field is only set to instruct the settlement token generator of expiration date. the actual settlement date is not updated in this field. |
For more details see JavaDoc.
FullPaymentData
The FullPaymentData extends both StandardPaymentData and PerRequestPaymentData and adds fields to store the actual bolt11Invoice and possiblity to specify exact dates for settlement validity, for use cases when paying for instance for a monthly subscription for a service and want the settlement token to be valid for exactly those dates.
Field | Type | Description |
---|---|---|
bolt11Invoice |
String |
The bolt11 lightning invoice displayed to the end user before paying and invoice. |
settlementValidFrom |
Instant |
The valid from timestamp used in generated settlement tokens. If null is no valid from used, only validUntil. |
settlementExpireDate |
Instant |
The settlement expire date sets the timestamp when a generated settlement token should expire. If not set will a settlementDuration be used, and if that is also null will default duration be set. This field is useful if a settlement should be valid the entire day or month. If settlement expire date is set it has precedence over settlementDuration. Important: Data in this field is only set to instruct the settlement token generator of expiration date. The actual settlement date is not updated in this field. |
For more details see JavaDoc.
3.2. @PaymentRequired Annotation
Another of the main components of the framework is the @PaymentRequired annotation used to mark that a service requires payment and initiates a new payment flow if needed.
Currently are only Spring REST Controllers (Annotated with @RestController) supported but other types of services will be supported in the future.
3.2.1. Available @PaymentRequired Parameters
The @PaymentRequired annotation can be customized to create order request information to the payment handler in various ways. See table below for a full list of available parameters.
Parameters | Required | Default Value | Description |
---|---|---|---|
articleId |
true, see description |
"" |
Determines the type of order that should be generated, used by PaymentHandler to determine order amount depending on article an units. (Required if not a custom OrderRequestGenerator is specified). |
units |
false |
1 |
The number of units for given article number. |
payPerRequest |
false |
false |
If payment is valid for one request only. If not will the settlement be valid for multiple request over a specified time period. |
orderRequestGenerator |
false |
DefaultOrderRequestGenerator.class |
Possibility to specify a custom order request generator, instead of the default one using the articleId and units to request an order. See section Order Request Generator Parameter for details. |
requestPolicy |
false |
WITH_BODY |
Defines what data in HTTP request that is considered relevant for determining a unique payment. See section Request Policy Parameter for details. |
customPolicy |
false |
NoCustomRequestPolicy.class |
The custom class if none of the predefined request policy types isn’t applicable and a custom implementation is necessary. |
paymentOptions |
false |
Empty list |
Set of custom extra options sent to payment handler when creating an order for an invoice. Each value should be of class org.lightningj.paywall.annotations.vo.PaymentOption that have two fields, option which acts as a key and value that contains the actual value. |
3.2.2. Examples of @PaymentRequired Annotations
The @PaymentRequired annotation can be placed on either the method or class level. If placed before the class declaration is all methods paywalled with the same parameters.
Below is an example of a pay walled method where a payment request request is initiated with article id "abc123" sent the payment handler to create an order for.
@RestController
public class Poc1RestController {
private static final String template = "PocService1, %s!";
private final AtomicLong counter = new AtomicLong();
@PaymentRequired(articleId = "abc123")
@RequestMapping("/poc1")
public PocResult poc1(@RequestParam(value="name", defaultValue="Poc1") String name) {
return new PocResult(counter.incrementAndGet(),
String.format(template, name));
}
}
If payment should be done per-request and not for a specified time add a payPerRequest parameter.
@RestController
public class Poc1RestController {
private static final String template = "PocService1, %s!";
private final AtomicLong counter = new AtomicLong();
@PaymentRequired(articleId = "abc123", payPerRequest = true)
@RequestMapping("/poc1")
public PocResult poc1(@RequestParam(value="name", defaultValue="Poc1") String name) {
return new PocResult(counter.incrementAndGet(),
String.format(template, name));
}
}
If all methods should be pay walled in a class with the same parameters add the annotation before the class declaration.
@PaymentRequired(articleId = "abc456")
@RestController
public class Poc1RestController {
private static final String template = "PocService1, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/poc1")
public PocResult poc1(@RequestParam(value="name", defaultValue="Poc1") String name) {
return new PocResult(counter.incrementAndGet(),
String.format(template, name));
}
}
3.2.3. The Request Policy Parameter
The request policy is in-charge of calculating a cryptographic hash of all significant data for a given payment flow. This is used to determine if a given settlement is valid for a related request.
For example if a service should require payment for access to a given REST WebService for a given amount of time it should specify URL_AND_METHOD and all types of requests to that URL and METHOD will be allowed until the related settlement expires (given that payPerRequest is set to false). What happens under the hood is that the URL and METHOD in the original request was included in the cryptographic hash and no other data. After successful payment and when the same request is snet again will the new requests URL and HTTP method be matched against the original one.
There exists a set of defined types of request policies that calculate the unique request from a given set of data.
Option | Description |
---|---|
URL_AND_METHOD |
Policy that checks the URL and Method of a request. |
URL_METHOD_AND_PARAMETERS |
Policy that checks the URL and Method and all parameters of a request. |
WITH_BODY |
Policy that checks the URL, Method, all parameters and full body data of a HTTP request. |
CUSTOM |
Custom implementation of calculating significant request data. |
Creating a Custom Request Policy
To create a custom request policy, create a class that implements org.lightningj.paywall.requestpolicy.RequestPolicy. It contains one required method significantRequestDataDigest that calculates a request.
The RequestPolicy interface have the following method defined.
public interface RequestPolicy {
/**
* Method in charge of generating a digest
* of all significant data in a request that is needed
* to determine that the call is same that is invoiced
*
* @param request the cachable http servlet request to aggregate request data for.
* @return a RequestData containing a secure cryptographic digest of all significant request data.
*
* @throws IllegalArgumentException if supplied request contained invalid data.
* @throws IOException if i/o related problems occurred reading the request data.
* @throws InternalErrorException if internal errors occurred reading the request data.
*/
RequestData significantRequestDataDigest(CachableHttpServletRequest request) throws IllegalArgumentException, IOException, InternalErrorException;
}
A tip is to aggregate all data required in a ByteArrayOutputStream and the create the cryptographic hash value with the DigestUtils.sha256(baos.toByteArray()) help method.
3.2.4. The Order Request Generator Parameter
By default is an OrderRequest generated to the PaymentHandler containing and article id, number of units, the payPerRequest flag and the list of paymentOptions. But it is possible the create a custom OrderRequestGenerator for specific purposes.
One use-case for creating a custom order request generator would be if the article Id or payment options should be decided dynamically depending on data in the http request, such as body json data, instead of static data from the @PaymentRequired annotation.
Creating a Custom Order Request Generator
To create a custom order generator create a class implementing org.lightningj.paywall.orderrequestgenerator.OrderRequestGenerator that contains one method that should generate a new OrderRequest object from the related PaymentRequired annotation and HTTP Request object.
public interface OrderRequestGenerator {
/**
* Method that should populate a new OrderRequest to initiate a
* payment flow using the PaymentRequired annotation and the
* related HTTP request.
* @param paymentRequired the related annotation.
* @param request the HTTP request related to the call.
* @return a new OrderRequest.
* @throws IllegalArgumentException if user supplied data was invalid to generate order request.
* @throws InternalErrorException if problem occurred generated order request data due to internal miss configuration.
*/
OrderRequest generate(PaymentRequired paymentRequired, CachableHttpServletRequest request) throws IllegalArgumentException, InternalErrorException;
}
3.3. Payment Flows
The framework is designed to work in different system configurations. It not always desirable for all micro services to have a direct connection with a lightning node but would like to centralise this functionality to a central system handling the payment and the actual the paywalled service just redirects the user to the central payment server until a settlement token have been issued and the requester is redirected back to the original system.
Currently is only one payment flow supported, the local payment flow, but others will be added in the future.
3.3.1. Local Payment Flow
The default, and currently only payment flow available, is the local payment flow. It’s used when the same system have all paywall components in same system (CheckSettlement controller, WebSocket Service, LightningHandler etc) and have a direct connection with a lightning node.
Below is a flow chart describing all the steps in the local payment flow in detail. The blue boxes indicate components that is a part of the target application, the rest is part of the Paywall framework.
- 1.1 PaywallInterceptor intercept the request
-
This component is a part of the paywall-spring component and is configured to parse all incoming request
- 1.2 RestController in target application
-
The PaywallInterceptor lookup if target controller contains a @PaymentRequired annotation. If that is the case and no valid settlement token exists in the HTTP header is a new Payment Flow initiated.
- 1.3 RequestPolicy Bean generates significant data
-
The request policy type is fetched up from the @PaymentRequired annotation. And then is the significant data from the request calculated.
- 1.4 OrderRequestGenerator generates a OrderRequest
-
The order request generator is fetched from the @PaymentRequired annotation. And a order request, usually article id and units, is created.
- 1.5 TokenGenerator generates PreImageData
-
The token generator bean generates a random preImage and preImageHash that is used to uniquely identify the payment flow and use in the lightning invoice.
- 1.6 PaymentHandler is called to generate an Order
-
The payment handler is called to create a new PaymentData which is used to create and keep track of an invoice. This is done by calling the method PaymentData newPaymentData(byte[] preImageHash, OrderRequest orderRequest) that needs to be implemented by the target application.
- 1.7 CurrencyConverter converts amount to used crypto currency
-
The defined CurrencyConverter is called to optionally convert the amount in the Order to the crypto amount used by the LightningHandler. By default is no conversion performed and the payment handler is required to create orders with amount in CryptoAmount (i.e BTC).
- 1.8 LightningHandler is called to create a lightning invoice
-
The configured LightningHandler is called to create an invoice for the related payment. The LightningHandler also subscribes to settlement and updates the PaymentHandler asynchronously using an event bus.
- 1.9 TokenGenerator generates Invoice JWT Token
-
The token generator generates a signed and encrypted JWT (Java Web Token) of type invoice used to certify the requester as owner of this payment flow and used when checking settlement.
Finally will the PaywallInterceptor generate a Invoice JSON Data structure and return it with HTTP status code 402.
The next step in the flow is for the requester to check settlement. This can be done in two way either by polling a Check Settlement REST API, or by subscribing to a pushed settlement messsage over a WebSocket (used the the Javascript library by default). The flow diagram describes the inner workings of the Check Settlement Controller.
- 2.1 Check Settlement Controller
-
A HTTP GET Request to check payment status. The controller fetches for Invoice HTTP parameter pwir from the request URL.
- 2.2 TokenGenerator parses the Invoice Token
-
Token Generator parses and validates the JWT and extracts the preImageHash.
- 2.3 The PaymentHandler is used to lookup the related PaymentData
-
The PaymentHandler’s PaymentData findPaymentData(byte[] preImageHash) is called (that needs to be implemented by the target application) to look up settlement status.
If the related PaymentData is marked as settled is 2.4 called otherwise is an empty Settlement JSON Data Structure returned with only the field settled set to false.
- 2.4 Token Generator generates a Settlement Token
-
TokenGenerator generates an encrypted and signed JWT of type settlement. Finally is a populated Settlement JSON Data Structure returned with all fields set.
The third step in the payment flow is for the request to call the target API again, this time with the settlement token set as HTTP Header with name Payment. This step is displayed in the first flow chart.
- 3.1 The PaywallInterceptor inspects the request again
-
This time it determines that settlement token exists and starts with verification of payment.
- 3.2 TokenGenerator parses the Settlement Token
-
The settlement token is parsed and validated. It also checks if related payment is payPerRequest and if that is the case is step 3.3 called.
- 3.3 PaymentHandler checks if request already have been executed
-
The PaymentHandler is called using the PaymentData findPaymentData(byte[] preImageHash) method to verify that the request haven’t already been processed.
- 3.4 RequestPolicy Bean generates significant data of new request
-
The significant data is calculated again.
- 3.5 PaywallInterceptor calls paywalled controller
-
If significant data matches with data in settlement token will the PaywallInterceptor let the request go through to the underlying controller.
- 3.6 After the Target API have processed the request
-
If the payment flow is of type payPerRequest is PaymentHandler’s method void updatePaymentData(PaymentEventType type, PaymentData paymentData, LightningHandlerContext context) called with PaymentEventType set to PaymentEventType.REQUEST_EXECUTED.
Finally it the response generated by the target API returned to the requester.
3.4. Customizing Paywall Components
Most of the components used in the Paywall framework can be customized by implementing the related interface or overriding existing classes. This section details how to customize some of them.
3.4.1. Custom CurrencyConverter
A CurrencyConverter is in charge of converting the Amount specified in an Order created by the PaymentHandler into the Amount that should be used in the Lightning invoice. One use-case is if the PaymentHandler returns the amount in FIAT USD and the LightningHandler requires BTC. Then a CurrencyConverter is needed to convert the amount.
By default is SameCryptoCurrencyConverter used which doesn’t do any conversion. It is assumed the PaymentHandler will return the Amount in the cryptocurrency used by the LightningHandler (i.e. BTC).
To customize, implement the interface org.lightningj.paywall.currencyconverter.CurrencyConverter that has one method CryptoAmount convert(Amount amount) that needs to be implemented. See JavaDoc for details.
3.4.2. Custom LightningHandler
A LightningHandler is in charge of connecting to a lightning node and create and subscribe to invoices. There exists one LND implementation in paywall-core (actually two classes Base and Simple) and one extension in paywall-spring adding Spring related functionality.
LND Implementation Class | Description | JavaDoc Link |
---|---|---|
BaseLNDLightningHandler |
Base implementation of LND Lightning Handler handling the methods for generateInvoice, lookupInvoice and invoice subscribing. See SimpleBaseLNDLightningHandler that manages APIs, opening/closing connection. Extends this if custom management of LND APIs should be done, otherwise use SimpleBaseLNDLightningHandler. |
|
SimpleBaseLNDLightningHandler |
Extension of BaseLNDLightningHandler that also manages APIs and opening/closing connection. Implementing classes only need to give host,port, path to TLS cert and macaroon. |
|
SpringLNDLightningHandler |
Spring implementation of LND Lightning Handler. |
To implement a custom LightningHandler implement the interface org.lightningj.paywall.lightninghandler.LightningHandler. See JavaDoc for details.
3.4.3. Custom KeyManager
A KeyManager is in-charge of maintaining cryptographic keys with in the system. There exists two types of key managers, SymmetricKeyManager managing symmetric keys (JavaDoc), and AsymmetricKeyManager (JavaDoc) managing asymmetric keys.
Asymmetric keys is used in payment flows requiring multiple systems where trust needs to be set up between them.
The default implementation is DefaultFileKeyManager that implements both SymmetricKeyManager and AsymmetricKeyManager and stores the keys on local disk, encrypted by a passphrase and generates the needed keys automatically when needed.
3.4.4. Custom TokenGenerator
The TokenGenerator is responsible for generating signed and encrypted JWT Tokens and PreImageData. There exists two implementations of TokenGenerator, SymmetricKeyTokenGenerator using symmetric key manager and used in the local payment flow, and AsymmetricKeyTokenGenerator using asymmetric keys for payment flows requiring setting up trust between different systems.
There are three type of JST tokens defined in table below. The local payment flow only uses the Invoice and Settlement tokens.
JWT Token Type | Description | Contains Data |
---|---|---|
Payment |
Used in distributed payment flows where JWT token contains Order information to create a PaymentData. |
OrderRequest, Order, RequestData |
Invoice |
Contains information about a lightning invoice, used when checking for settlement to prove ownership of the payment flow. |
OrderRequest, MinimalInvoice, RequestData |
Settlement |
Contains information that an invoice have been settled, including how for how long the settlement is valid. |
OrderRequest, Settlement, RequestData |
To create a customized TokenGenerator implement the interface org.lightningj.paywall.tokengenerator.TokenGenerator See JavaDoc for details.
4. Spring and SpringBoot API
Currently is the LightningJ Paywall target to SpringBoot applications but other Java platforms might be implemented in the future. This chapter describes all Spring specific parts of the framework in detail.
The framework is not specifically target for SpringBoot, all components in written for the more general Spring version 5.1.4 and up. But test have mainly be done with Spring Boot 2.1.3.
The following Spring modules are required:
-
spring-context
-
spring-core
-
spring-web
-
spring-webmvc
-
spring-websocket
-
spring-messaging
4.1. Available Profiles
The Paywall Spring module is configured using profiles and currently only one profile exists, paywall_local, that enables all the local payment flow’s default beans.
4.1.1. Local Payment Flow Profile
The local payment flow is a basic setup where the same node hosts all the required functionality to service payment flows. I.e the node have access to a Lightning Node and hosts services for for QR Code and Check Settlement. It also uses symmetric keys to sign and encrypt JWT tokens since there are no requirements for intra-system trust.
To activate the local payment flow add the value paywall_local to the setting spring.profiles.active in application.properties. See below for an example:
spring.profiles.active=paywall_local
The profile registers the following beans by default using class org.lightningj.paywall.spring.local.LocalProfileBeanConfiguration:
Bean | Type | Registered Implementation | Description |
---|---|---|---|
currencyConverter |
org.lightningj.paywall.currencyconverter.CurrencyConverter |
org.lightningj.paywall.currencyconverter.SameCryptoCurrencyConverter |
Implementation that expects payment handler to generate a CryptoAmount and does no convertion. |
paywallExceptionHandler |
org.lightningj.paywall.spring.PaywallExceptionHandler |
org.lightningj.paywall.spring.SpringPaywallExceptionHandler |
Bean that converts exceptions to returned error message data. This implementation converts exception according to Server Side Exception to HTTP Status Code. |
lightningHandler |
org.lightningj.paywall.lightninghandler.LightningHandler |
org.lightningj.paywall.spring.SpringLNDLightningHandler |
A LND implementation requiring direct access to a LND node to create invoices and subscribe to settlements. |
keyManager |
org.lightningj.paywall.keymgmt.KeyManager |
org.lightningj.paywall.spring.SpringDefaultFileKeyManager |
Keymanager that generates symmetric key and stores them on the local file system encrypted with a passphrase. |
qrCodeGenerator |
org.lightningj.paywall.qrcode.QRCodeGenerator |
org.lightningj.paywall.qrcode.DefaultQRCodeGenerator |
Default implementation of QRCodeGenerator that generates PNG images of specified size. |
tokenGenerator |
org.lightningj.paywall.tokengenerator.TokenGenerator |
org.lightningj.paywall.tokengenerator.SymmetricKeyTokenGenerator |
JWT Token Generator using symmetric key to sign and encrypt the token. |
orderRequestGeneratorFactory |
org.lightningj.paywall.orderrequestgenerator.OrderRequestGeneratorFactory |
org.lightningj.paywall.orderrequestgenerator.OrderRequestGeneratorFactory |
This class should generally not be overloaded, instead use customized OrderRequestGenerator configured in the @PaymentRequired annotation. |
requestPolicyFactory |
org.lightningj.paywall.requestpolicy.RequestPolicyFactory |
org.lightningj.paywall.requestpolicy.RequestPolicyFactory |
This class should generally not be overloaded, instead use customized RequestPolicy configured in the @PaymentRequired annotation. |
paymentFlowManager |
org.lightningj.paywall.paymentflow.PaymentFlowManager |
org.lightningj.paywall.spring.local.SpringLocalPaymentFlowManager |
Local payment flow manager that expects all functionality to be in the same system. |
webSocketSettledPaymentHandler |
org.lightningj.paywall.spring.websocket.WebSocketSettledPaymentHandler |
org.lightningj.paywall.spring.websocket.WebSocketSettledPaymentHandler |
A WebSocket specific implementation when listing of settled payment handler. |
4.1.2. Customizing Bean Configuration
It is possible to customize beans that is registered for a given profile. In order to do this add the setting paywall.custombeans.enable:
spring.profiles.active=<your_profile>
Then create a custom Configuration implementation that extends the default one. Below is an example configuration overriding the default CurrencyConverter with a custom implementation:
@Configuration
public class CustomLocalProfileBeanConfiguration extends LocalProfileBeanConfiguration {
@Bean({"currencyConverter"})
@Override
public CurrencyConverter getCurrencyConverter() {
return new CustomCurrencyConverter();
}
}
4.2. The Paywall Interceptor (Filter)
The main component in the Paywall Spring framework is the Paywall Interceptor which filter all requests and checks if target controller is annotated with @RESTController and @PaymentRequired and in that case starts a payment flow if a settlement JWT token isn’t included in the header with name Payment.
If the filter determines that payment is required it initiates a payment flow according to the configured profile and returns status code 401 (PAYMENT_REQUIRED) with a newly generated Invoice according to schema specified in section Invoice JSON Data.
Currently are only @RestController annotated services supported but other types of controllers will be supported in the future.
4.2.1. Interceptor Error Handling
If payment related error occurred in the Paywall Interceptor is an error message returned with JSON (or XML) according to Paywall Error JSON Data and http status code is mapped to the generated exception according to table Server Side Exception to HTTP Status Code.
4.2.2. Paywall Data as XML Response instead of JSON
The response is by default a JSON response with content type application/json but if header Accept is set to application/xml or appending .xml to URL a XML variant of all paywall related responses will be returned. The XML will be structured according to the following XSD schema
4.3. Available Spring Configuration Properties
Paywall-Spring contains a configuration bean PaywallProperties that contains different settings available to the applicaton’s application.properties file.
Minimal configuration is:
spring.profiles.active=paywall_local paywall.lnd.hostname=somehost.org paywall.lnd.port=10009 paywall.lnd.tlscertpath=/home/lnd/.lnd/tls.cert paywall.lnd.macaroonpath=/home/lnd/.lnd/data/chain/bitcoin/testnet/invoice.macaroon paywall.lnd.connectstring=8371729292821728191012918129172271827281262611282@10.10.10.1:9735 paywall.keys.keystorepath=~/ta-demo-keys paywall.keys.password=foobar123
Important: If invoice.macaroon is used it is also required to set the setting paywall.lnd.connectstring since the macaroon doesn’t have access rights to read node information automatically.
Property | Required | Default Value | Description |
---|---|---|---|
LND and Lightning Properties: |
Settings related to connecting to used LND Node. |
||
paywall.lnd.hostname |
true |
n/a |
The hostname of IP address of the LND node to connect to. Required if running local payment flow. |
paywall.lnd.port |
true |
n/a |
The port number of the LND node to connect to. Required if running local payment flow. |
paywall.lnd.tlscertpath |
true |
n/a |
The path to the LND tls certificate to trust, securing the communication to the LND node. Should point to an file readable by the current user. Required if running local payment flow. |
paywall.lnd.macaroonpath |
true |
n/a |
The path to the macaroon file that is used to authenticate to the LND node. The macaroon should have invoice creation rights. Required if running local payment flow. |
paywall.lnd.connectstring |
false |
n/a |
The connect string displayed in node info part of generated invoices. It only needed to set this property if "paywall.lnd.connectstring" is set to true and macaroon used to connect to LND doesn’t have access rights to retrieve information. The connect string can be fetched using 'lncli getinfo' command. |
paywall.lnd.network |
false |
UNKNOWN |
The network the LND node is connected to. (Optional) If LND macaroon used have access right to fetch information, this can be done automatically. Default UNKNOWN. The current network can be fetched using 'lncli getinfo' command. Valid values are MAIN_NET, TEST_NET and UNKNOWN. |
paywall.lnd.currency |
false |
BTC |
The currency code the connected LND Node used. Should be one of CryptoAmount constants 'BTC' or 'LTC'. |
paywall.lightninghandler.autoconnect |
false |
true |
if BasePaymentHandler should connect automatically to Lightning Node upon initialization of bean. if set to false should the implementing application connect the lightning handler manually during startup. |
Key Management Settings: |
Settings used for managing cryptographic keys for signing and encrypting the JWT Token. |
||
paywall.keys.password |
false |
n/a |
The path of directory where key files are stored. Recommended to set in a production environment. If not set is a temporary directory used. Keys are created automatically in the directory if not exist. |
paywall.keys.keystorepath |
false |
n/a |
The configured pass phrase used to protect generated keys. Recommended to set a good password in a production environment. If not set is no password protection used to encrypt the keys. |
paywall.keys.truststorepath |
false |
n/a |
The path of directory where trusted public key files are stored. For future use in a distributed setup. When using local payment flow symmetric keys are used and this settings is not needed. |
Java Web Token (JWT) Settings: |
Settings used to configure the generation of JWT Tokens. |
||
paywall.jwt.notbefore |
false |
n/a |
The time in seconds for the not before field in generated JWT tokens. This can be positive if it should be valid in the future, or negative to support skewed clocked between systems. If unset is no not before date set in the generated JWT tokens. (Optional) |
Generated Invoice and Settlement Settings: |
Settings used to configure the generation of Invoices and Settings. |
||
paywall.invoice.defaultvalidity |
false |
3600 (1 hour) |
The default validity in seconds for generated invoices if no expire date have been set explicit in PaymentData. |
paywall.invoice.includenodeinfo |
false |
true |
If node connection information should be included in generated invoices. |
paywall.invoice.registernew |
false |
false |
If settled invoice are received before any order have been created it should registered as new payments automatically before marking them as settled. For future use in a distributed setup. Not used in local payment flow mode. |
paywall.settlement.defaultvalidity |
false |
24 * 60 * 60 (24 hours) |
The default validity for generated settlements if no valid until date have been set explicit in PaymentData. |
QR Code Generation End-Point Settings: |
Settings used to configure the QR Code Image Generation. |
||
paywall.qrcode.width.default |
false |
300 |
The default QR Code width if no width parameter is specified in QR Code generation request. |
paywall.qrcode.height.default |
false |
300 |
The default QR Code height if no height parameter is specified in QR Code generation request. |
paywall.qrcode.url |
false |
/paywall/genqrcode |
The URL to controller that generates QR code images. |
Check Settlement End-Point Settings: |
Settings used to configure the Check Settlement End Point. |
||
paywall.settlement.url |
false |
/paywall/api/checkSettlement |
The URL to the check settlement controller. |
Settlement WebSocket End-Point Settings: |
Settings used to configure the Settlement WebSocket End Point. |
||
paywall.websocket.enable |
false |
true |
if WebSocket functionality should be enabled. |
paywall.websocket.settlement.url |
false |
/paywall/api/websocket/checksettlement |
URL of the end point where check settlement Web Socket is listening. |
4.4. Available Supporting Services End-Points
The Spring Component in LightningJ Paywall provides a set of supporting end-points for handling payment flows. One is for generating QR Codes, and one REST interface for checking settlements using polling and one WebSocket end-point to set up a channel to receive settlement as fast as possible.
4.4.1. QR Code Generator End-Point
Running in local payment flow mode there is a QR Code generation service used to generate QR images for invoices. By default it is located at path '/paywall/genqrcode' but can be modified to a custom location with setting paywall.qrcode.url in application.properties.
The service is quite simple supports the following parameters and the GET HTTP Method:
Parameter | Required | Description |
---|---|---|
d |
true |
The data used to generate QR code for, I.e. bolt11 invoice. This parameter is set automatically in the qrLink field in the invoice json structure. |
w |
false |
The width of the generated image. If not set is default width set by paywall.qrcode.width.default in application.properties. |
h |
false |
The height of the generated image. If not set is default height set by paywall.qrcode.height.default in application.properties. |
The service will return image data with content type image/png and content length set. The constructed URL can be used directly as src attribute in an image html tag.
4.4.2. Check Settlement End-Point
In local payment flow mode there is a REST Service automatically created that can be used to check if a given invoice have been settled.
To call the service to a GET HTTP request to /paywall/api/checkSettlement (the end-point path is configurable with setting paywall.settlement.url in application.properties) with the query parameter 'pwir' set to a URL encoded invoice JWT token.
The field checkSettlementLink in Invoice JSON Object contains the URL pre-populated with the parameter set.
The service supports handling XML responses in the same way as the PaywallInterceptor and using the same XSD schema. Just as PaywallInterceptor the service sets the HTTP header value: PAYWALL_MESSAGE:TRUE to indicate this is a Paywall Related message.
For an unsettled payment will the following response be sent:
{
"status": "OK",
"type": "settlement",
"preImageHash": null,
"token": null,
"settlementValidUntil": null,
"settlementValidFrom": null,
"payPerRequest": null,
"settled": false
}
A settled payment will return the following:
{
"status": "OK",
"type": "settlement",
"preImageHash": "CP6p6AqgD7yL7QinVZDfkptiatr1ZkWN2MWVQ2WuSMg3",
"token": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..c2g7sb8Rqz-fsoItbrnJ3g.l_c4MzlyTItGp_hbl2tyTSHXBq_8-P0Eds1d09CKiEV-RjqLyD0msk3-gn_DLpz-v-Eke2EHZa4J0vWVwzcxM06eu8tgBX4jIg7SIMD4Lr79PB4v6vPwyf3MnZsnBYGTUNP86CAjVRa-0mF1SuTBtjU05YsPGqEmqiPThpXyG3lRxarQzGJEMA4jUaivTdGGChBFWRJsEsZHOs1fm2EJZ3YNtL55V91GFAyE-diGj_tvhHqFIbjl_VvDJza96B0NZrxDFQbUrXWU9WFubSJq4zV9m7mHiJ5wTr-Jf7nSpUIUFXb-oH9OYjQF0Dk9zPCSz6r3JGk9vnUmhyR5WvAl9Rw3qm-rYg-BOVD9tEKl2K-U6ZKuLK2Q-EDta6hVDHHnl39iCIQMzFdB3cVMSHId0yZw1Va_5metok4TqRKFUvLsTNR93oeesew2NxqfKETUBoA4AoLs2THkEKFLXtPjYyD2rf7V7TCZkudUlZ0aSa8JCZZUaJSW4kTCNmZLo5zVtdrwsaGeJcdaAOtce-s0oT0rpTymCYU3KSl9_EgXiPvjS0sLrTfR7WaxHQJyfcRV.36IeZ1Nl8yiGD-Q2USzbog",
"settlementValidUntil": "2019-06-02T07:10:29.354+0000",
"settlementValidFrom": null,
"payPerRequest": false,
"settled": true
}
For more details see the Settlement JSON Data description.
4.4.3. Check Settlement WebSocket End-Point
It is possible to subscribe to settlement tokens using a WebSocket connection. The websocket is using Stomp protocol over SockJS in order to have a fallback to older browsers not supporting WebSocket.
To subscribe to a payment flows settlement connect to the URL set in the checkSettlementWebSocketEndpoint field of the invoice JSON and subscribe to the unique channel from the field checkSettlementWebSocketQueue.
Example code to connect to WebSocket using Javascript and set the required invoice JWT token in the header, asserting ownership of the payment flow. (The example requires stomp.js and sockjs.js libraries, see Javascript section for details):
function processWebSocketMessage(message){
if(message.body){
var settlement = JSON.parse(message.body);
if(settlement.status === PaywallResponseStatus.OK){
// Process Settlement
}else{
// error occurred
}
}else{
console.debug("Paywall WebSocket, received empty message, ignoring.");
}
}
function processWebSocketError(error){
var errorObject;
if(error.headers !== undefined){
errorObject = {status: PaywallResponseStatus.SERVICE_UNAVAILABLE,
message: error.headers.message,
errors: [error.headers.message]
};
}else{
errorObject = {status: PaywallResponseStatus.SERVICE_UNAVAILABLE,
message: error,
errors: [error ]
};
}
}
var socket;
var stompSocket;
// Function that takes the invoice JSON Object and sets the invoice token in the
// Stomp connect header
function connect(invoice){
socket = new SockJS(paywall.paywall.genCheckSettlementWebSocketLink());
stompSocket = Stomp.over(socket);
var headers = {"token": invoice.token};
stompSocket.connect({}, function(frame) {
stompSocket.subscribe(invoice.checkSettlementWebSocketQueue, processWebSocketMessage, headers);
}, processWebSocketError);
};
function close(){
if(stompSocket !== undefined){
stompSocket.disconnect();
socket.close();
}
};
It is possible to override the default endpoint location of /paywall/api/websocket/checksettlement with the setting paywall.websocket.settlement.url in application properties. It is also possible to disable the web socket functionality with paywall.websocket.enable (enabled by default).
5. Javascript API
The paywall project has a Javascript library, made to simply preforming AJAX request to a paywalled service.
The library’s main class is PaywallHttpRequest which wraps the standard XMLHttpRequest with paywall functionality by triggering events such as when invoice was received and when it was settled. The PaywallHttpRequest automatically calls the underlying service again after settlement.
The library also contains help methods for retrieving remaining time on invoice or settlement before it expires and which units to display invoice amount in.
5.1. Prerequisites
In order to use the Javascript API you need to import the script paywall.js into your web page. paywall.js has two dependency libraries, sockjs.js and stomp.js used to open up web sockets.
Simples way to get started with the javascript library is to use the CDN download points of the javascript libraries by adding the following to the HTML files:
<!-- JavaScript Required for Paywall-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.3.0/sockjs.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js" ></script>
<script src="https://github.com/lightningj-org/paywall/releases/download/v0.0.1/paywall.js" ></script>
If you want to host the files locally in your application to avoid external dependencies then download them into a /js sub directory of your web application’s static files and instead add the snippet below to the HTML file.
<script src="js/sockjs.js"></script>
<script src="js/stomp.js"></script>
<script src="js/paywall.js"></script>
The required javascript files can be downloaded here:
Debug Version | Minified Version |
---|---|
5.2. The PaywallHttpRequest Class
To create a request to a paywalled server is very similar to a regular XMLHttpRequest but using the PaywallHttpRequest class instead.
In more details the PaywallHttpRequest works in the following way:
-
It wraps one XMLHttpRequest instance and opens a connection to the service.
-
As soon as the header have been downloaded, it is inspected for 402 (Payment Required).
-
If found is payment flow started, invoice is downloaded and a WebSocket channel is opened to the server.
-
As soon as a settlement is sent through the WebSocket, a new XMLHttpRequest is created towards the service, with all original request parameters set once more and the settlements JWT token in the request header
-
The new call should be accepted by the service and an onload event is triggered when download is complete.
The PaywallHttpRequest can also be used towards non-paywalled services and will then function as a regular XMLHttpRequest with the exception that onloadstart event is never triggered.
5.2.1. Example Usages
Plain Javascript is used in the examples but it should relative easy to adopt the code to the Javascript framework of choice.
A Simple Paywall Call
Below is a simple example of calling a paywalled service. In addition to a normal XMLHttpRequest there are two event listeners created. One when invoice have been created and should be displayed to the end user. And one when payment have been settled and invoice panel should be removed from the web page.
// Create a PaywallHttpRequest instance
var paywallHttpRequest = new PaywallHttpRequest();
// First set up event listeners
paywallHttpRequest.paywall.addEventListener("InvoiceListener", PaywallEventType.INVOICE, function (type, invoice) {
// Add a Paywall Invoice event Listener that displays the invoice for the user.
showInvoicePanel(invoice);
});
paywallHttpRequest.paywall.addEventListener("SettledListener", PaywallEventType.SETTLED, function (type, settlement) {
// Hide the invoice panel as soon as the invoice have been payed.
hideInvoicePanel();
});
paywallHttpRequest.onload = function(){
// Process the service response as would be done with a regular XMLHttpRequest
processServiceResponse(paywallHttpRequest.responseText);
}
// Open up a connection to the paywalled service.
paywallHttpRequest.open("POST","/someRestService");
// Send the data to the service that will trigger the payment flow if required.
paywallHttpRequest.send("{'data':'value'}");
Displaying Invoice
The example below show Javascript to populate a panel in pure Javascript. This could be done in a much more elegant way in a modern framework such as Angular or React.
function showInvoicePanel(invoice){
// The invoice contains a number of fields that might be displayable.
document.getElementById("invoicetext").innerText = invoice.description;
document.getElementById('invoicebolt11').innerText = invoice.bolt11Invoice;
// To display a QR Code, There is a QR generator link inside the paywallHttpRequest object
// constructing a full URL to the QR Generation endpoint.
// Just have a img tag and set the src attribute to:
var invoiceQRImg = document.getElementById('invoiceqr');
invoiceQRImg.src = paywallHttpRequest.paywall.genQRLink();
// Amount field have a special help function to display amount in a specified unit.
// In this example we display the amount in BIT.
document.getElementById('invoiceamount').innerText = paywallHttpRequest.paywall.getInvoiceAmount().as(BTCUnit.BIT);
// If node info exists on server it is also possible to display connection
// information
document.getElementById('invoiceNodeInfo').innerText = invoice.nodeInfo.connectString;
}
5.2.2. Displaying Remaining Times
There also exists help methods to display a count down counter of remaining validity of invoice and settlement as well as a count down until a settlement is valid if settlment validity is set in future.
The methods are paywallHttpRequest.paywall.getInvoiceExpiration(), paywallHttpRequest.paywall.getSettlementExpiration() and paywallHttpRequest.paywall.getSettlementValidFrom(). The method returns a PaywallTime object that have help methods to retrieve the number of hours, minutes and seconds left in order to create a hh:mm:ss counter. The counter never gets to negative but stops as 00:00:00.
Example code to create a count down counter displaying mm:ss left on an invoice validity:
function updateInvoiceRemainingTime(){
var remainingTime = paywallHttpRequest.paywall.getInvoiceExpiration().remaining();
document.getElementById('invoiceTimeRemaining').innerText = timeRemaining.minutes() + ":" + timeRemaining.seconds();
}
setInterval(updateInvoiceRemainingTime, 1000);
An alternative to create a remaining time object is to create a PaywallAmount object manually with a given JSON CryptoAmount.
var amountInSat = new PaywallAmount(invoice.invoiceAmount).as(BTCUnit.SAT);
See classes PaywallTime and PaywallTimeUnit in Javascript API documentation for more details.
5.2.3. Displaying Amount with a Given Unit
The Invoice JSON always returns amount in a base unit (i.e satoshi) with a given magnetude of none, milli or nano.
To display amount in other units such as BTC, micro BTC or BIT etc. There exists a help method to convert invoice amount into a specified unit. There are two ways of doing this:
// One way is to call paywallHttpRequest object
var amountInBit = paywallHttpRequest.paywall.getInvoiceAmount().as(BTCUnit.BIT);
// The other if you have access to invoice object is to create a PaywallAmount object and
// pass the invoice.invoiceAmount.
var amountInBit2 = new PaywallAmount(invoice.invoiceAmount).as(BTCUnit.BIT);
Available BTC Units are:
Unit | Description |
---|---|
BTC |
BTC, i.e 100.000.000 Satoshis. |
MILLIBTC |
One thousand part of BTC, i.e 100.000 Satoshis. |
BIT |
In BIT, i.e 100 Satoshis. |
SAT |
In Satoshis. |
MILLISAT |
In milli satoshis, 1/1000 satoshi. |
NANOSAT |
In nano satoshis, 1/1000.000 satoshi. |
5.2.4. Reusing Settlement for Multiple Calls
If the payment flow is not per-request is is possible to reuse the paywallHttpRequest as long as it has the state SETTLED. To do this just perform a recall of first open() then send() methods. As long as the data in open and send calls fulfils the defined request policy the calls will succeed.
5.2.5. Calling Non-Paywalled Services
It is possible to use PaywallHttpRequest as a regular XMLHttpRequest to non-paywalled services and it will work in the same way with the only difference that onloadstart event is not triggered.
5.2.6. Handling Links from the Invoice JSON Object
The links in Invoice JSON can be both relative or full URLs depending on server side configuration. There exists help methods in PaywallHttpRequest that always constructs the full URLs.
These help methods are: paywallHttpRequest.paywall.genQRLink(), paywallHttpRequest.paywall.genCheckSettlementLink(), paywallHttpRequest.paywall.genCheckSettlementWebSocketLink().
5.2.7. Error Handling
There are three types of error that can occur, either it is a XMLHttpRequest error, API error or Paywall related error.
To handle a XMLHttpRequest error (that is triggered when connection related issues occurs) is 'onerror' event handler called in same way as XMLHttpRequest.
To handle API errors from the underlying service is done in the same way as would have been done in a regular XMLHttpRequest after load is status code and response text examined for error message.
If paywall error occurs is a PAYWALL_ERROR event triggered and the payment flow state is set to PAYWALL_ERROR. The error message can be retrieved with paywallHttpRequest.paywall.getPaywallError(). See table 'Paywall Error JSON Object Properties' for details about the generated error messages.
5.2.8. Available PaywallHttpRequest States
A PaywallHttpRequest has a state that can be fetched by the paywallHttpRequest.paywall.getState() that returns one of the values of the PaywallState enumeration defined in table below.
State | Description |
---|---|
NEW |
Payment flow is new and no invoice have yet been generated. |
INVOICE |
Invoice have been generated and is waiting to be settled. |
INVOICE_EXPIRED |
Generated invoice have expired and a new payment flow have to be generated. |
SETTLED |
Payment have been settled and the payment flow should be ready to perform the call. If multiple calls is possible is up to the settlement type. |
EXECUTED |
Payment type is of type pay per request and request have been processed successfully. Never set if related payment flow is not pay-per-request. Then it will be SETTLED until SETTLEMENT expires. |
SETTLEMENT_NOT_YET_VALID |
Generated settlement is not yet valid and need to wait until call can be performed. |
SETTLEMENT_EXPIRED |
Generated settlement have expired and new payment flow have to be generated. |
PAYWALL_ERROR |
Paywall API related error occurred during processing of payment flow, see paywallError object for details. |
ABORTED |
Request was aborted by the user by calling the abort() method. |
5.2.9. Available Events Generated by PaywallHttpRequest
Wrapped XMLHttpRequest Events
During POST of data the following upload events are also triggered, see XMLHttpRequest standard for details.
One exception is when calling unpaywalled services with PaywallHttpRequest, in that case is 'onloadstart' event never triggered since it was captured when parsing headers for '402 Payment Required' header.
Paywall Specific Events
There are a number of paywall related events extending the regular XMLHttpRequest events in order to handle displaying of invoice and hiding invoice upon settlement.
To register a listener use the method paywallHttpRequest.paywall.addEventListener(name, type, callback), where the name parameter should be a unique name for the listener within the PaywallHttpRequest object, type is one of defined event types in table below, with the special type 'ALL' matching all paywall related events.
To remove a listener from a PaywallHttpRequest use paywallHttpRequest.paywall.removeEventListener(name)
Event Type | Description | Object Type |
---|---|---|
INVOICE |
Invoice have been generated and is waiting to be settled. Time remaining of invoice can be fetched with the paywallHttpRequest.paywall.getInvoiceExpiration() method. |
Invoice JSON Object |
INVOICE_EXPIRED |
Generated invoice have expired and a new payment flow have to be generated. |
Invoice JSON Object |
SETTLED |
Payment have been settled and the payment flow should be ready to perform the call. If multiple calls is possible is up to the settlement type. |
Settlement JSON Object |
EXECUTED |
Payment type is of type pay per request and request have been processed successfully. |
Settlement JSON Object |
SETTLEMENT_NOT_YET_VALID |
Generated settlement is not yet valid and need to wait until call can be performed. Time remaining until settlement is valid can be fetched with the paywallHttpRequest.paywall.getSettlementValidFrom() method. |
Settlement JSON Object |
SETTLEMENT_EXPIRED |
Generated settlement have expired and new payment flow have to be generated. Time remaining until settlement is expired can be fetched with the paywallHttpRequest.paywall.getSettlementExpiration() method. |
Settlement JSON Object |
PAYWALL_ERROR |
Paywall API related error occurred during processing of payment flow, see paywallError object for details. |
Paywall Error Object |
ALL |
Special value used when registering new listener that should receive notification for all events related to this paywall flow. |
N/A |
5.3. Defined JSON Data structures
This section defines all JSON Data Structures used in the underlying API. When using the PaywallHttpRequest the structures are usually sent when event is triggered.
All structures can also be retrieved as XML by setting the 'Accept' header to content type 'application/xml' or appending '.xml' to the request service url.
5.3.1. Invoice JSON Object
An invoice JSON Object is returned whenever a service with @PaymentRequired annotation determines that a new payment flow is required. The service will the return HTTP Status PAYMENT_REQUIRED (402) and the Invoice Json Object as data.
Property | Type | Description |
---|---|---|
status |
String |
The status of the response, should always be 'OK'. Used to indicate if this JSON Object is not an error message. |
type |
String |
The type of JSON Object, always the value 'invoice'. |
preImageHash |
String |
The generated preImageHash from PreImageData which acts as an unique id for the payment flow. The string is base58 encoded. |
bolt11Invoice |
String |
The bolt11 invoice to display for the requester. |
description |
String |
Description to display in the invoice. (Optional). |
invoiceAmount |
Amount Json Object |
The amount in the invoice. (Optional) |
nodeInfo |
Node Info Json Object |
Information about the related lightning node. (Optional) |
token |
String |
The generated JWT invoice token used to track the payment when checking settlement. This is sent in the header of websocket connections or calls the checkSettlement service. |
invoiceDate |
String |
The time this invoice was created. |
invoiceExpireDate |
String |
The time the invoice will expire. |
payPerRequest |
boolean |
If payment is for this api is for one time only or usage is for a given period of time. |
requestPolicyType |
String |
Specifying type of policy used for aggregating significant request data. See section defining RequestPolicyType values in @PaymentRequired annotation for description. |
checkSettlementLink |
String |
Link to settlement controller for checking payment state. Used if it’s not possible to use WebSockets. |
qrLink |
String |
Link to QR Code generator service. This is the full link that can be set in src attribute if <img> tags in order to display the QR Code. |
checkSettlementWebSocketEndpoint |
String |
URL to the WebSocket CheckSettlement EndPoint. This connection is done automatically by PaywallHttpRequest. |
checkSettlementWebSocketQueue |
String |
The preImageHash unique (payment flow) web socket queue to subscribe to. |
Crypto Amount JSON Object
Crypt Amount JSON is a sub object inside the Invoice JSON Object and specifies the amount to pay.
Property | Type | Description |
---|---|---|
value |
Number |
The crypto amount value. For BTC it is based on satoshis with given magnetude. |
currencyCode |
String |
Specification of type of crypto currency. Currently is only 'BTC' supported. |
magnetude |
String |
The magnetude of specified base unit, either 'NONE', 'MILLI' or 'NANO'. If not specified is 'NONE' (full satoshis) assumed. |
Node Info JSON Object
Node Info is an optional sub object to Invoice JSON specifying how to connect to the service lightning node. It is configured on server side if this should be populated or not.
Property | Type | Description |
---|---|---|
publicKeyInfo |
String |
The underlying lightning node’s public Key information. |
nodeAddress |
String |
The underlying lightning the node’s address. |
nodePort |
Number |
The underlying lightning the node’s port. |
mainNet |
Boolean |
If the node is connected to testnet or real production network. |
connectString |
String |
The complete connect string to the lightning node. |
5.3.2. Settlement JSON Object
The settlement JSON Object is sent through the web socket as soon as settlement was detected or as a response to the checkSettlement endpoint. I contains the JWT token that can be used towards the paywalled API as a proof of payment.
Property | Type | Description |
---|---|---|
status |
String |
The status of the response, should always be 'OK'. Used to indicate if this JSON Object is not an error message. |
type |
String |
The type of JSON Object, always the value 'settlement'. |
preImageHash |
String |
The generated preImageHash from PreImageData which acts as an unique id for the payment flow. The string is base58 encoded. (Optional, always set if settled) |
token |
String |
The generated settlement JWT Token that should be set as header value in regular API call in order for the @PaymentRequired annotation to accept it. This is done automatically by PaywallHttpRequest class. (Optional, always set if settled) |
settlementValidUntil |
String |
The date and time the settlement is valid until. (Optional, always set if settled) |
settlementValidFrom |
String |
The date and time the settlement is valid from (Optional). |
payPerRequest |
Boolean |
If related payment is for one request only or if multiple requests can be done that fits the request policy. (Optional, always set if settled) |
settled |
Boolean |
If related payment have been settled. |
5.3.3. Paywall Error JSON Object
The paywall error JSON is created if error occurred in the paywall related components. The error object if occurred can be fetched with paywallHttpRequest.paywall.getPaywallError(). Regular errors from the underlying API is handled in the same way as if XMLHttpRequest would be used.
Property | Type | Description |
---|---|---|
status |
String |
The name of the related the HTTP status code, for example 'UNAUTHORIZED'. The used HTTP status code to exception mapping is described in table below. |
message |
String |
A descriptive error message associated with exception. |
errors |
String |
A list of more detailed error messages of problems that occurred. (Optional) |
reason |
String |
If error is related to JWT token, otherwise null. Available values are EXPIRED, NOT_YET_VALID, NOT_FOUND, INVALID. (Optional) |
There are a defined service side exception to http status mapping for all defined services such as the payment required filter, QR code generator end point and checkSettlement end point.
Http Status Code | Mapped Exception |
---|---|
BAD_REQUEST (400) |
IllegalArgumentException |
UNAUTHORIZED (401) |
IllegalArgumentException |
SERVICE_UNAVAILABLE (504) |
IOException |
INTERNAL_SERVER_ERROR (500) |
All other exceptions |
5.4. Javascript Doc
Latest Javascript API documentation describing all API calls for the PaywallHttpRequest can be found here.
6. For Paywall Developers
LightningJ Paywall is a Java project built using Gradle. Tests is written using Groovy and Spock Framework.
6.1. Building
To build the project use:
./gradlew build
6.2. Testing the framework
The framework have multiple test suites:
-
Java Unit tests using Spock
-
Javascript Unit tests using Jasmine
-
Java Integration tests connecting to LND node.
-
Java Functional tests that tests a Spring Boot application and Javascript library in Chrome and Firefox.
6.2.1. Unit Tests
To run the unit tests run the command:
./gradlew check
This will run both Java and Javascript unit tests at once. To only run jasmine test it is possible by issuing:
./gradlew grunt_jasmine
6.2.2. Integration Tests
There exists a test suite that run LND connection specific tests. To run these you need access to a LND node and configure the following properties in your ~/.gradle/gradle.properties.
paywall.integration.test.lnd.host=<SET THIS> paywall.integration.test.lnd.port=<SET THIS> paywall.integration.test.lnd.tlscertpath=<SET PATH>/tls.cert paywall.integration.test.lnd.macaroonpath=<SET PATH>/admin.macaroon
Then run the tests with the command:
./gradlew integrationTest
6.2.3. Functional Tests
The Javascript API and overall Spring Boot functionality it tested with functional tests. There exists functional tests for Chrome and Firefox browser and requires that you have these browsers installed in you development environment. Sometimes the version of the browser must match the version of the selenium version in paywall-springboot2/build.gradle in order for the test to run properly.
To run the functional test suite run:
./gradlew functionalTest
To only run a specific browser it is possible to instead use the tasks chromeTest or firefoxTest.
6.2.4. Pre-Release Tests
Before release is should unit, integration and functional tests be verified with the command.
./gradlew preReleaseTest
This requires that integration and functional test suites are set up.
6.2.5. Test Reports
Latest Java test reports are located here.
6.3. Generating a Release
6.3.1. GPG Sign Releases using SmartCard
To GPG Sign generated archives before publishing them to central repository using GPG Smartcard make sure to configure the following in ~/.gradle/gradle.properties
signing.gnupg.executable=gpg2 signing.gnupg.useLegacyGpg=false signing.gnupg.keyName=<your key id>
6.3.2. Uploading Archives to Maven Central
The signed releases should be uploaded to maven central. In order to do this there is needed an account in oss.sonatype.org with access rights to manage lightningj projects.
Before uploading set the following to settings in ~/.gradle/gradle.properties
ossrhUsername= ossrhPassword=
Then to build, sign and upload archives to Maven Central run the command (first make sure to update the version):
./gradlew uploadArchives
The release is placed in Staging Repositories section. Select latest ‘open’ and press ‘close’ Enter a comment and wait for verification to complete (Press Refresh). Then press ‘Release’ to release the version. The release will be at Maven Central within a couple of hours.
6.3.3. Updating the Website
To generate documentation use:
./gradlew build preReleaseTest doc
The generated documentation will be placed in build/docs/html5. After verifying it is correct it is possible to publish the web site to github using the command:
./gradlew gitPublishPush
Enter your Github credentials in the GUI and the website will be updated.
6.3.4. Creating a Release Draft in GitHub
To upload javascript to github and draft a new release use:
./gradlew build githubRelease
Before creating a draft set a personal access token from Github and add it to settings in ~/.gradle/gradle.properties
paywallGithubReleaseToken=
Personal access tokens can be generated under developer settings in your github account, the token should have write access to the paywall repository.