Введение
Это руководство направлено на программирование веб-сайта для приема биткойнов. В предыдущем уроке мы удалили « Глобус , чтобы у нас было 100% суверенитета над нашим кодом и, соответственно, над нашими деньгами. По иронии судьбы, мы собираемся добавить strike, который является сторонним поставщиком платежей за молнию. Как и в прошлом, мы начнем с простого, а затем удалим его и заменим нашим собственным кодом позже.
Небольшая заметка о текущих узлах молнии. Я играл с « LND и c-lightning » пару недель и не мог заставить их работать на удовлетворительном уровне, чтобы удовлетворить наши потребности. « LND требовал, чтобы все было на одном сервере, что означало, что наша идея инъекции была просто непрактичной, а «c-lightning слишком сильно опирался на «elements », чтобы мы могли использовать его автономно. Однако меня это не волнует, поскольку это потрясающее программное обеспечение, которое все еще находится в альфа-версии.
SQL
Мы внесли значительные изменения в SQL (это было обусловлено) в основном для того, чтобы имена таблиц имели смысл.
ECS_ столы
Некоторые таблицы должны иметь префикс ECS_, это означает, что они являются основными таблицами для ECS, такими как пользовательские и пользовательские настройки.
ecs_coldstorageaddresses ecs_emailtemplates ecs_user ecs_user_settings
таблицы поиска
префикс lookup_ был введен как способ стандартизации данных поиска, которые мы будем использовать в ECS.
lookup_payment_providers
Эта таблица используется для хранения поставщиков платежей на данный момент у нас есть «Bitcoin Core Node» и «strike», но мы можем добавить больше в будущем.
CREATE TABLE `lookup_payment_providers` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `providername` INTEGER, `external` INTEGER DEFAULT 0 );
заказ_ столы
Таблицы с префиксом order_ - это таблицы, которые напрямую относятся к заказу.
order_meta order_payment_details order_product order_product_meta The product meta holds information about the product such as size CREATE 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 - отличная молниеносная реализация, которая сняла с нас большую часть тяжелой работы. Просто создайте учетную запись, а затем получите api из раздела настроек / API KEYS, как показано ниже.
добавьте этот ключ api в файл .env вместе с конечной точкой
mainnet STRIKEENDPOINT=https://api.strike.acinq.co STRIKEAPIKEY=sk_dsdsdsdsd testnet STRIKEENDPOINT=https://api.dev.strike.acinq.co STRIKEAPIKEY=sk_dsdsdsdsd
Код
В этом руководстве мы не выполняли интеграцию с SR.js, поскольку это был совершенно новый UX-поток, который мы хотели сначала запустить автономно, а затем интегрировать. Вы можете найти код этой ветки здесь
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>
Приведенный выше HTML-код создает удобство оформления заказа, как показано ниже. Сначала мы создаем предварительный загрузчик, который используем при подключении к серверу и генерируем плату.
<div id="order-preload" align="center"> <div >Generating Invoice...</div> <div class="lds-dual-ring"></div> </div>
Затем у нас есть QR-код, а также платежный адрес, чтобы пользователь мог произвести оплату.
<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>
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>
Давайте взглянем на этот код и посмотрим, что именно происходит. Первое, что мы делаем, это загружаем QR-класс.
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
Далее мы настраиваем некоторые переменные:
checkpaymentres: используется для установки времени проверки, когда мы ищем платеж.
serverurl: URL-адрес сервера ECS
адрес: адрес, возвращаемый Strike для целей оплаты.
Thankstest: текст для сохранения отображается после завершения заказа
//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.";
Затем мы выполняем ajax-вызов ECS и сообщаем ему, что мы хотим сгенерировать счет.
uid: идентификатор пользователя, который мы используем
currency: валюта, в которой мы хотим создать счет в
сумме: сумма в сатоши для счета-фактуры
desc: описание товара, который мы продаем, в счете-фактуре.
var request = new XMLHttpRequest(); //var server = request.open( "GET", serverurl + "/strike/charge?uid=3¤cy=btc&amount=2000&desc=free btc", true );
Затем мы ждем ответа от сервера и, если статус действителен (между 200 и 400, мы его обрабатываем), мы берем информацию из ответа, заполняем просмотр платежа и отображаем его, скрываем предварительный загрузчик просмотрите и запустите таймер checkPayment.
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();
Далее у нас есть пара функций checkPayment и stopPayment. CheckPayment вызывает сервер ECS и проверяет, был ли платеж произведен пользователем каждые 10 секунд. После того, как платеж был произведен, он вызывает, показывает вид благодарности, скрывает вид платежа и останавливает таймер checkPayment.
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(); }
наконец, у нас есть слушатель, который срабатывает при нажатии на copy href и копирует адрес в буфер обмена.
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); });
СЕРВЕР
Мы внесли несколько изменений в сервер для работы с отредактированными изменениями базы данных, как упоминалось выше. Мы не будем перечислять их здесь, остановимся только на изменениях в забастовке.
Первой добавленной нами функцией был маршрут для создания заряда. Это вызывает помощника по забастовке.
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); });
Функция заряда помощника по забастовке запрашивает оплату из забастовочных магазинов в нашей таблице сеанса с информацией о сумме, валюте и т. Д. И возвращает детали, чтобы можно было сгенерировать счет.
/* 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 Lightning We 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;
Мы добавили в webhook 2 функции для работы с Strike. Первый чековый платеж просто просматривает базу данных, чтобы узнать, был ли платеж обработан, и если он есть, он возвращает 1, если нет, он возвращает 0.
APP.JS app.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 HELPER this.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,'[email protected]'); res.send(JSON.stringify({ status: 1 })); }); }); }
Следующая функция ожидает, пока Strike обработает платеж, когда он вызывает этот URL с информацией, и мы соответствующим образом обновляем нашу базу данных.
APP.JS app.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" })); }); } }
Вывод
Теперь у нас есть интегрированная Lightning в наш стек через стороннюю организацию, следующие логические шаги:
- интегрировать в SR.js
- замените strike на наш узел молнии
Мы сосредоточимся на пункте 1 в следующем уроке и пункте 2 в будущем.