自前ノードを使って簡単なライトニングアプリを作ってみる(前編)

自前ノードを使って簡単なライトニングアプリを作ってみる(前編)

ライトニングノードを立ち上げてから「自分のノードを使ったアプリとか作ってみたいな〜」とずっと思ってたので、この機会に最近プログラミング始めてみました。
パソコン苦手だったので、最初すごい拒否反応がありました笑
というわけで超初心者ですが、今回は自前ノードを使った簡単なアプリを作りたいと思います。

成果物は記事最下部にあります↓

フォームに入力した金額分のインボイスを発行し、支払いが完了したらメッセージを表示する、といったアプリです。質素なものですが(後編では少し見栄えも整えたいと思います)、これを自分のノードを使って作りたいと思います。

準備

今回のアプリでは主にvue.js、expressを使います。アプリは自分でホスティングするのではなくherokuにデプロイするので、
node.js
vue cli
heroku cli
git
LNDノード(umbrelも可)
を使用します。

lnd.confの編集,TLS証明書の作成

LNDではノードを操作するためのインターフェースとしてgRPCRESTが提供されています。前編ではgRPCを、後編ではRESTとTorを使用します。
LNDへの接続はTLSを使用して暗号化されるため、ノードに接続するときは、クライアントがTLS証明書を使用する必要があります。
デフォルトでは、ローカルホストからの接続のみを許可するように設定されてるので、外部からLNDノードを操作する場合は、これを許可するようにTLS証明書を変更する必要があります。
LNDではlnd.confにtlsextraipを設定することで、ローカルホスト以外のIPアドレスを介してLNDに接続できるようになるので、アプリからLNDノードに接続するために、ノードのグローバルIPアドレスをtlsextraipに設定します。また、IPアドレスの代わりにtlsextradomainでドメインを追加することもできます。加えて、rpcインターフェースを外部に公開するように、rpclistenを以下のように設定します。

tlsextraip=ノードのIPアドレス
rpclisten=0.0.0.0:10009

tlsextraiptlsextradomainは複数追加できます。umbrelの場合はすでに設定されているものを消さないようにしてください。
編集が完了したら古いTLS証明書を削除します。

rm ~/.lnd/tls.*
umbrelの場合は
rm ~/umbrel/lnd/tls.*

削除したらLNDを再起動してください。LNDが正常に起動すれば、新しくTLS証明書が作成されます。umbrelの場合だと再起動しても作成されない時があったので、その場合は何回か再起動してみてください。

プロジェクトの作成

lnd.confの編集が完了したらアプリを作成していきます。まずはvue cliを使ってプロジェクトを作成します。ターミナルで下記コマンドを実行します。

vue create lapp-sample

vue2のデフォルトのプリセットでいいのでDefault ([Vue 2] babel, eslint)を選択します。
完成したらプロジェクトのディレクトリに移動し、

cd lapp-sample

サーバーを起動します。

npm run serve

サーバーはポート8080番で起動するので、ブラウザでhttp://localhost:8080を開きます。以下のページが表示されていれば正常に起動しています。(ターミナルでctrl+cで停止)

サーバーサイドの作成

次にサーバーサイドを作成していきます。
まずは必要なパッケージをインストールします。

npm install express socket.io ln-service

今回はgRPCクライアントにalexbosworth氏ln-serviceを使用します。ln-serviceを使用することで、簡単にgRPCを介してノードに接続することができます。

次にプロジェクトのルートディレクトリにserverを作成し、配下のindex.jsにコードを書いていきます。

mkdir server && touch server/index.js

index.jsは以下のようにしました。createInvoiceでインボイスを作成、subscribeToInvoicesでインボイスの更新を購読、支払いがあったタイミングでインボイスをクライアントに送信します。

const express = require("express");
const app = express();
const server = require("http").createServer(app);
const { Server } = require("socket.io");
const io = new Server(server, {
  path: "/ws",
});
const {
  authenticatedLndGrpc,
  createInvoice,
  subscribeToInvoices,
} = require("ln-service");

const PORT = 3000;

const { lnd } = authenticatedLndGrpc({
  cert: "ここを書き換えます",
  macaroon: "ここを書き換えます",
  socket: "ここを書き換えます",
});

app.use(express.json());

app.use(express.static("dist"));

app.post("/api/invoice", async (req, res) => {
  try {
    const { tokens } = req.body;
    const { request } = await createInvoice({ lnd, tokens });
    res.send(request);
  } catch (error) {
    res.status(500).send({ msg: "Failed to create invoice" });
  }
});

const sub = subscribeToInvoices({ lnd });

sub.on("invoice_updated", (invoice) => {
  if (!invoice.is_confirmed) return;

  io.emit("invoice_settled", invoice.request);
});

server.listen(PORT, () => {
  console.log(`listening on port ${PORT}`);
});

各自設定が必要な部分はauthenticatedLndGrpccertmacaroonsocketです。この三つをLNDに合わせた値に設定することで、ノードに接続することができます。これらはあとで環境変数に設定するので、まずはローカルで動作確認するためにコードに直接書いていきます。
一つ目のcertlnd.confの編集で作り直したtls.certをbase64にエンコードしたものです。下記コマンドを実行することで取得できます。

base64 ~/.lnd/tls.cert | tr -d '\n'
umbrelの場合
base64 ~/umbrel/lnd/tls.cert | tr -d '\n'

