Skip to content

Instantly share code, notes, and snippets.

@bitkotaro
Created November 12, 2021 11:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bitkotaro/55de811847ae1bba55739e2a58795374 to your computer and use it in GitHub Desktop.
Save bitkotaro/55de811847ae1bba55739e2a58795374 to your computer and use it in GitHub Desktop.
HS_HOST=
LND_MACAROON=
LND_CERT=
<template>
<div id="app">
<Nav />
<Modal v-show="modal" />
<Home v-show="!settled" />
<Settled v-show="settled" />
</div>
</template>
<script>
import Home from "./views/Home.vue";
import Settled from "./views/Settled.vue";
import Nav from "./components/Nav.vue";
import Modal from "./components/Modal.vue";
import { mapState } from "vuex";
export default {
name: "App",
components: {
Home,
Settled,
Modal,
Nav,
},
computed: {
...mapState(["modal", "settled"]),
},
created() {
this.$store.dispatch("ws");
},
};
</script>
<style lang="scss">
@import "@/assets/stylesheet/style.scss";
</style>
<template>
<div class="container">
<form @submit.prevent="createInvoice">
<label for="value"></label>
<input type="number" id="value" v-model="value" />
<br />
<button type="submit" class="btn" :disabled="!value">
<span class="fa-stack">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fas fa-bolt fa-stack-1x"></i>
</span>
Pay with lightning
</button>
</form>
</div>
</template>
<script>
export default {
name: "Home",
data: () => ({
value: null,
}),
methods: {
createInvoice() {
this.$store.dispatch("createInvoice", this.value);
},
},
};
</script>
// server/routes/invoices.js
const express = require("express");
const router = express.Router();
const { createInvoice } = require("../controllers/lnd-rest");
router.post("/", async (req, res, next) => {
try {
if (!req.body.value) {
res
.status(400)
.send({ error: `A value is required to create an invoice` });
} else {
const request = await createInvoice(req.body.value);
res.send({ request });
}
} catch (error) {
next(error);
}
});
module.exports = router;
// server/controllers/lnd-rest.js
const SocksProxyAgent = require("socks-proxy-agent");
const got = require("got");
const WebSocket = require("ws");
const agent = new SocksProxyAgent("socks5h://127.0.0.1:9050");
const host = process.env.HS_HOST;
const macaroon = Buffer.from(process.env.LND_MACAROON, "base64").toString("hex");
const cert = Buffer.from(process.env.LND_CERT, "base64").toString("ascii");
const createInvoice = async (value) => {
const { payment_request } = await got
.post(`https://${host}:8080/v1/invoices`, {
headers: {
"Grpc-Metadata-macaroon": macaroon,
},
agent: { https: agent },
https: { certificateAuthority: cert },
json: {
value,
memo: "Sample lapp payment",
},
})
.json();
return payment_request;
};
const subscribeToInvoices = (io) => {
let ws;
ws = new WebSocket(`wss://${host}:8080/v1/invoices/subscribe`, {
headers: {
"Grpc-Metadata-macaroon": macaroon,
},
agent,
ca: cert,
});
ws.on("open", () => {
console.log("Connected to LND");
});
ws.on("message", (message) => {
const msg = JSON.parse(message);
if (msg.error || !msg.result.settled) return;
io.emit("invoice_settled", { request: msg.result.payment_request });
});
ws.on("error", (error) => {
console.log(`LND conection error: ${error}`);
ws.close();
});
ws.on("close", (reason) => {
console.log(`LND connection failure: ${reason}`);
setTimeout(() => {
subscribeToInvoices(io);
}, 5000);
});
};
module.exports = { createInvoice, subscribeToInvoices };
<template>
<div class="overlay">
<div class="modal">
<div class="modal-header">
<template v-if="request"> Awaiting payment </template>
<template v-if="error">
<div>
<i class="fas fa-exclamation-triangle error"></i>
Ooops!
</div>
</template>
<div class="loading" v-if="!error">
<span v-for="(n, i) in 3" :key="i"></span>
</div>
</div>
<div class="modal-content">
<template v-if="request">
<vue-qrcode class="qr" :value="request" :width="200"></vue-qrcode>
<span id="request" style="display: none">{{ request }}</span>
<button class="btn" @click.prevent="copyRequest" :disabled="copied">
{{ copied ? "Copied!" : "COPY" }}
</button>
<p>or</p>
<a class="btn joule-btn" :href="`lightning:${request}`">
<i class="fas fa-wallet fa-sm"></i>
Open in Wallet
</a>
</template>
<template v-if="error">
<div class="error-msg">
Error: <strong> {{ error }}</strong>
</div>
<button class="btn" @click.prevent="done">OK</button>
</template>
</div>
</div>
</div>
</template>
<script>
import VueQrcode from "vue-qrcode";
import { mapState } from "vuex";
export default {
components: {
VueQrcode,
},
data: () => ({
copied: false,
}),
computed: {
...mapState(["request", "error"]),
},
methods: {
copyRequest() {
const text = document.getElementById("request").innerText;
const input = document.createElement("input");
input.setAttribute("value", text);
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.parentElement.removeChild(input);
this.copied = true;
},
done() {
this.$store.dispatch("done");
},
},
};
</script>
<template>
<div class="nav">
Sample Lapp
<img
class="logo"
alt="Vue logo"
src="../assets/image/logo.png"
width="40"
height="40"
/>
</div>
</template>
<script>
export default {};
</script>
// server/index.js
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" });
if (process.env.NODE_ENV !== "production") {
require("dotenv").config({ path: ".env.local" });
}
const invoicesRouter = require("./routes/invoices.js");
const { subscribeToInvoices } = require("./controllers/lnd-rest.js");
app.use(express.json());
app.use(express.static("dist"));
app.get("/", (req, res) => {
res.sendFile("/dist/index.html");
});
app.use("/api/invoices", invoicesRouter);
app.use((error, req, res, next) => {
console.log(error);
res.status(500).send({ error: "Internal server error" });
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server started at port ${PORT}`);
});
subscribeToInvoices(io);
<template>
<div class="container">
<video
src="https://drive.google.com/uc?id=1sUojduyQcS4dnsovHFeocy4vp1Y33MCn&export=download"
autoplay
playsinline
muted
loop
></video>
<div class="settled-msg">
<p>Thank you!</p>
<p>Your payment has been received</p>
</div>
<button class="btn" @click.prevent="done">DONE</button>
</div>
</template>
<script>
export default {
methods: {
done() {
this.$store.dispatch("done");
},
},
};
</script>
// src/store/index.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
import { io } from "socket.io-client";
const socket = io({ path: "/api/ws" });
Vue.use(Vuex);
export default new Vuex.Store({
state: {
request: null,
settled: false,
modal: false,
error: false,
},
mutations: {
setRequest: (state, request) => (state.request = request),
setSettled: (state, bool) => (state.settled = bool),
setModal: (state, bool) => (state.modal = bool),
setError: (state, error) => (state.error = error),
},
actions: {
async createInvoice({ commit }, value) {
try {
commit("setModal", true);
const { data } = await axios.post("/api/invoices", { value });
commit("setRequest", data.request);
} catch (error) {
commit("setError", error.response.data.error);
}
},
ws({ state, commit }) {
socket.on("invoice_settled", ({ request }) => {
if (state.request === request) {
commit("setSettled", true);
commit("setModal", false);
commit("setRequest", null);
}
});
},
done({ commit }) {
commit("setModal", false);
commit("setError", false);
commit("setSettled", false);
},
},
modules: {},
});
body {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
height: 100vh;
margin: 0;
}
#app {
height: 100%;
}
/* ------------ variable ------------ */
$primary: #42b883;
$light: #78ebb3;
$dark: #008756;
/* ------------ Nav ------------ */
.nav {
width: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
margin: 5px;
font-size: 40px;
font-weight: bold;
.logo {
padding-left: 10px;
}
}
/* ------------ Home, Settled ------------ */
.container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.logo {
width: 200px;
}
input {
height: 30px;
border: 1px solid #dadada;
border-radius: 20px;
margin: 40px;
&:focus {
outline: 0;
border: 1px solid $primary;
}
}
video {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
z-index: -100;
background-color: #000;
background-size: cover;
}
.settled-msg {
margin: 10px;
p {
color: #fff;
font-weight: bold;
font-size: 50px;
margin: 0;
}
& p:nth-child(2) {
font-size: 18px;
}
}
}
/* ------------ Modal ------------ */
.overlay {
z-index: 100;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
padding: 0 !important;
.modal {
z-index: 100;
width: 300px;
border-radius: 20px;
margin: 2em;
padding: 1em;
background: #fff;
display: flex;
flex-direction: column;
.modal-header {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: $primary;
font-size: 20px;
font-weight: bold;
.success {
color: #22bb33;
margin-right: 5px;
}
.error {
color: #bb2124;
margin-right: 5px;
}
}
.modal-content {
word-wrap: break-word;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.qr {
margin: 5px;
}
p {
font-weight: bold;
margin: 10px;
}
.joule-btn {
background-color: #fff;
color: $primary;
border: 2px solid $primary;
transition: all 0.2s;
text-decoration: none;
&:hover {
background-color: $primary;
color: #fff;
i {
color: #fff;
}
}
}
.error-msg {
padding: 15px;
}
}
.modal-footer {
display: flex;
justify-content: space-between;
button {
padding: 4px 10px;
}
}
}
}
/* ------------ Common ------------ */
.btn {
font-size: 15px;
font-weight: bold;
color: #fff;
background-color: $primary;
border: none;
border-radius: 30px;
padding: 10px 20px;
cursor: pointer;
transition: all 0.2s;
.fa-bolt {
color: $primary;
transform: rotate(10deg);
}
&:hover {
background-color: $dark;
}
}
.loading span {
width: 8px;
height: 8px;
margin: 0 2px;
background: $primary;
border-radius: 50%;
display: inline-block;
animation-name: dots;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
&:nth-child(2) {
animation-delay: 0.4s;
}
&:nth-child(3) {
animation-delay: 0.8s;
}
}
@keyframes dots {
50% {
opacity: 0;
transform: scale(0.7) translateY(2px);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment