自前ノードを使って簡単なライトニングアプリを作ってみる
ライトニングノードを立ち上げてから「自分のノードを使ったアプリとか作ってみたいな〜」とずっと思ってたので、この機会に最近プログラミング始めてみました。
パソコン疎いので環境構築からめっちゃ苦戦してます。
というわけで色々ど素人ですが、今回は自前ノードを使った簡単なアプリを作りたいと思います。
準備
以下のものを使用します。
- Node.js
- Vue CLI
- Heroku CLI
- Git
- LNDノード(Umbrelも可)
lnd.confの編集, TLS証明書の作成
LNDではノードを操作するためのインターフェースとしてgRPCとRESTが提供されています。
LNDへの接続はTLSを使用して暗号化されるため、ノードに接続するときは、クライアントがTLS証明書を使用する必要があります。
デフォルトでは、ローカルホストからの接続のみを許可するように設定されてるので、外部からLNDノードを操作する場合は、これを許可するようにTLS証明書を変更する必要があります。
LNDではlnd.confにtlsextraip
を設定することで、ローカルホスト以外のIPアドレスを介してLNDに接続できるようになります。したがって、アプリからLNDノードに接続するために、ノードのグローバルIPアドレスをtlsextraip
に設定します。また、RPCインターフェースを外部に公開するように、rpclisten
を以下のように設定します。
tlsextraip=ノードのIPアドレス
rpclisten=0.0.0.0:10009
tlsextraip
、tlsextradomain
は複数追加できます。Umbrelの場合はすでに設定されているものを消さないようにしてください。
編集が完了したら古いTLS証明書を削除します。
rm ~/.lnd/tls.*
umbrelの場合は
rm ~/umbrel/lnd/tls.*
削除したらLNDを再起動してください。新しいTLS証明書が作成されます。
プロジェクトの作成
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クライアントにalex bosworth氏の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: "/api/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}`);
});
各自設定が必要な部分はauthenticatedLndGrpc
のcert
とmacaroon
とsocket
です。この三つをLNDに合わせた値に設定することで、ノードに接続することができます。これらはあとで環境変数に設定するので、まずはローカルで動作確認するためにコードに直接書いていきます。
一つ目のcert
はlnd.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.macaroon
、invoice.macaroon
、readonly.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に転送するように設定してください。
以上、三つが確認出来たら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: 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: "/api/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
https://xxx-xxx-xxx.herokuapp.com/ deployed to Heroku
と出力されていればデプロイ完了です。アプリのURLにアクセスしてみてください。こんな感じに動けば成功です。
お疲れ様でした!
[追記]IPアドレスの取得
グローバルIPアドレスの取得は下記コマンドを実行してください。
curl inet-ip.info