二つ目のmacaroonはLNDのmacaroonファイルをbase64にエンコードしたものです。macaroonファイルはクライアントがLNDの操作について権限があるかどうかを確認するためのファイルで、権限別にadmin.macarooninvoice.macaroonreadonly.macaroonが存在します。権限は名前の通りです。今回はinvoiceの操作のみ行えれば良いので、invoice.macaroonを使用します。

base64 ~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon | tr -d '\n'
umbrelの場合
base64 ~/umbrel/lnd/data/chain/bitcoin/mainnet/invoice.macaroon | tr -d '\n'

三つ目のsocketにはノードのグローバルIPアドレスを設定します。環境によってはルーターでポート転送をする必要があるかもしれません。socketで設定したポートからのパケットをノードのポート10009に転送するように設定してください。
※自宅の回線がIPv6サービス(transix、DS-Lite等)を利用している場合、おそらくポート転送ができません。IPv4 PPPoEと併用することでできるようになりますが、少し手間がかかります。後編ではTorのhidden serviceを利用するのでポート転送は必要ありません。

以上三つが確認出来たらauthenticatedLndGrpcの部分を書き換えてください(これらの値を第三者に教えたり公開したりしないでください!)。こんな感じになるとおもいます。

const { lnd } = authenticatedLndGrpc({
  cert: "LS0tLSL..........0tLS0tDg==",
  macaroon: "AgEKbC..........n4cUct",
  socket: 1.2.3.4:56789,
});

フロントエンドの作成

サーバーサイドの作成が終わったので、次はフロントエンドを作成していきます。まずは必要なパッケージをインストールします。

npm install axios socket.io-client

プロキシを設定します。プロジェクトのルートディレクトリにvue.config.jsを作成し

touch vue.config.js

以下のようにします。

module.exports = {
  devServer: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
      },
      "/ws": {
        target: "http://localhost:3000",
        ws: true,
      },
    },
  },
};

インストールが完了したらsrc/App.vueを編集していきます。

<template>
  <div id="app">
    <template v-if="!settled">
      <img alt="Vue logo" src="./assets/logo.png" />
      <form @submit.prevent="createInvoice">
        <input type="number" v-model="amount" />
        <button type="submit">Pay</button>
      </form>
      <p style="word-break: break-all;">{{ request }}</p>
    </template>
    <template v-else>
      <h1>Payment successful!</h1>
      <button @click.prevent="done">Done</button>
    </template>
  </div>
</template>

<script>
import axios from "axios";
import io from "socket.io-client";
const socket = io({ path: "/ws" });

export default {
  name: "App",
  data: () => ({
    amount: null,
    request: null,
    settled: false,
  }),
  methods: {
    async createInvoice() {
      try {
        const { data } = await axios.post("/api/invoice", {
          tokens: this.amount,
        });
        this.request = data;
      } catch (error) {
        alert(error.response.data.msg);
      }
    },
    done() {
      this.amount = null;
      this.request = null;
      this.settled = false;
    },
    ws() {
      socket.on("invoice_settled", (request) => {
        if (this.request === request) this.settled = true;
      });
    },
  },
  created() {
    this.ws();
  },
};
</script>

styleタグは省略していますがそのままにしてます。ロゴもかっこいいので残しました。Payment successful!の部分は支払いが完了した際に表示するメッセージです。自由に書き換えてください。

動作確認

準備が整ったので動作確認してみます。サーバーを起動します。

npm run serve
node server/index.js

サーバーが起動したらブラウザでhttp://localhost:8080にアクセスします。フォームに金額を入力しPayボタンを押してください。うまくいけばインボイスが届きます。届いたらウォレットでインボイスの支払いを行ってください。支払いが完了したタイミングで設定したメッセージが表示されれば正常に動作しています。

デプロイ

せっかくなのでherokuにデプロイしてみます。まずserver/index.jsの一部を書き換えます。

const PORT = process.env.PORT;

const { lnd } = authenticatedLndGrpc({
  macaroon: process.env.MACAROON,
  cert: process.env.CERT,
  socket: process.env.SOCKET,
});

次にpackage.jsonを編集します。

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "start": "node ./server/index.js" この行を追加してください
  },

アプリを作成します。ターミナルで下記コマンドを実行します。

heroku create

出力されるhttps://xxx-xxx-xxx.herokuapp.comがアプリのURLです。

次に環境変数を設定します。サーバーサイドの作成で確認した三つの値をそれぞれ設定してください。

heroku config:set CERT="ここに設定します"
heroku config:set MACAROON="ここに設定します"
heroku config:set SOCKET="ここに設定します"

設定できたらデプロイします。

git add . && git commit -m "init" && git push heroku master

Build succeeded!と出力されていればデプロイ成功です。アプリのURLにアクセスしてみてください。こんな感じに動けば成功です!

さいごに

簡単なものですが作ることができました。後編ではQRコードを表示させたりもうちょっと見た目も整えたいと思います。お疲れ様でした!

[追記]IPアドレスの取得

グローバルIPアドレスの取得は下記コマンドを実行してください。

curl inet-ip.info

また、ローカルIPアドレスは以下のコマンドで取得できます。

ip a

有線で運用されているかたはeth0,無線の方はwlan0inetがローカルIPアドレスです。

Remaining : 0 characters / 0 images
100

Sign up / Continue after login

Related stories

Writer

ブケレ組です

Share

Popular stories

チャネルバックアップファイルを自動でコピーしてみる!

70

チャネルバックアップファイルを自動でクラウドに保存してみる

53