Part 15: Adding 3rd party Lightning
Introduction
This guide aims to program a website to accept Bitcoin. In the previous tutorial, We removed “Globee” so that we had 100% sovereignty over our code and by extension our money. Ironically we are going to add “strike” which is a 3rd party lightning payment provider. Just as we did in the past we will start the easy way and then remove it and replace it with our own code at a later date.
A quick note on the current lightning nodes. I played with “LND” and “c-lightning” for a couple of weeks and could not get them to work to a satisfactory level to fulfil our needs “LND” required everything to be on the one server which meant our idea of injecting was just not practical and “c-lightning leaned too heavily on the “elements” project for us to use it a standalone manner. However, this does not concern me as it is amazing software that is still in Alpha.
The SQL
We made significant changes to the SQL (it was due) mainly to give the tables names that made sense.
ECS_ tables
A number of tables to have the ECS_ prefix this means they are core tables to ECS such as user & user settings
ecs_coldstorageaddresses
ecs_emailtemplates
ecs_user
ecs_user_settings
lookup_ tables
the prefix lookup_ was introduced as a way to standardise the lookup data we will use in ECS.
lookup_payment_providers
This table is used to hold the payment providers at the moment we have “Bitcoin Core Node” and “strike” but we may add more in the future.
CREATE TABLE `lookup_payment_providers` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`providername` INTEGER,
`external` INTEGER DEFAULT 0
);
order_ tables
The tables with the order_ prefix are tables that relate directly to the order
order_meta
order_payment_details
order_product
order_product_metaThe product meta holds information about the product such as sizeCREATE TABLE `order_product_meta` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`productid` INTEGER,
`metaname` TEXT,
`metavalue` TEXT
);The payment details table holds which method we used to process payment as well as any charge / return objects that the provided us. CREATE TABLE `order_payment_details` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`address` TEXT,
`providerid` INTEGER,
`paymentobject` TEXT,
`paymentresponseobject` TEXT
);
Strike
“Strike” is an excellent lightning implementation who have taken a lot of the heavy lifting away from us. Simply create an account and then get the api from the settings / API KEYS section as shown below.
add this api key to the .env file along with the endpoint
mainnetSTRIKEENDPOINT=https://api.strike.acinq.co
STRIKEAPIKEY=sk_dsdsdsdsdtestnetSTRIKEENDPOINT=https://api.dev.strike.acinq.co
STRIKEAPIKEY=sk_dsdsdsdsd
The Code
In this tutorial, we have not integrated with SR.js as it was a brand new UX flow we wanted to get it working standalone first and integrate it later. You can find the code for this branch “here”
HTML
<div id="order-preload" align="center">
<div >Generating Invoice...</div>
<div class="lds-dual-ring"></div>
</div>
<div class="order-qrcode" id="order-qrcode" style="display: none">
<div id="order-details">
<div align="center">
<div class="order-pr--number" id="order-id"></div>
<span class="order-pr--pay" id="order-amount"></span>
<span>BTC</span>
</div>
<canvas id="qr" height="500%" width="500%" align="center"></canvas>
<div id="lightaddress" class="order-pr--value"></div>
<div>
<a href="" id="order-pr--wallet" class="order-pr--wallet">Open Wallet</a>
<a href="" id="order-pr--copy" class="order-pr--copy">Copy</a>
</div>
</div>
<div id="order-thanks" class="order-thanks" style="display: none" align="center">
Thanks you for your order
</div>
</div>
The above HTML creates a checkout experience as broken down below. Firstly we create a preloader that we use whilst we connect to the server and generate a charge.
<div id="order-preload" align="center">
<div >Generating Invoice...</div>
<div class="lds-dual-ring"></div>
</div>
Next, we have the QR code as well as the payment address so the user can make payment.
<div class="order-qrcode" id="order-qrcode" style="display: none">
<div id="order-details">
<div align="center">
<div class="order-pr--number" id="order-id"></div>
<span class="order-pr--pay" id="order-amount"></span>
<span>BTC</span>
</div>
<canvas id="qr" height="500%" width="500%" align="center"></canvas>
<div id="lightaddress" class="order-pr--value"></div>
<div>
<a href="" id="order-pr--wallet" class="order-pr--wallet">Open Wallet</a>
<a href="" id="order-pr--copy" class="order-pr--copy">Copy</a>
</div>
</div>
Lastly, we have the thank you screen once payment has been made.
<div id="order-thanks" class="order-thanks" style="display: none" align="center">
Thanks you for your order
</div>
</div>
JAVASCRIPT
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script> <script>
//hold the checkpayment interval function
var checkpaymentres = "";
var serverurl = "http://127.0.0.1:3030";
var address = "";
var thankstext = "Thanks for your order. You will receive nothing.";
var request = new XMLHttpRequest();
//var server =
request.open(
"GET",
serverurl + "/strike/charge?uid=3¤cy=btc&amount=2000&desc=free btc",
true
);
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
// parse the data
var data = JSON.parse(request.responseText);
//debug
//console.log(data.payment)
lightelement = document.getElementById("lightaddress");
lightelement.innerHTML = data.payment.payment_request;
orderid = document.getElementById("order-id");
orderid.innerHTML = "Order " + data.payment.id;
var total = parseFloat(data.payment.amount) * 0.00000001;
orderamount = document.getElementById("order-amount");
orderamount.innerHTML = "Pay " + String(total);
// builds and displays the QR code
new QRious({
element: document.getElementById("qr"),
value: data.payment.payment_request,
size: 400
});
preload = document.getElementById("order-preload");
preload.style = "display: none";
qrcode = document.getElementById("order-qrcode");
qrcode.style = "display: visible";
qrcode = document.getElementById("order-pr--wallet");
qrcode.href = "lightning:" + data.payment.payment_request;
address = data.payment.payment_request;//check for payment every 10 seconds
checkpaymentres = setInterval(checkPayment, 10000);
}
};
request.onerror = function() {
// There was a connection error of some sort
};
request.send();function stopPaymentCheck() {
clearInterval(checkpaymentres);
}function checkPayment() {
//debug
//console.log('check payment ticker')
//var url = serverurl+"/webhook/checkpayment?address="+address+"&token="+token;
//var url = serverurl+"webhook/checkStrikePayment?address="+address;
//debug
console.log("checking for payment for address:" + address);
request.open(
"GET",
serverurl + "/webhook/checkstrikepayment?address=" + address,
true
);
//request.open('GET',"https://ecs.cryptoskillz.com/strike/charge?uid=3¤cy=btc&amount=2000&desc=free btc", true);
//call it
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
// parse the data
var data = JSON.parse(request.responseText);
//debug
//console.log(data.status)if (data.status == 1) {
orderthanks = document.getElementById("order-thanks");
orderthanks.style = "display: visible";
orderthanks.innerHTML = thankstext;orderdetails = document.getElementById("order-details");
orderdetails.style = "display: none";
stopPaymentCheck();
}
}
};
request.onerror = function() {
// There was a connection error of some sort
};
request.send();
}document.getElementById("order-pr--copy").addEventListener("click", function() {
const el = document.createElement("textarea"); // Create a <textarea> element
el.value = address; // Set its value to the string that you want copied
el.setAttribute("readonly", ""); // Make it readonly to be tamper-proof
el.style.position = "absolute";
el.style.left = "-9999px"; // Move outside the screen to make it invisible
document.body.appendChild(el);
el.select(); // Select the <textarea> content
document.execCommand("copy"); // Copy - only works as a result of a user action (e.g. click events)
document.body.removeChild(el);
});
</script>
Let us take a look at this code and see what exactly is happening. The first thing we are doing is loading a QR class
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
Next, we are setting up some variables:
checkpaymentres: this is used to set the time check when we are looking for a payment.
serverurl: the URL of ECS server
address: the address returned from Strike for payment purposes
thankstest: the text to store display once an order is complete
//hold the checkpayment interval function
var checkpaymentres = "";
var serverurl = "http://127.0.0.1:3030";
var address = "";
var thankstext = "Thanks for your order. You will receive nothing.";
Next, we make an ajax call to ECS and telling it we want to generate an invoice.
uid: the userid we are using
currency: the currency we want to create the invoice in
amount: the amount in Satoshis for the invoice
desc: the description of the item we are selling in the invoice
var request = new XMLHttpRequest();
//var server =
request.open(
"GET",
serverurl + "/strike/charge?uid=3¤cy=btc&amount=2000&desc=free btc",
true
);
next, we wait for a response from the server and if the status is valid (between 200 and 400 we process it) we take the information from the response populate the payment view and display it, hide the preloader view and start the checkPayment timer.
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
// parse the data
var data = JSON.parse(request.responseText);
lightelement = document.getElementById("lightaddress");
lightelement.innerHTML = data.payment.payment_request;
orderid = document.getElementById("order-id");
orderid.innerHTML = "Order " + data.payment.id;
var total = parseFloat(data.payment.amount) * 0.00000001;
orderamount = document.getElementById("order-amount");
orderamount.innerHTML = "Pay " + String(total);
// builds and displays the QR code
new QRious({
element: document.getElementById("qr"),
value: data.payment.payment_request,
size: 400
});
preload = document.getElementById("order-preload");
preload.style = "display: none";
qrcode = document.getElementById("order-qrcode");
qrcode.style = "display: visible";
qrcode = document.getElementById("order-pr--wallet");
qrcode.href = "lightning:" + data.payment.payment_request;
address = data.payment.payment_request;//check for payment every 10 seconds
checkpaymentres = setInterval(checkPayment, 10000);
}
};
request.onerror = function() {
// There was a connection error of some sort
};
request.send();
Next, we have a couple of functions checkPayment and stopPayment. CheckPayment calls the ECS server and checks for it payment has been made by the user every 10 seconds. Once a payment has been made then it calls shows the thankyou view, hides the payment view and stops the checkPayment timer.
function stopPaymentCheck()
{
clearInterval(checkpaymentres);
}function checkPayment()
{
request.open(
"GET",
serverurl + "/webhook/checkstrikepayment?address=" + address,
true
);
//request.open('GET',"https://ecs.cryptoskillz.com/strike/charge?uid=3¤cy=btc&amount=2000&desc=free btc", true);
//call it
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
// parse the data
var data = JSON.parse(request.responseText);
//debug
//console.log(data.status)if (data.status == 1) {
orderthanks = document.getElementById("order-thanks");
orderthanks.style = "display: visible";
orderthanks.innerHTML = thankstext;orderdetails = document.getElementById("order-details");
orderdetails.style = "display: none";
stopPaymentCheck();
}
}
};
request.onerror = function() {
// There was a connection error of some sort
};
request.send();
}
lastly, we have a listener that fires when the copy href has been clicked and copies the address to the clipboard
document.getElementById("order-pr--copy").addEventListener("click", function() {
const el = document.createElement("textarea"); // Create a <textarea> element
el.value = address; // Set its value to the string that you want copied
el.setAttribute("readonly", ""); // Make it readonly to be tamper-proof
el.style.position = "absolute";
el.style.left = "-9999px"; // Move outside the screen to make it invisible
document.body.appendChild(el);
el.select(); // Select the <textarea> content
document.execCommand("copy"); // Copy - only works as a result of a user action (e.g. click events)
document.body.removeChild(el);
});
SERVER
We made several changes to the server to work with the refactored database changes as mentioned above. We will not list these here, will stick to the strike changes only.
The first function we added was a route to create a charge. This calls the strike helper.
app.get("/strike/charge", (req, res) => {
res = generic.setHeaders(res);
//load the back office helper
let strikehelper = require('./api/helpers/strike.js').strike;
let strike = new strikehelper();//debug
strike.charge(req,res);
});
The strike helper charge function requests a charge from strike stores in our session table with the information amount, currency etc and returns the details so an invoice can be generated.
/*Just as we did with BTC we started off using a 3rd party API and once we had an understanding of how things work
we moved onto owing the entire stack. We are doing the exact same thing with LightningWe are using the rather excellent https://strike.acinq.co for this purpose.*/
const config = require('./config');
//console.log(config.bitcoin.network)//load SQLlite (use any database you want or none)
const sqlite3 = require("sqlite3").verbose();
//open a database connection
let db = new sqlite3.Database("./db/db.db", err => {
if (err) {
console.error(err.message);
}
});var request = require("request");//note why uppercase here?
var strike = function ()
{
this.test = function test(req,res)
{res.send(JSON.stringify({ status: "ok" }));
}//create a charge
this.charge = function charge(req,res)
{
//build the options object
var options = {
method: 'POST',
url: process.env.STRIKEENDPOINT + '/api/v1/charges',
headers: {
'cache-control': 'no-cache',
'Content-Type': 'application/json' },
body: {
amount: parseFloat(req.query.amount),
description: req.query.desc,
currency: req.query.currency
},
json: true,
auth: {
user: process.env.STRIKEAPIKEY,
pass: '',
}
};//call strike
request(options, function (error, response, body) {
if (error) throw new Error(error);
//debug
//console.log(body)//turn it into a BTC amount
//note : in a future update we may go ahead and store everything Satoshis.
// we could also use req.query.amount here
// we may want to store order_meta and product_meta here in the future if so we will make those generic functions
var amount = parseFloat(body.amount) * 0.00000001;
//insert a session
db.run(
`INSERT INTO sessions(address,userid,net,amount,paymenttype) VALUES(?,?,?,?,?)`,
[body.payment_request, req.query.uid, process.env.LIGHTNETWORK,String(amount),2],
function(err)
{
if (err)
{
//return error
res.send(JSON.stringify({ error: err.message }));
return;
}//store the order product details
db.run(
`INSERT INTO order_product(address,name,price,quantity) VALUES(?,?,?,?)`,
[body.payment_request,req.query.desc, String(amount),1],
function(err)
{
if (err)
{
//return error
res.send(JSON.stringify({ error: err.message }));
return;
}
//store the order_payment_details
db.run(
`INSERT INTO order_payment_details(address,providerid,paymentobject) VALUES(?,?,?)`,
[body.payment_request,2, JSON.stringify(body)],
function(err)
{
if (err)
{
//return error
res.send(JSON.stringify({ error: err.message }));
return;
}
//return the required details to the front end
var obj = {id:body.id,amount:body.amount,payment_request:body.payment_request}
res.send(JSON.stringify({ payment: obj }));
//debug
//console.log(body.payment_request);
}
);
}
);
}
);
});
}}
exports.strike = strike;
We added 2 functions to webhook to deal with Strike. The first check payment simply looks in the database to see if a payment has been processed and if it has it returns 1 if it has not it returns 0.
APP.JSapp.post("/webhook/checkstrikepayment", (req, res) => {
res = generic.setHeaders(res);
//load the back office helper
let webhookhelper = require('./api/helpers/webhook.js').webhook;
let webhook = new webhookhelper();//debug
webhook.checkStrikePayment(req,res);
});WEBHOOK HELPERthis.checkStrikePayment = function checkStrikePayment(req,res)
{
//debug
//console.log(req.body.data.payment_request)
//return;
let data = [1,1, req.body.data.payment_request];
let sql = `UPDATE sessions SET processed = ?,swept=? WHERE address = ?`;
db.run(sql, data, function(err) {
//console.log(result)
if (err) {
res.send(JSON.stringify({ status: 0 }));
}
//store payment object
let data = [JSON.stringify(req.body.data),req.body.data.payment_request];
let sql = `UPDATE order_payment_details SET paymentresponseobject = ? WHERE address = ?`;
db.run(sql, data, function(err)
{
if (err) {
res.send(JSON.stringify({ status: 0 }));
}
//send emails to admin
generic.sendMail(2,'cryptoskillz@protonmail.com');res.send(JSON.stringify({ status: 1 }));
});
});}
The next function waits for Strike to process payment when it does it calls this URL with the information and we update our database accordingly
APP.JSapp.get("/webhook/strikenotification", (req, res) => {
res = generic.setHeaders(res);
//load the back office helper
let webhookhelper = require('./api/helpers/webhook.js').webhook;
let webhook = new webhookhelper();//debug
webhook.strikeNotification(req,res);
});WEBHOOK HELPER//recieve a payment notificaiotn from strike
this.strikeNotification = function strikeNotification(req,res)
{
//todo: store the payment object
if (req.query.address != '')
{
let data = [1,1, req.query.address];
let sql = `UPDATE sessions SET processed = ?,swept=? WHERE address = ?`;
db.run(sql, data, function(err) {
if (err) {
return console.error(err.message);
}
res.send(JSON.stringify({ "status": "ok" }));
});
}
}
Conclusion
Now we have Integrated Lightning into our stack via a 3rd party the next logical steps are
- integrate into SR.js
- replace strike with our lightning node
We will focus on point 1 in the next tutorial and point 2 some point in the future.