Введение

Это руководство направлено на программирование веб-сайта для приема биткойнов. В предыдущем уроке мы удалили « Глобус , чтобы у нас было 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&currency=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&currency=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&currency=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&currency=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 в наш стек через стороннюю организацию, следующие логические шаги:

  1. интегрировать в SR.js
  2. замените strike на наш узел молнии

Мы сосредоточимся на пункте 1 в следующем уроке и пункте 2 в будущем.