├── public
├── robots.txt
├── img
│ ├── icon.png
│ ├── favicon.ico
│ └── icons
│ │ ├── favicon-16x16.png
│ │ └── favicon-32x32.png
├── manifest.json
└── index.html
├── .browserslistrc
├── cypress.json
├── babel.config.js
├── nodemon.json
├── tests
├── unit
│ ├── .eslintrc.js
│ ├── service
│ │ ├── lnd.spec.js
│ │ └── bitcoind.spec.js
│ └── vue
│ │ └── Home.spec.js
└── e2e
│ ├── .eslintrc.js
│ ├── specs
│ ├── Peers.js
│ ├── Channels.js
│ ├── Tools.js
│ ├── System.js
│ └── Home.js
│ ├── support
│ ├── index.js
│ └── commands.js
│ └── plugins
│ └── index.js
├── media
├── home.png
├── peers.png
├── system.png
├── channels.png
├── invoices.png
└── payments.png
├── postcss.config.js
├── src
├── assets
│ └── logo.png
├── vue
│ ├── lib
│ │ ├── debounce.js
│ │ ├── form.js
│ │ ├── api.js
│ │ └── registerServiceWorker.js
│ ├── components
│ │ ├── FormGrid.vue
│ │ ├── FormFieldset.vue
│ │ ├── AttributeList.vue
│ │ ├── Attribute.vue
│ │ ├── Info.vue
│ │ ├── NavItem.vue
│ │ ├── Dot.vue
│ │ ├── FormButton.vue
│ │ ├── BtcPanel.vue
│ │ ├── Meter.vue
│ │ ├── LndPanel.vue
│ │ ├── Nav.vue
│ │ ├── Progress.vue
│ │ ├── Loading.vue
│ │ ├── FormSwitch.vue
│ │ ├── InvoiceForm.vue
│ │ ├── FormField.vue
│ │ ├── ChannelForm.vue
│ │ └── PaymentForm.vue
│ ├── pages
│ │ ├── Login.vue
│ │ ├── Tools.vue
│ │ ├── Peers.vue
│ │ ├── Invoices.vue
│ │ ├── Home.vue
│ │ ├── Payments.vue
│ │ ├── System.vue
│ │ └── Channels.vue
│ ├── store
│ │ ├── modules
│ │ │ ├── btc.js
│ │ │ ├── addresses.js
│ │ │ ├── system.js
│ │ │ ├── invoices.js
│ │ │ ├── lnd.js
│ │ │ ├── peers.js
│ │ │ ├── payments.js
│ │ │ └── channels.js
│ │ └── index.js
│ ├── mixins
│ │ └── peers.js
│ ├── sections
│ │ ├── NodeConnection.vue
│ │ ├── LndInfo.vue
│ │ ├── NewPeer.vue
│ │ ├── ListPayments.vue
│ │ ├── ClosedChannels.vue
│ │ ├── BtcInfo.vue
│ │ ├── LoginForm.vue
│ │ ├── ListPeers.vue
│ │ ├── ListInvoices.vue
│ │ ├── NewAddress.vue
│ │ ├── SysInfo.vue
│ │ └── ListChannels.vue
│ ├── router
│ │ └── index.js
│ └── App.vue
└── main.js
├── .editorconfig
├── server
├── auth.js
├── api
│ ├── index.js
│ ├── login
│ │ └── index.js
│ ├── sys
│ │ ├── index.js
│ │ └── service.js
│ ├── btc
│ │ ├── service.js
│ │ └── index.js
│ └── lnd
│ │ ├── index.js
│ │ └── service.js
├── services
│ ├── lnd.js
│ └── bitcoin.js
├── index.js
├── env.js
└── configure.js
├── bin
└── blitzbank.sh
├── .gitignore
├── .eslintrc.js
├── .snyk
├── jest.config.js
├── .github
├── workflows
│ └── nodejs.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── vue.config.js
├── LICENSE
├── ssl
├── dev-cert.pem
└── dev-key.pem
├── package.json
└── README.md
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | current node
4 | not dead
5 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "pluginsFile": "tests/e2e/plugins/index.js"
3 | }
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [
3 | "server",
4 | "vue.config.js"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/media/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/media/home.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/media/peers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/media/peers.png
--------------------------------------------------------------------------------
/media/system.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/media/system.png
--------------------------------------------------------------------------------
/media/channels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/media/channels.png
--------------------------------------------------------------------------------
/media/invoices.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/media/invoices.png
--------------------------------------------------------------------------------
/media/payments.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/media/payments.png
--------------------------------------------------------------------------------
/public/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/public/img/icon.png
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/public/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/public/img/favicon.ico
--------------------------------------------------------------------------------
/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dennisreimann/blitzbank-dashboard/HEAD/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/server/auth.js:
--------------------------------------------------------------------------------
1 | module.exports = (req, res, next) => {
2 | if (req.isAuthenticated()) {
3 | next()
4 | } else {
5 | res.sendStatus(401)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/vue/lib/debounce.js:
--------------------------------------------------------------------------------
1 | const timers = {}
2 |
3 | module.exports = (key, fn, delay = 250) => {
4 | clearTimeout(timers[key])
5 | timers[key] = setTimeout(fn, delay)
6 | }
7 |
--------------------------------------------------------------------------------
/bin/blitzbank.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | export NODE_ENV=production
4 |
5 | dir=server
6 |
7 | if [ -d "$dir" ]
8 | then
9 | node $dir
10 | else
11 | node node_modules/@blitzbank/dashboard/$dir
12 | fi
13 |
--------------------------------------------------------------------------------
/tests/e2e/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | 'cypress'
4 | ],
5 | env: {
6 | mocha: true,
7 | 'cypress/globals': true
8 | },
9 | rules: {
10 | strict: 'off'
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/vue/components/FormGrid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/tests/e2e/specs/Peers.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/api/introduction/api.html
2 | describe('Peers', () => {
3 | beforeEach(() => {
4 | cy.visit('/peers')
5 | })
6 |
7 | it('shows the page', () => {
8 | cy.contains('h1', 'Peers')
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/tests/unit/service/lnd.spec.js:
--------------------------------------------------------------------------------
1 | import lnd from '../../../server/api/lnd/service'
2 |
3 | describe('LND Service', () => {
4 | it('gets result', async () => {
5 | const result = await lnd('getWalletInfo')
6 | expect(result.alias).toMatch('alice')
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/tests/e2e/specs/Channels.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/api/introduction/api.html
2 | describe('Channels', () => {
3 | beforeEach(() => {
4 | cy.visit('/channels')
5 | })
6 |
7 | it('shows the page', () => {
8 | cy.contains('h1', 'Channels')
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/tests/unit/service/bitcoind.spec.js:
--------------------------------------------------------------------------------
1 | import lnd from '../../../server/api/btc/service'
2 |
3 | describe('BTC Service', () => {
4 | it('gets result', async () => {
5 | const result = await lnd('getBlockchainInfo')
6 | expect(result.chain).toMatch('regtest')
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/server/api/index.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 |
3 | const router = Router()
4 |
5 | router.use('/btc', require('./btc'))
6 | router.use('/lnd', require('./lnd'))
7 | router.use('/sys', require('./sys'))
8 | router.use('/login', require('./login'))
9 |
10 | module.exports = router
11 |
--------------------------------------------------------------------------------
/src/vue/pages/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Login
4 |
5 |
6 |
7 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/tests/e2e/specs/Tools.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/api/introduction/api.html
2 | describe('Tools', () => {
3 | beforeEach(() => {
4 | cy.visit('/tools')
5 | })
6 |
7 | it('shows the page', () => {
8 | cy.contains('h1', 'Tools')
9 | cy.contains('h3', 'New address')
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/src/vue/lib/form.js:
--------------------------------------------------------------------------------
1 | export function field (value = '', isValid = null, message = null) {
2 | return { value, isValid, message }
3 | }
4 |
5 | export function reset (...fields) {
6 | fields.forEach(field => {
7 | field.value = null
8 | field.message = null
9 | field.isValid = null
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/tests/e2e/specs/System.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/api/introduction/api.html
2 | describe('System', () => {
3 | beforeEach(() => {
4 | cy.visit('/system')
5 | })
6 |
7 | it('shows the page', () => {
8 | cy.contains('h1', 'Node Information')
9 | cy.contains('h2', 'LND')
10 | cy.contains('h2', 'Bitcoin')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/tests/e2e/specs/Home.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/api/introduction/api.html
2 | describe('Home', () => {
3 | beforeEach(() => {
4 | cy.visit('/')
5 | })
6 |
7 | it('shows a dashboard with the basic node information', () => {
8 | cy.contains('h1', 'alice')
9 | cy.contains('h2', 'LND')
10 | cy.contains('h2', 'Bitcoin')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Blitzbank Dashboard",
3 | "short_name": "Blitzbank",
4 | "icons": [
5 | {
6 | "src": "./img/icon.png",
7 | "sizes": "512x512",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "background_color": "#003366",
14 | "theme_color": "#F39E39"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | /tests/e2e/videos/
6 | /tests/e2e/screenshots/
7 |
8 | # local env files
9 | .envrc
10 | .env.local
11 | .env.*.local
12 |
13 | # Log files
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
18 | # Editor directories and files
19 | .idea
20 | .vscode
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw*
26 | /.env
27 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const isProd = process.env.NODE_ENV === 'production'
2 |
3 | module.exports = {
4 | root: true,
5 | env: {
6 | node: true
7 | },
8 | extends: [
9 | 'plugin:vue/recommended',
10 | '@vue/standard'
11 | ],
12 | rules: {
13 | 'no-console': isProd ? 'error' : 'off',
14 | 'no-debugger': isProd ? 'error' : 'off'
15 | },
16 | parserOptions: {
17 | parser: 'babel-eslint'
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/server/api/login/index.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const passport = require('passport')
3 | const authenticate = require('../../auth')
4 | const router = Router()
5 |
6 | router.get('/',
7 | authenticate,
8 | (req, res) => { res.json(req.user) })
9 |
10 | router.post('/',
11 | passport.authenticate('local', { session: true }),
12 | (req, res) => { res.json(req.user) })
13 |
14 | module.exports = router
15 |
--------------------------------------------------------------------------------
/server/services/lnd.js:
--------------------------------------------------------------------------------
1 | const lnService = require('ln-service')
2 | const {
3 | LND_RPC_HOST: host,
4 | LND_RPC_PORT: rpcPort,
5 | LND_CERT_BASE64: cert,
6 | LND_MACAROON_BASE64: macaroon
7 | } = require('../env')
8 |
9 | const socket = `${host}:${rpcPort}`
10 | const options = { socket, cert, macaroon }
11 | const { lnd } = lnService.authenticatedLndGrpc(options)
12 |
13 | module.exports = {
14 | lnd,
15 | lnService
16 | }
17 |
--------------------------------------------------------------------------------
/server/services/bitcoin.js:
--------------------------------------------------------------------------------
1 | const RpcClient = require('bitcoind-rpc')
2 |
3 | const {
4 | BITCOIND_RPC_USER: user,
5 | BITCOIND_RPC_PASSWORD: pass,
6 | BITCOIND_RPC_PROTOCOL: protocol,
7 | BITCOIND_RPC_HOST: host,
8 | BITCOIND_RPC_PORT: port
9 | } = require('../env')
10 |
11 | const options = { protocol, host, port, user, pass }
12 | const rpcClient = new RpcClient(options)
13 |
14 | module.exports = {
15 | bitcoin: rpcClient
16 | }
17 |
--------------------------------------------------------------------------------
/.snyk:
--------------------------------------------------------------------------------
1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2 | version: v1.13.4
3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date
4 | ignore:
5 | 'npm:dns-sync:20170909':
6 | - ln-service > dns-sync:
7 | reason: unfixed
8 | expires: '2019-05-10T13:09:37.326Z'
9 | 'npm:shelljs:20140723':
10 | - ln-service > dns-sync > shelljs:
11 | reason: unfixed
12 | expires: '2019-05-10T13:09:37.326Z'
13 | patch: {}
14 |
--------------------------------------------------------------------------------
/src/vue/store/modules/btc.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 |
3 | const state = {
4 | blockchainInfo: undefined
5 | }
6 |
7 | const actions = {
8 | async loadBlockchainInfo ({ commit }) {
9 | const { data } = await API.get('btc/blockchaininfo')
10 | commit('setBlockchainInfo', data)
11 | }
12 | }
13 |
14 | const mutations = {
15 | setBlockchainInfo (state, info) {
16 | state.blockchainInfo = info
17 | }
18 | }
19 |
20 | export default {
21 | namespaced: true,
22 | state,
23 | actions,
24 | mutations
25 | }
26 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | 'js',
4 | 'jsx',
5 | 'json',
6 | 'vue'
7 | ],
8 | transform: {
9 | '^.+\\.vue$': 'vue-jest',
10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
11 | '^.+\\.jsx?$': 'babel-jest'
12 | },
13 | snapshotSerializers: [
14 | 'jest-serializer-vue'
15 | ],
16 | testMatch: [
17 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
18 | ],
19 | testURL: 'http://localhost/'
20 | }
21 |
--------------------------------------------------------------------------------
/src/vue/store/modules/addresses.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 |
3 | const state = {
4 | newAddress: undefined
5 | }
6 |
7 | const actions = {
8 | async createAddress ({ commit }, payload) {
9 | const { data: { address } } = await API.post('lnd/addresses', payload)
10 | commit('setNewAddress', address)
11 | }
12 | }
13 |
14 | const mutations = {
15 | setNewAddress (state, address) {
16 | state.newAddress = address
17 | }
18 | }
19 |
20 | export default {
21 | namespaced: true,
22 | state,
23 | actions,
24 | mutations
25 | }
26 |
--------------------------------------------------------------------------------
/src/vue/components/FormFieldset.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
5 |
6 |
7 |
8 |
18 |
19 |
31 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | node-version: [10.x, 12.x]
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v1
15 | - name: Use Node ${{ matrix.node-version }}
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | - name: Install
20 | run: npm ci
21 | env:
22 | CI: true
23 | # - name: Build
24 | # run: npm run build
25 |
--------------------------------------------------------------------------------
/src/vue/mixins/peers.js:
--------------------------------------------------------------------------------
1 | export default {
2 | methods: {
3 | peerNameForPublicKey (publicKey) {
4 | const peer = this.peerForPublicKey(publicKey)
5 | return (peer && peer.alias) || publicKey
6 | },
7 |
8 | peerColorForPublicKey (publicKey) {
9 | const peer = this.peerForPublicKey(publicKey)
10 | return (peer && peer.color) || '#000000'
11 | },
12 |
13 | peerForPublicKey (publicKey) {
14 | const { peers } = this.$store.state.peers
15 | return peers && peers.find(peer => peer.publicKey === publicKey)
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Blitzbank Dashboard
10 |
11 |
12 | Blitzbank Dashboard required JavaScript to be enabled.
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/unit/vue/Home.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount, createLocalVue } from '@vue/test-utils'
2 | import Vuex from 'vuex'
3 | import Home from '../../../src/vue/pages/Home'
4 | import store from '../../../src/vue/store'
5 |
6 | const localVue = createLocalVue()
7 |
8 | localVue.use(Vuex)
9 |
10 | describe('Home.vue', () => {
11 | beforeEach(() => {
12 | store.commit('lnd/setInfo', {
13 | alias: 'alice'
14 | })
15 | })
16 |
17 | it('renders props.msg when passed', () => {
18 | const wrapper = shallowMount(Home, { store, localVue })
19 | const h1 = wrapper.find('h1')
20 | expect(h1.text()).toBe('alice')
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/vue/lib/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const headers = {
4 | accept: 'application/json',
5 | 'content-type': 'application/json'
6 | }
7 | const config = {
8 | headers,
9 | withCredentials: true,
10 | validateStatus (status) {
11 | return status < 500
12 | }
13 | }
14 |
15 | const post = (path, payload = {}) => axios.post(`/api/${path}`, payload, config)
16 | const put = (path, payload = {}) => axios.put(`/api/${path}`, payload, config)
17 | const del = (path, data = {}) => axios.delete(`/api/${path}`, Object.assign({}, config, { data }))
18 | const get = path => axios.get(`/api/${path}`, config)
19 |
20 | export default {
21 | get,
22 | post,
23 | put,
24 | del
25 | }
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/server/api/sys/index.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const router = Router()
3 | const system = require('./service')
4 | const authenticate = require('../../auth')
5 |
6 | router.get('/', authenticate, async (req, res) => {
7 | try {
8 | const [osInfo, info, memory, disk, network] = await Promise.all([
9 | system.retrieveOsInfo(),
10 | system.retrieveInfo(),
11 | system.retrieveMemory(),
12 | system.retrieveDisk(),
13 | system.retrieveNetwork()
14 | ])
15 | res.json({ os: osInfo, info, memory, disk, network })
16 | } catch (err) {
17 | const { message } = err
18 | res.status(500).send(message)
19 | }
20 | })
21 |
22 | module.exports = router
23 |
--------------------------------------------------------------------------------
/src/vue/store/modules/system.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 |
3 | const state = {
4 | os: undefined,
5 | info: undefined,
6 | memory: undefined,
7 | disk: undefined,
8 | network: undefined
9 | }
10 |
11 | const actions = {
12 | async loadSystemInfo ({ commit }) {
13 | const { data } = await API.get('sys')
14 | commit('setSystemInfo', data)
15 | }
16 | }
17 |
18 | const mutations = {
19 | setSystemInfo (state, info) {
20 | state.os = info.os
21 | state.info = info.info
22 | state.disk = info.disk
23 | state.memory = info.memory
24 | state.network = info.network
25 | }
26 | }
27 |
28 | export default {
29 | namespaced: true,
30 | state,
31 | actions,
32 | mutations
33 | }
34 |
--------------------------------------------------------------------------------
/src/vue/sections/NodeConnection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Connection
4 |
5 |
6 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
36 |
--------------------------------------------------------------------------------
/src/vue/components/AttributeList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
36 |
--------------------------------------------------------------------------------
/server/api/btc/service.js:
--------------------------------------------------------------------------------
1 | const camelizeKeys = require('camelize-keys')
2 | const { bitcoin } = require('../../services/bitcoin')
3 |
4 | const decorate = camelizeKeys
5 |
6 | module.exports = (fnName, ...args) =>
7 | new Promise((resolve, reject) => {
8 | try {
9 | const fn = bitcoin[fnName]
10 | if (typeof fn === 'function') {
11 | const handle = (err, { result }) => {
12 | err ? reject(err) : resolve(decorate(result))
13 | }
14 | args[0] === undefined
15 | ? bitcoin[fnName](handle)
16 | : bitcoin[fnName](...args, handle)
17 | } else {
18 | reject(new Error(`${fnName} is not a Bitcoin service function.`))
19 | }
20 | } catch (err) {
21 | reject(err)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/tests/e2e/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/src/vue/pages/Tools.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tools
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
39 |
--------------------------------------------------------------------------------
/src/vue/store/modules/invoices.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 | import debounce from '../../lib/debounce'
3 |
4 | const state = {
5 | invoices: undefined
6 | }
7 |
8 | const actions = {
9 | async loadInvoices ({ commit }) {
10 | const { data } = await API.get('lnd/invoices')
11 | commit('setInvoices', data)
12 | },
13 |
14 | async createInvoice (_, payload) {
15 | await API.post('lnd/invoices', payload)
16 | refreshInvoices(this)
17 | }
18 | }
19 |
20 | const mutations = {
21 | setInvoices (state, { invoices = [] }) {
22 | state.invoices = invoices
23 | }
24 | }
25 |
26 | export const refreshInvoices = store => debounce('REFRESH_INVOICES', () => {
27 | store.dispatch('invoices/loadInvoices')
28 | })
29 |
30 | export default {
31 | namespaced: true,
32 | state,
33 | actions,
34 | mutations
35 | }
36 |
--------------------------------------------------------------------------------
/src/vue/store/modules/lnd.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 | import debounce from '../../lib/debounce'
3 |
4 | const state = {
5 | info: undefined,
6 | balance: undefined
7 | }
8 |
9 | const actions = {
10 | async loadInfo ({ commit }) {
11 | const { data: info } = await API.get('lnd/info')
12 | commit('setInfo', info)
13 | },
14 |
15 | async loadBalance ({ commit }) {
16 | const { data: balance } = await API.get('lnd/balance')
17 | commit('setBalance', balance)
18 | }
19 | }
20 |
21 | const mutations = {
22 | setInfo (state, info) {
23 | state.info = info
24 | },
25 |
26 | setBalance (state, balance) {
27 | state.balance = balance
28 | }
29 | }
30 |
31 | export const refreshBalance = store => debounce('REFRESH_BALANCE', () => {
32 | store.dispatch('lnd/loadBalance')
33 | })
34 |
35 | export default {
36 | namespaced: true,
37 | state,
38 | actions,
39 | mutations
40 | }
41 |
--------------------------------------------------------------------------------
/src/vue/store/modules/peers.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 | import debounce from '../../lib/debounce'
3 |
4 | const state = {
5 | peers: undefined
6 | }
7 |
8 | const actions = {
9 | async loadPeers ({ commit }) {
10 | const { data } = await API.get('lnd/peers')
11 | commit('setPeers', data)
12 | },
13 |
14 | async connectPeer (_, { addr }) {
15 | await API.post('lnd/peers', { addr })
16 | refreshPeers(this)
17 | },
18 |
19 | async disconnectPeer (_, { pubkey }) {
20 | await API.del(`lnd/peers/${pubkey}`)
21 | refreshPeers(this)
22 | }
23 | }
24 |
25 | const mutations = {
26 | setPeers (state, peers = []) {
27 | state.peers = peers
28 | }
29 | }
30 |
31 | export const refreshPeers = store => debounce('REFRESH_PEERS', () => {
32 | store.dispatch('peers/loadPeers')
33 | })
34 |
35 | export default {
36 | namespaced: true,
37 | state,
38 | actions,
39 | mutations
40 | }
41 |
--------------------------------------------------------------------------------
/tests/e2e/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/tests/e2e/plugins/index.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/guides/guides/plugins-guide.html
2 |
3 | // if you need a custom webpack configuration you can uncomment the following import
4 | // and then use the `file:preprocessor` event
5 | // as explained in the cypress docs
6 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
7 |
8 | // /* eslint-disable import/no-extraneous-dependencies, global-require */
9 | // const webpack = require('@cypress/webpack-preprocessor')
10 |
11 | module.exports = (on, config) => {
12 | // on('file:preprocessor', webpack({
13 | // webpackOptions: require('@vue/cli-service/webpack.config'),
14 | // watchOptions: {}
15 | // }))
16 |
17 | return Object.assign({}, config, {
18 | fixturesFolder: 'tests/e2e/fixtures',
19 | integrationFolder: 'tests/e2e/specs',
20 | screenshotsFolder: 'tests/e2e/screenshots',
21 | videosFolder: 'tests/e2e/videos',
22 | supportFile: 'tests/e2e/support/index.js'
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/vue/pages/Peers.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
48 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const { readFileSync } = require('fs')
2 | const configure = require('./server/configure')
3 | const { SERVER_HOST, SERVER_PORT, SSL_CERT_PATH, SSL_KEY_PATH } = require('./server/env')
4 |
5 | module.exports = {
6 | devServer: {
7 | host: SERVER_HOST,
8 | port: SERVER_PORT,
9 |
10 | // FIXME: Disable progress output until this issue is resolved:
11 | // https://github.com/vuejs/vue-cli/issues/4557#issuecomment-545965828
12 | progress: false,
13 |
14 | https: {
15 | key: readFileSync(SSL_KEY_PATH),
16 | cert: readFileSync(SSL_CERT_PATH)
17 | },
18 |
19 | // https://webpack.js.org/configuration/dev-server/#devservertransportmode
20 | transportMode: 'ws',
21 |
22 | // https://webpack.js.org/configuration/dev-server/#devserveronlistening
23 | onListening (devServer) {
24 | const { app, listeningApp: server, socketServer: { wsServer } } = devServer
25 | configure(app, server, wsServer)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/src/vue/lib/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from 'register-service-worker'
4 |
5 | if (process.env.NODE_ENV === 'production') {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready () {
8 | console.log(
9 | 'App is being served from cache by a service worker.\n' +
10 | 'For more details, visit https://goo.gl/AFskqB'
11 | )
12 | },
13 | registered () {
14 | console.log('Service worker has been registered.')
15 | },
16 | cached () {
17 | console.log('Content has been cached for offline use.')
18 | },
19 | updatefound () {
20 | console.log('New content is downloading.')
21 | },
22 | updated () {
23 | console.log('New content is available; please refresh.')
24 | },
25 | offline () {
26 | console.log('No internet connection found. App is running in offline mode.')
27 | },
28 | error (error) {
29 | console.error('Error during service worker registration:', error)
30 | }
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Dennis Reimann (https://dennisreimann.de)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/vue/pages/Invoices.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
53 |
--------------------------------------------------------------------------------
/src/vue/components/Attribute.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ label }}
4 | {{ value }}
5 |
6 |
7 |
8 |
23 |
24 |
50 |
--------------------------------------------------------------------------------
/src/vue/components/Info.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ message }}
7 |
8 |
9 |
10 |
32 |
33 |
55 |
--------------------------------------------------------------------------------
/src/vue/pages/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ info.alias }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
54 |
--------------------------------------------------------------------------------
/src/vue/pages/Payments.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }}
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
56 |
--------------------------------------------------------------------------------
/src/vue/store/modules/payments.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 | import debounce from '../../lib/debounce'
3 |
4 | const state = {
5 | payments: undefined,
6 | paymentRequests: {}
7 | }
8 |
9 | const actions = {
10 | async loadPayments ({ commit }) {
11 | const { data } = await API.get('lnd/payments')
12 | commit('setPayments', data)
13 | },
14 |
15 | async decodePaymentRequest ({ commit }, { request }) {
16 | const { data } = await API.get(`lnd/paymentrequests/${request}`)
17 | data.request = request
18 | commit('setPaymentRequest', data)
19 | },
20 |
21 | async payPaymentRequest (_, payload) {
22 | await API.post('lnd/payments', payload)
23 | refreshPayments(this)
24 | }
25 | }
26 |
27 | const mutations = {
28 | setPayments (state, { payments = [] }) {
29 | state.payments = payments
30 | },
31 |
32 | setPaymentRequest (state, { request, ...paymentRequest }) {
33 | state.paymentRequests = {
34 | ...state.paymentRequests,
35 | [request]: paymentRequest
36 | }
37 | }
38 | }
39 |
40 | export const refreshPayments = store => debounce('REFRESH_PAYMENTS', () => {
41 | store.dispatch('payments/loadPayments')
42 | })
43 |
44 | export default {
45 | namespaced: true,
46 | state,
47 | actions,
48 | mutations
49 | }
50 |
--------------------------------------------------------------------------------
/src/vue/pages/System.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Node Information
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
54 |
--------------------------------------------------------------------------------
/server/api/btc/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | const { Router } = require('express')
3 | const bitcoind = require('./service')
4 | const { NODE_ENV } = require('../../env')
5 | const authenticate = require('../../auth')
6 |
7 | const router = Router()
8 |
9 | // Reference: https://github.com/bitpay/bitcoind-rpc/blob/master/lib/index.js#L160
10 | const ROUTES = [
11 | // General
12 | ['get', '/blockchaininfo', 'getBlockchainInfo'],
13 | ['get', '/blockcount', 'getBlockCount'],
14 | ['get', '/block/:header', 'getBlock', req => req.params.header]
15 | ]
16 |
17 | ROUTES.map(([method, route, rpc, getPayload]) => {
18 | router[method](route, authenticate, async (req, res) => {
19 | const payload = getPayload && getPayload(req)
20 | try {
21 | if (NODE_ENV === 'development' && payload) console.debug(payload)
22 | let result
23 | if (typeof rpc === 'object') {
24 | const calls = await Promise.all(rpc.map(c => bitcoind(c, payload)))
25 | result = calls.reduce((res, callRes) => Object.assign(res, callRes), {})
26 | } else {
27 | result = await bitcoind(rpc, payload)
28 | }
29 | if (NODE_ENV === 'development') console.debug(result)
30 | res.json(result)
31 | } catch (err) {
32 | res.status(500).send(err.message)
33 | }
34 | })
35 | })
36 |
37 | module.exports = router
38 |
--------------------------------------------------------------------------------
/src/vue/components/NavItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | {{ icon }}
9 |
10 |
11 | {{ title }}
12 |
13 |
14 |
15 |
16 |
38 |
39 |
73 |
--------------------------------------------------------------------------------
/src/vue/components/Dot.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
31 |
32 |
75 |
--------------------------------------------------------------------------------
/src/vue/store/modules/channels.js:
--------------------------------------------------------------------------------
1 | import API from '../../lib/api'
2 | import debounce from '../../lib/debounce'
3 |
4 | const state = {
5 | activeChannels: undefined,
6 | pendingChannels: undefined,
7 | closedChannels: undefined
8 | }
9 |
10 | const actions = {
11 | async loadChannels ({ commit }) {
12 | const { data } = await API.get('lnd/channels')
13 | commit('setChannels', data)
14 | },
15 |
16 | async loadClosedChannels ({ commit }) {
17 | const { data } = await API.get('lnd/channels/closed')
18 | commit('setClosedChannels', data)
19 | },
20 |
21 | async openChannel (_, payload) {
22 | await API.post('lnd/channels', payload)
23 | refreshChannels(this)
24 | },
25 |
26 | async closeChannel (_, { id, ...payload }) {
27 | await API.del(`lnd/channels/${id}`, payload)
28 | refreshChannels(this)
29 | }
30 | }
31 |
32 | const mutations = {
33 | setChannels (state, { channels = [], pendingChannels = [] }) {
34 | state.activeChannels = channels
35 | state.pendingChannels = pendingChannels
36 | },
37 |
38 | setClosedChannels (state, { channels = [] }) {
39 | state.closedChannels = channels
40 | }
41 | }
42 |
43 | export const refreshChannels = store => debounce('REFRESH_CHANNELS', () => {
44 | store.dispatch('channels/loadChannels')
45 | store.dispatch('channels/loadClosedChannels')
46 | })
47 |
48 | export default {
49 | namespaced: true,
50 | state,
51 | actions,
52 | mutations
53 | }
54 |
--------------------------------------------------------------------------------
/src/vue/components/FormButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ title }}
7 |
8 |
9 |
10 |
25 |
26 |
71 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | this file is used to start the server in production mode:
3 | uses the shared server config for the API, adds production
4 | config, mounts the UI and launches an express app.
5 | */
6 | const { readFileSync } = require('fs')
7 | const { join, resolve } = require('path')
8 | const { createServer } = require('https')
9 |
10 | const WebSocket = require('ws')
11 | const express = require('express')
12 | const history = require('connect-history-api-fallback')
13 | const configure = require('./configure')
14 | const { SERVER_PORT, SSL_CERT_PATH, SSL_KEY_PATH } = require('./env')
15 |
16 | const app = express()
17 | const cert = readFileSync(SSL_CERT_PATH)
18 | const key = readFileSync(SSL_KEY_PATH)
19 | const server = createServer({ cert, key }, app)
20 | const socketServer = new WebSocket.Server({ server })
21 |
22 | // Session, API and Websockets
23 | configure(app, server, socketServer)
24 |
25 | // Security
26 | app.disable('x-powered-by')
27 |
28 | // Single Page App
29 | app.use(history())
30 |
31 | // Dashboard UI
32 | // https://cli.vuejs.org/guide/deployment.html
33 | const publicPath = resolve(__dirname, '../dist')
34 | const staticConf = { maxAge: '1y', etag: false };
35 |
36 | ['img', 'css', 'js'].forEach(dir => {
37 | app.use(express.static(join(publicPath, dir), staticConf))
38 | })
39 | app.use(express.static(publicPath))
40 |
41 | // Go 🚀
42 | server.listen(SERVER_PORT, () => console.debug(`⚡️ Server running on port ${SERVER_PORT}!`))
43 |
--------------------------------------------------------------------------------
/server/env.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | const assert = require('assert')
4 |
5 | const {
6 | // Bitcoin
7 | BITCOIND_RPC_USER,
8 | BITCOIND_RPC_PASSWORD,
9 | BITCOIND_RPC_PROTOCOL = 'http',
10 | BITCOIND_RPC_HOST = 'localhost',
11 | BITCOIND_RPC_PORT = 8332,
12 | // LND
13 | LND_MACAROON_BASE64,
14 | LND_CERT_BASE64,
15 | LND_RPC_HOST = 'localhost',
16 | LND_RPC_PORT = 10009,
17 | // App
18 | AUTH_USERNAME,
19 | AUTH_PASSWORD,
20 | SSL_KEY_PATH,
21 | SSL_CERT_PATH,
22 | PUBLIC_HOST,
23 | SERVER_HOST,
24 | SERVER_PORT = 4000,
25 | NODE_ENV = 'production',
26 | SESSION_SECRET = Math.random().toString(36).replace(/[^a-z]+/g, '')
27 | } = process.env
28 |
29 | assert(BITCOIND_RPC_USER && BITCOIND_RPC_PASSWORD, 'Provide the BITCOIND_RPC_USER and BITCOIND_RPC_PASSWORD environment variables.')
30 | assert(LND_CERT_BASE64 && LND_MACAROON_BASE64, 'Provide the LND_CERT_BASE64 and LND_MACAROON_BASE64 environment variables.')
31 | assert(AUTH_USERNAME && AUTH_PASSWORD, 'Provide the AUTH_USERNAME and AUTH_PASSWORD environment variables.')
32 | assert(SSL_KEY_PATH && SSL_CERT_PATH, 'Provide the SSL_KEY_PATH and SSL_CERT_PATH environment variables.')
33 |
34 | module.exports = {
35 | BITCOIND_RPC_USER,
36 | BITCOIND_RPC_PASSWORD,
37 | BITCOIND_RPC_PROTOCOL,
38 | BITCOIND_RPC_HOST,
39 | BITCOIND_RPC_PORT,
40 | LND_RPC_HOST,
41 | LND_RPC_PORT,
42 | LND_CERT_BASE64,
43 | LND_MACAROON_BASE64,
44 | AUTH_USERNAME,
45 | AUTH_PASSWORD,
46 | SSL_KEY_PATH,
47 | SSL_CERT_PATH,
48 | SERVER_HOST,
49 | SERVER_PORT,
50 | PUBLIC_HOST,
51 | NODE_ENV,
52 | SESSION_SECRET
53 | }
54 |
--------------------------------------------------------------------------------
/ssl/dev-cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEcTCCAtmgAwIBAgIQSQuaBtLtoc/gBiBDmDrNazANBgkqhkiG9w0BAQsFADB9
3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKTAnBgNVBAsMIGRlbm5p
4 | c3JlaW1hbm5ATWV0dWxza3kuZnJpdHouYm94MTAwLgYDVQQDDCdta2NlcnQgZGVu
5 | bmlzcmVpbWFubkBNZXR1bHNreS5mcml0ei5ib3gwHhcNMTkwNTA5MTMyMTMzWhcN
6 | MjkwNTA5MTMyMTMzWjBUMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2Vy
7 | dGlmaWNhdGUxKTAnBgNVBAsMIGRlbm5pc3JlaW1hbm5ATWV0dWxza3kuZnJpdHou
8 | Ym94MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1ATiG8hHKS+e5qbL
9 | ImxPhPqMgFI5wKTwo7oDwnLOaTxgZ3jmG7SPQY+dVUPiS5WA59rUQxoGjxb78PIX
10 | EA79Zq4W4FosQkLHgBh+ScyPbLdCteUvWfBvHcgAG2EgGu7jyXgyfQfHzSRr24EE
11 | G8MkEddxxn+hVD6J4y1VWsYGgqD8JtgIgDgGveAxb5EOceE52UIMczdYk9Yut1ja
12 | e8XBFPmbvxQLVzBGuc0XMsxdhaQhnDdK4A8rAB7jZ5IpU4YyR4yxg9liLtVd3PSU
13 | 1FKdox/OyDSBfdZ5R95D8qFMV7fodTU0SF6JN+rqm0Puz4MWAh1EotIIQ5p7vEtu
14 | kxutWQIDAQABo4GVMIGSMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEF
15 | BQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFPS2e2W0mij3IZRF0/DK+Vrr
16 | sOBgMDwGA1UdEQQ1MDOCDmZ1bGxub2RlLmxvY2Fsgglsb2NhbGhvc3SHBH8AAAGH
17 | EAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAASSUV8SXCjPNXKK
18 | 4vOFxohZ9acR/cXu0gJNEVG5B+d3EDgjuvtPKNyk3MH4kmv6GTViF9P9DMLtiAqU
19 | htIS6n/zIMKD6TzhePkUcU4/SdesB55L9FrFCKd8Mh3ont/GZQDJny0IVxcLikSM
20 | zlsjGZfOEW9luSB/LL5AE/996Xdbr1S2w5VWPWJwMUrdFznIUTIc4M/ff98VbT7G
21 | wiBG9lHndmpnpAs1+7293JuGDDkWSv23WQUnVjo4rZx1O/o9GmniiFH22mE25BKF
22 | usO6nlbocU8R0mcg2GKJAeTfT1q+K0DHqto5SLjx9C82dWkCc7Oa9YPGFkVEr61y
23 | lSg6m7tAOlQ5VSGig7AaTUy3tWnm4uWnd7ybrkPCjSi3fyiVUxPYvuF/jPJ7fes+
24 | g+FeGf+ckvBQ3FjYy7oQeym1/Xcr5BtxK480W74nbublQD+YdkvwMlk5CFNpmd0o
25 | zE7dAohYb528gt4Q1pz8POQY9WFa/kEJmvBwLM66WevRyo9lOg==
26 | -----END CERTIFICATE-----
27 |
--------------------------------------------------------------------------------
/src/vue/sections/LndInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | LND
4 |
5 |
6 |
7 |
8 |
12 |
13 | Fully synced
14 |
15 |
19 |
20 | Sync in progress
21 |
22 |
23 |
27 |
31 |
32 |
33 |
34 | {{ info.publicKey }}
35 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
66 |
--------------------------------------------------------------------------------
/src/vue/pages/Channels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title }} {{ pendingChannelsCount }} pending
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
65 |
--------------------------------------------------------------------------------
/src/vue/components/BtcPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bitcoin {{ blockchainInfo.chain }}net
4 |
5 |
6 |
9 |
13 |
14 | Fully synced
15 |
16 |
20 |
21 | {{ blockchainInfo.initialblockdownload ? "Initial sync" : "Sync" }}
22 | in progress
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
64 |
65 |
71 |
--------------------------------------------------------------------------------
/ssl/dev-key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUBOIbyEcpL57m
3 | pssibE+E+oyAUjnApPCjugPCcs5pPGBneOYbtI9Bj51VQ+JLlYDn2tRDGgaPFvvw
4 | 8hcQDv1mrhbgWixCQseAGH5JzI9st0K15S9Z8G8dyAAbYSAa7uPJeDJ9B8fNJGvb
5 | gQQbwyQR13HGf6FUPonjLVVaxgaCoPwm2AiAOAa94DFvkQ5x4TnZQgxzN1iT1i63
6 | WNp7xcEU+Zu/FAtXMEa5zRcyzF2FpCGcN0rgDysAHuNnkilThjJHjLGD2WIu1V3c
7 | 9JTUUp2jH87INIF91nlH3kPyoUxXt+h1NTRIXok36uqbQ+7PgxYCHUSi0ghDmnu8
8 | S26TG61ZAgMBAAECggEACefogI/M81msPO6SExuoY3gpVF2DIUMTkzK/tjgS+Mu/
9 | XVZCugynnNKO8UroqctkaHDK9g+jOtBCGTHWbgOlR0TfAMB1zOq903hRfjU2hkfR
10 | sBnzKmqXZnbPunfBPkDnF6SsBzdz2FvZRjoy7PEjIEpGoJWZ/gul+Z1GnaUe7L9u
11 | 7r4ec+B1AMSC0GHHx68bbv7pEZILQLYXqLR/Vz4m/bOWgDAmbKek8BPlBwsElTkg
12 | uG/n6+bp9pnHhieYqMnzZY+tVvZdBbDX06fR9Ilyn4XIewuQIOSflLbF7XusrZRY
13 | +wy+pt92e4b/w4EuOGIeqZtex+0r5PA/qk184fX2+QKBgQDVxseRJ3TScvlPRYBf
14 | jTMuXKKy6W50X4ncRPTO8fNyt7Jbg5UGiU76jTkM7WOwYPPiRR581pmj+UvAjSHq
15 | YHlHWNrlPxSoFwGQabD/5/nQFmdRnqg5nI5kxa8d2IaiQc1Ohec+04hwCTPQOcZ+
16 | 6+8GLpAR/gtE6iOxgY0tovcCnwKBgQD95T5ZapP6KwFWd++P1jm9ZAOA9bXCsu58
17 | WaFoXLeIZheUMOhPevAL0jb6eZMt4Cf5pk1yQRIMVZpGI+rpI1LL3zipb26dVAjZ
18 | R9GTjkHMTE9661eC3gqsjweiJq2hHlvBnToV7WLG1PdfD/qzeoqcTnLpzGjONYBw
19 | DoDJw0KFBwKBgHOBLb7+BJ7YxF/Se6QlFKxOHRJyEd4K6N/82hEepZ0sJ9BObizT
20 | 77psp5CWizB3kg6Frg1hni38urNVDigm7CBioBMRXEXd+Fhg4uPCITYPhM+S2+4U
21 | 6tMEBLQpk8UDrLxqmSFXBfcS4c32CYv2SnWanvk5vDkGETcNeoxX6wKdAoGBAL9q
22 | EnyVNmT4ATUwjOLTLorTmTSjiln0TopfhKnKpO/nkEVALhSl6c3vuVVTTRvcECdO
23 | Mrs9xZ6Y9wuETrlf0S805mIPScTBMz6kv6NQL9kXeyB/x2U5g7Ce0LF2GZcL7T9m
24 | CwG3C649pxX7VRX1AAVhu476ddVpTqCsvnnehQsHAoGANPAtKbJTFUPMr4nTlLV0
25 | c2ogfhfgpL4uxNgNpb8uDkkCl2mkim2r2JUj/UJHvK5dn7p7XuZ/T9egma+M3pwW
26 | 0JIDl6ZOhIOGiqrN6fMeP1XYcL+Tlap46JqkvAJH2tZcvEyZOKcSj5GCplC2L2Ra
27 | f2/nFWewCOfHb7vfHFK/zT8=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/src/vue/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import store from '../store'
4 |
5 | Vue.use(Router)
6 |
7 | const LOGIN_PATH = '/login'
8 |
9 | const router = new Router({
10 | mode: 'history',
11 | base: process.env.BASE_URL,
12 | routes: [
13 | {
14 | path: '/',
15 | name: 'home',
16 | component: () => import(/* webpackChunkName: "home" */ '../pages/Home')
17 | },
18 | {
19 | path: LOGIN_PATH,
20 | name: 'login',
21 | component: () => import(/* webpackChunkName: "login" */ '../pages/Login')
22 | },
23 | {
24 | path: '/peers',
25 | name: 'peers',
26 | component: () => import(/* webpackChunkName: "peers" */ '../pages/Peers')
27 | },
28 | {
29 | path: '/channels',
30 | name: 'channels',
31 | component: () => import(/* webpackChunkName: "channels" */ '../pages/Channels')
32 | },
33 | {
34 | path: '/invoices',
35 | name: 'invoices',
36 | component: () => import(/* webpackChunkName: "invoices" */ '../pages/Invoices')
37 | },
38 | {
39 | path: '/payments',
40 | name: 'payments',
41 | component: () => import(/* webpackChunkName: "payments" */ '../pages/Payments')
42 | },
43 | {
44 | path: '/tools',
45 | name: 'tools',
46 | component: () => import(/* webpackChunkName: "tools" */ '../pages/Tools')
47 | },
48 | {
49 | path: '/system',
50 | name: 'system',
51 | component: () => import(/* webpackChunkName: "system" */ '../pages/System')
52 | }
53 | ]
54 | })
55 |
56 | router.beforeEach((to, origin, next) => {
57 | if (to.path !== LOGIN_PATH && !store.getters.isAuthenticated) {
58 | next({ path: LOGIN_PATH })
59 | } else {
60 | next()
61 | }
62 | })
63 |
64 | export default router
65 |
--------------------------------------------------------------------------------
/src/vue/components/Meter.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ value }}
7 |
12 | {{ percent }}%
13 |
14 | {{ title }}
15 |
16 |
17 |
18 |
43 |
44 |
89 |
--------------------------------------------------------------------------------
/src/vue/components/LndPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | LND
4 |
5 |
6 |
10 |
11 | Fully synced
12 |
13 |
17 |
18 | Sync in progress
19 |
20 |
21 |
22 | {{ info.peersCount }} peers
23 |
24 | {{ info.activeChannelsCount }} active channels
25 |
26 | {{ info.pendingChannelsCount }} pending channels
27 |
28 |
32 | {{ balance.chainBalanceSats }}
33 |
34 | = {{ balance.chainBalanceBtc }}
35 |
36 |
37 |
41 | {{ balance.pendingChainBalanceSats }}
42 |
43 | = {{ balance.pendingChainBalanceBtc }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
68 |
--------------------------------------------------------------------------------
/src/vue/components/Nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
17 |
23 |
29 |
35 |
42 |
49 |
50 |
51 |
52 |
53 |
67 |
68 |
90 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import Vue from 'vue'
3 | import VueSocket from 'vue-native-websocket'
4 | import Clipboard from 'clipboard'
5 | import router from './vue/router'
6 | import store from './vue/store'
7 | import App from './vue/App'
8 | import './vue/lib/registerServiceWorker'
9 |
10 | Vue.config.productionTip = false
11 |
12 | const mount = '#app'
13 | const socketPath = process.env.NODE_ENV === 'development' ? '/sockjs-node' : '/'
14 |
15 | // Automatic Global Registration of Base Components
16 | // https://vuejs.org/v2/guide/components-registration.html#Automatic-Global-Registration-of-Base-Components
17 | const requireComponent = require.context('./vue/components', false, /Form[A-Z]\w+\.(vue|js)$/)
18 |
19 | requireComponent.keys().forEach(fileName => {
20 | const componentConfig = requireComponent(fileName)
21 | const componentName = fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
22 |
23 | Vue.component(componentName, componentConfig.default || componentConfig)
24 | });
25 |
26 | // Load info via API, if that works init the app
27 | (async () => {
28 | try {
29 | await store.dispatch('checkSession')
30 |
31 | // FIXME: Remove the scheme once this gets merged and released:
32 | // https://github.com/nathantsoi/vue-native-websocket/pull/90
33 | const wsSocketUrl = `wss://${window.location.host}${socketPath}`
34 |
35 | Vue.use(VueSocket, wsSocketUrl, { store: store, format: 'json' })
36 |
37 | new Vue({
38 | router,
39 | store,
40 | render: h => h(App)
41 | }).$mount(mount)
42 |
43 | new Clipboard('[data-clipboard-text]')
44 | .on('error', event => {
45 | console.error('Clipboard error:', event)
46 | })
47 | } catch (error) {
48 | console.error(error)
49 | document.querySelector(mount).innerHTML = 'Could not initialize app: Fetching data failed.'
50 | }
51 | })()
52 |
--------------------------------------------------------------------------------
/src/vue/components/Progress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | {{ percent }}%
12 |
13 |
14 |
15 |
35 |
36 |
84 |
--------------------------------------------------------------------------------
/src/vue/components/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 | Loading data.
10 |
11 |
12 |
13 |
14 |
83 |
--------------------------------------------------------------------------------
/src/vue/sections/NewPeer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Add new peer
4 |
31 |
32 |
33 |
34 |
70 |
71 |
80 |
--------------------------------------------------------------------------------
/src/vue/components/FormSwitch.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
11 | {{ icon }}
12 |
13 |
14 |
15 |
69 |
70 |
97 |
--------------------------------------------------------------------------------
/src/vue/sections/ListPayments.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
13 | {{ payment.id }} {{ payment.isPrivate ? "🔒" : "" }}
14 |
15 |
16 |
20 | {{ ' ' }}
21 | {{ peerNameForPublicKey(payment.destination) }}
22 |
23 |
27 |
28 | {{ payment.fee }}
29 | ({{ payment.hops.length }} hops)
30 |
31 |
35 |
36 |
37 |
38 |
39 | No payments, yet.
40 |
41 |
42 |
43 |
44 |
45 |
46 |
85 |
--------------------------------------------------------------------------------
/server/configure.js:
--------------------------------------------------------------------------------
1 | /*
2 | shared server configuration for dev and prod env.
3 | dev: Webpack Dev Server -> vue.config.js
4 | prod: Express -> ./index.js
5 | */
6 | const logger = require('morgan')
7 | const passport = require('passport')
8 | const session = require('express-session')
9 | const { Strategy: LocalStrategy } = require('passport-local')
10 | const { json } = require('express')
11 | const { subscribeToGraph, subscribeToInvoices, subscribeToTransactions } = require('ln-service/push')
12 | const { lnd } = require('./services/lnd')
13 | const { NODE_ENV, AUTH_USERNAME, AUTH_PASSWORD, SESSION_SECRET } = require('./env')
14 |
15 | const { log } = console
16 | const isDevelopment = NODE_ENV === 'development'
17 |
18 | module.exports = (app, server, socketServer) => {
19 | // Logging
20 | app.use(logger(isDevelopment ? 'dev' : 'combined'))
21 |
22 | // Body parsing
23 | app.use(json())
24 |
25 | // Session and login
26 | // http://www.passportjs.org/docs/configure/
27 | // http://www.passportjs.org/packages/passport-local/
28 | passport.use(new LocalStrategy(
29 | (username, password, done) =>
30 | username === AUTH_USERNAME && password === AUTH_PASSWORD
31 | ? done(null, { username })
32 | : done(null, false)
33 | ))
34 |
35 | passport.serializeUser(({ username }, done) => {
36 | done(null, username)
37 | })
38 |
39 | passport.deserializeUser((username, done) => {
40 | done(null, { username })
41 | })
42 |
43 | app.use(session({
44 | secret: SESSION_SECRET,
45 | saveUninitialized: false,
46 | resave: false,
47 | cookie: { secure: true }
48 | }))
49 |
50 | app.use(passport.initialize())
51 | app.use(passport.session())
52 |
53 | // API
54 | app.use('/api', require('./api'))
55 |
56 | // Websocket for LN Service Push methods
57 | // https://github.com/websockets/ws/blob/master/doc/ws.md
58 | // https://github.com/alexbosworth/ln-service/tree/master/push
59 | const wss = [socketServer]
60 |
61 | // FIXME: "I believe that error is related to doing a subscription
62 | // or call to the API before LND is fully started and is a relatively
63 | // benign error where you can just retry"
64 | // https://github.com/alexbosworth/ln-service/issues/108#issuecomment-559879822
65 | subscribeToGraph({ lnd, log, wss })
66 | subscribeToInvoices({ lnd, log, wss })
67 | subscribeToTransactions({ lnd, log, wss })
68 | }
69 |
--------------------------------------------------------------------------------
/src/vue/sections/ClosedChannels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Closed channels
6 |
10 |
11 |
14 | {{ peerNameForPublicKey(channel.partnerPublicKey) }}:
15 | {{ channel.id }}
16 |
17 |
18 |
22 |
26 |
30 |
31 |
32 |
33 |
34 | No closed channels, yet.
35 |
36 |
37 |
38 |
39 |
40 |
41 |
84 |
--------------------------------------------------------------------------------
/src/vue/components/InvoiceForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
47 |
48 |
49 |
98 |
99 |
108 |
--------------------------------------------------------------------------------
/src/vue/sections/BtcInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bitcoin {{ blockchainInfo.chain }}net
4 |
5 |
6 |
7 |
10 |
14 |
15 | Fully synced
16 |
17 |
21 |
22 | {{ blockchainInfo.initialblockdownload ? "Initial sync" : "Sync" }}
23 | in progress
24 |
28 |
29 |
30 |
34 |
37 | {{ blockchainInfo.pruned ? 'active' : 'disabled' }}
38 |
39 | (lowest-height complete block: {{ blockchainInfo.pruneheight }})
40 |
41 |
42 |
43 |
44 |
45 | Softforks
46 |
47 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
98 |
--------------------------------------------------------------------------------
/src/vue/sections/LoginForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
107 |
108 |
113 |
--------------------------------------------------------------------------------
/src/vue/sections/ListPeers.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Connected peers
5 |
10 |
11 | {{ ' ' }}
12 | {{ peer.alias }}
17 | {{ peer.publicKey }}
18 |
19 |
20 |
24 |
28 |
29 |
33 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
100 |
101 |
106 |
--------------------------------------------------------------------------------
/src/vue/sections/ListInvoices.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
13 | {{ invoice.id }} {{ invoice.isPrivate ? "🔒" : "" }}
14 |
15 |
16 |
20 |
24 |
28 |
33 |
38 |
43 |
44 |
45 |
51 |
52 |
53 |
54 |
55 | No invoices, yet.
56 |
57 |
58 |
59 |
60 |
61 |
62 |
98 |
99 |
105 |
--------------------------------------------------------------------------------
/src/vue/sections/NewAddress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | New address
4 |
9 |
13 | Your shiny new address:
14 | {{ newAddress }}
15 |
16 |
56 |
57 |
58 |
59 |
115 |
116 |
125 |
--------------------------------------------------------------------------------
/server/api/lnd/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | const { Router } = require('express')
3 | const service = require('./service')
4 | const { NODE_ENV } = require('../../env')
5 | const authenticate = require('../../auth')
6 |
7 | const router = Router()
8 |
9 | // Reference: https://github.com/alexbosworth/ln-service
10 | const ROUTES = [
11 | // General
12 | ['get', '/info', 'getWalletInfo'],
13 | ['get', '/balance', ['getChainBalance', 'getPendingChainBalance']],
14 |
15 | // Peers
16 | ['get', '/peers', 'getPeers'],
17 | ['post', '/peers', 'addPeer', req => {
18 | const [pubkey = '', socket = ''] = req.body.addr.split('@')
19 | return { public_key: pubkey.trim(), socket: socket.trim() }
20 | }],
21 | ['delete', '/peers/:public_key', 'removePeer', req => req.params],
22 |
23 | // Addresses
24 | ['post', '/addresses', 'createChainAddress', req => {
25 | const format = req.body.format
26 | return { format }
27 | }],
28 |
29 | // Channels
30 | ['get', '/channels', ['getChannels', 'getPendingChannels']],
31 | ['get', '/channels/closed', ['getClosedChannels']],
32 | ['get', '/channels/:id', 'getChannel', req => req.params],
33 | ['get', '/channels/balance', 'getChannelBalance'],
34 | ['post', '/channels', 'openChannel', req => {
35 | const { pubkey, funding, pushing, isPrivate } = req.body
36 | return {
37 | partner_public_key: pubkey,
38 | local_tokens: parseInt(funding),
39 | give_tokens: parseInt(pushing),
40 | is_private: isPrivate
41 | }
42 | }],
43 | ['delete', '/channels/:id', 'closeChannel', req => {
44 | const { id } = req.params
45 | const { socket, partnerPublicKey, transactionId, transactionVout } = req.body
46 | return {
47 | id,
48 | socket,
49 | public_key: partnerPublicKey,
50 | transaction_id: transactionId,
51 | transaction_vout: transactionVout
52 | }
53 | }],
54 |
55 | // Invoices
56 | ['get', '/invoices', 'getInvoices'],
57 | ['post', '/invoices', 'createInvoice', req => {
58 | const { description, secret, amount } = req.body
59 | return {
60 | description,
61 | secret,
62 | tokens: amount
63 | }
64 | }],
65 |
66 | // Payments
67 | ['get', '/payments', 'getPayments'],
68 | ['get', '/paymentrequests/:request', 'decodePaymentRequest', req => req.params],
69 | ['post', '/payments', 'pay', req => {
70 | return { request: (req.body.request || '').replace(/\s+/g, '') }
71 | }]
72 | ]
73 |
74 | ROUTES.map(([method, route, rpc, getPayload]) => {
75 | router[method](route, authenticate, async (req, res) => {
76 | const payload = getPayload && getPayload(req)
77 | try {
78 | if (NODE_ENV === 'development' && payload) console.debug(payload)
79 | let result
80 | if (typeof rpc === 'object') {
81 | const calls = await Promise.all(rpc.map(c => service(c, payload)))
82 | result = calls.reduce((res, callRes) => Object.assign(res, callRes), {})
83 | } else {
84 | result = await service(rpc, payload)
85 | }
86 | if (NODE_ENV === 'development') console.debug(result)
87 | res.json(result)
88 | } catch (error) {
89 | res.status(error.status).send(error.details)
90 | }
91 | })
92 | })
93 |
94 | module.exports = router
95 |
--------------------------------------------------------------------------------
/src/vue/store/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import Vue from 'vue'
3 | import Vuex from 'vuex'
4 | import API from '../lib/api'
5 | import btc from './modules/btc'
6 | import lnd, { refreshBalance } from './modules/lnd'
7 | import addresses from './modules/addresses'
8 | import channels, { refreshChannels } from './modules/channels'
9 | import invoices, { refreshInvoices } from './modules/invoices'
10 | import payments, { refreshPayments } from './modules/payments'
11 | import peers, { refreshPeers } from './modules/peers'
12 | import system from './modules/system'
13 |
14 | Vue.use(Vuex)
15 |
16 | const strict = process.env.NODE_ENV !== 'production'
17 |
18 | export default new Vuex.Store({
19 | strict,
20 | state: {
21 | user: undefined,
22 | socket: {
23 | isConnected: false,
24 | reconnectError: false
25 | }
26 | },
27 | getters: {
28 | isAuthenticated: (state) => {
29 | return !!state.user
30 | }
31 | },
32 | mutations: {
33 | SOCKET_ONOPEN (state, event) {
34 | Vue.prototype.$socket = event.currentTarget
35 | state.socket.isConnected = true
36 | },
37 | SOCKET_ONCLOSE (state, event) {
38 | state.socket.isConnected = false
39 | },
40 | SOCKET_ONERROR (state, event) {
41 | console.error(state, event)
42 | },
43 | // mutations for reconnect methods
44 | SOCKET_RECONNECT (state, count) {
45 | console.debug(state, count)
46 | },
47 | SOCKET_RECONNECT_ERROR (state) {
48 | state.socket.reconnectError = true
49 | },
50 | // default handler called for all methods
51 | SOCKET_ONMESSAGE (state, message) {
52 | console.debug('socket', message)
53 |
54 | // https://github.com/alexbosworth/ln-service/blob/master/lightning/subscribe_to_transactions.js
55 | if (message.address || message.tokens) {
56 | refreshBalance(this)
57 | refreshInvoices(this)
58 | refreshPayments(this)
59 | }
60 |
61 | // https://github.com/alexbosworth/ln-service/blob/master/lightning/subscribe_to_graph.js
62 | if (message.public_key && message.updated_at) {
63 | refreshPeers(this)
64 | }
65 |
66 | // https://github.com/alexbosworth/ln-service/blob/master/lightning/subscribe_to_channels.js
67 | if (message.transaction_id || message.partner_public_key) {
68 | refreshChannels(this)
69 | }
70 | },
71 | setUser (state, user) {
72 | state.user = user
73 | }
74 | },
75 | actions: {
76 | async checkSession ({ commit, dispatch }) {
77 | const { data, status } = await API.get('login')
78 |
79 | if (status === 200) {
80 | dispatch('lnd/loadInfo')
81 | commit('setUser', data)
82 | }
83 | },
84 |
85 | async login ({ commit, dispatch }, { username, password }) {
86 | const { data, status } = await API.post('login', { username, password })
87 |
88 | if (status === 200) {
89 | dispatch('lnd/loadInfo')
90 | commit('setUser', data)
91 |
92 | return true
93 | } else {
94 | return false
95 | }
96 | }
97 | },
98 | modules: {
99 | addresses,
100 | btc,
101 | channels,
102 | invoices,
103 | payments,
104 | lnd,
105 | peers,
106 | system
107 | }
108 | })
109 |
--------------------------------------------------------------------------------
/server/api/sys/service.js:
--------------------------------------------------------------------------------
1 | const { promisify } = require('util')
2 | const childProcess = require('child_process')
3 | const si = require('systeminformation')
4 | const axios = require('axios')
5 | const { format: formatDate, formatDistanceToNow } = require('date-fns')
6 | const fileSize = require('filesize')
7 | const execute = promisify(childProcess.exec)
8 |
9 | const exec = async cmd => {
10 | try {
11 | const { stdout } = await execute(cmd)
12 |
13 | return stdout
14 | } catch (err) {
15 | console.error(err)
16 |
17 | return err.message
18 | }
19 | }
20 |
21 | const retrieveOsInfo = async () => {
22 | try {
23 | const { hostname, platform, distro, release, arch: architecture } = await si.osInfo()
24 |
25 | return {
26 | hostname,
27 | platform,
28 | architecture,
29 | version: `${distro} ${release}`
30 | }
31 | } catch (err) {
32 | console.warn('Failed to retrieve OS information:', err)
33 | }
34 | }
35 |
36 | const retrieveInfo = async () => {
37 | try {
38 | const externalIP = await retrieveExternalIP()
39 | const { current: time, uptime } = await si.time()
40 | const upSince = new Date() - uptime * 1000
41 |
42 | return {
43 | externalIP,
44 | time: formatDate(time, 'yyyy-MM-dd HH:mm'),
45 | uptime: formatDistanceToNow(upSince)
46 | }
47 | } catch (err) {
48 | console.warn('Failed to retrieve info:', err)
49 | }
50 | }
51 |
52 | const retrieveDisk = async () => {
53 | try {
54 | const info = await si.fsSize()
55 | const { type, used, size: total } = info[0]
56 |
57 | return {
58 | type,
59 | total: fileSize(total),
60 | free: fileSize(total - used),
61 | used: fileSize(used),
62 | usedPercent: Math.round(100 / (total / used))
63 | }
64 | } catch (err) {
65 | console.warn('Failed to retrieve disk information:', err)
66 | }
67 | }
68 |
69 | const retrieveMemory = async () => {
70 | try {
71 | const info = await si.mem()
72 | const { total, active: used, available: free } = info
73 |
74 | return {
75 | total: fileSize(total),
76 | free: fileSize(free),
77 | used: fileSize(used),
78 | usedPercent: Math.round(100 / (total / used))
79 | }
80 | } catch (err) {
81 | console.warn('Failed to retrieve memory information:', err)
82 | }
83 | }
84 |
85 | const retrieveNetwork = async () => {
86 | try {
87 | const info = await si.networkStats()
88 | const netw = info[0]
89 |
90 | return {
91 | rxTotal: fileSize(netw.rx_bytes),
92 | txTotal: fileSize(netw.tx_bytes),
93 | rxSec: netw.rx_sec && `${fileSize(netw.rx_sec)}/s`,
94 | txSec: netw.tx_sec && `${fileSize(netw.tx_sec)}/s`
95 | }
96 | } catch (err) {
97 | console.warn('Failed to retrieve network information:', err)
98 | }
99 | }
100 |
101 | const retrieveExternalIP = async () => {
102 | try {
103 | const ipInfo = await axios.get('https://api.ipify.org?format=json')
104 |
105 | return ipInfo.data.ip
106 | } catch (err) {
107 | console.warn('Failed to retrieve external IP address.')
108 | }
109 | }
110 |
111 | module.exports = {
112 | exec,
113 | retrieveOsInfo,
114 | retrieveInfo,
115 | retrieveDisk,
116 | retrieveMemory,
117 | retrieveNetwork,
118 | retrieveExternalIP
119 | }
120 |
--------------------------------------------------------------------------------
/src/vue/components/FormField.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | {{ label }}
11 |
15 | {{ hint }}
16 |
17 |
21 | {{ message }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
56 |
57 |
78 |
79 |
163 |
--------------------------------------------------------------------------------
/src/vue/sections/SysInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | System
4 |
5 |
6 | Overview
7 |
8 |
9 | {{ info.externalIP }}
10 |
11 |
15 |
19 |
20 |
21 | Disk
22 |
23 |
24 |
30 |
31 |
35 |
39 |
43 |
44 |
45 | Memory
46 |
47 |
48 |
54 |
55 |
59 |
63 |
67 |
68 |
69 | Network
70 |
71 |
75 |
79 |
83 |
87 |
88 |
89 | Operating System
90 |
91 |
95 |
99 |
103 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
131 |
132 |
134 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@blitzbank/dashboard",
3 | "version": "0.1.2",
4 | "description": "Dashboard for your Bitcoind/LND full node.",
5 | "author": "Dennis Reimann (https://dennisreimann.de)",
6 | "scripts": {
7 | "build": "npm run cleanup && vue-cli-service build",
8 | "test": "vue-cli-service test:unit",
9 | "lint": "vue-cli-service lint",
10 | "cleanup": "rimraf dist",
11 | "release": "npm run lint && npm test && npm run build && npm publish",
12 | "start": "npm run cleanup && nodemon --exec 'vue-cli-service serve'",
13 | "start:prod": "$npm_package_bin_blitzbank",
14 | "test:e2e": "vue-cli-service test:e2e"
15 | },
16 | "main": "server/index.js",
17 | "files": [
18 | "bin",
19 | "dist",
20 | "server"
21 | ],
22 | "dependencies": {
23 | "axios": "0.19.0",
24 | "bitcoin-units": "0.3.0",
25 | "bitcoind-rpc": "0.8.1",
26 | "bufferutil": "4.0.1",
27 | "camelize-keys": "1.0.0",
28 | "clipboard": "2.0.4",
29 | "connect-history-api-fallback": "1.6.0",
30 | "cross-env": "6.0.3",
31 | "date-fns": "2.8.1",
32 | "dotenv": "8.2.0",
33 | "express": "4.17.1",
34 | "express-session": "1.17.0",
35 | "filesize": "6.0.1",
36 | "ln-service": "47.7.0",
37 | "lndconnect": "0.2.10",
38 | "morgan": "1.9.1",
39 | "passport": "0.4.1",
40 | "passport-local": "1.0.0",
41 | "qrcode": "1.4.4",
42 | "register-service-worker": "1.6.2",
43 | "systeminformation": "4.16.0",
44 | "vue": "2.6.11",
45 | "vue-native-websocket": "2.0.13",
46 | "vue-router": "3.1.3",
47 | "vuex": "3.1.2",
48 | "utf-8-validate": "5.0.2",
49 | "ws": "7.2.1"
50 | },
51 | "devDependencies": {
52 | "@babel/core": "7.7.5",
53 | "@vue/cli-plugin-babel": "4.1.1",
54 | "@vue/cli-plugin-e2e-cypress": "4.1.1",
55 | "@vue/cli-plugin-eslint": "4.1.1",
56 | "@vue/cli-plugin-pwa": "4.1.1",
57 | "@vue/cli-plugin-unit-jest": "4.1.1",
58 | "@vue/cli-service": "4.1.1",
59 | "@vue/eslint-config-standard": "5.0.1",
60 | "@vue/test-utils": "1.0.0-beta.30",
61 | "babel-core": "7.0.0-bridge.0",
62 | "babel-eslint": "10.0.3",
63 | "babel-jest": "24.9.0",
64 | "eslint": "6.7.2",
65 | "eslint-plugin-import": "2.19.1",
66 | "eslint-plugin-node": "10.0.0",
67 | "eslint-plugin-vue": "6.0.1",
68 | "lint-staged": "9.5.0",
69 | "nodemon": "2.0.2",
70 | "rimraf": "3.0.0",
71 | "standard": "14.3.1",
72 | "vue-template-compiler": "2.6.11",
73 | "webpack": "4.41.3",
74 | "webpack-dev-server": "3.9.0"
75 | },
76 | "bin": {
77 | "blitzbank": "./bin/blitzbank.sh"
78 | },
79 | "bugs": "https://github.com/dennisreimann/blitzbank-dashboard/issues",
80 | "engines": {
81 | "node": ">=10.13"
82 | },
83 | "gitHooks": {
84 | "pre-commit": "lint-staged"
85 | },
86 | "homepage": "https://github.com/dennisreimann/blitzbank-dashboard",
87 | "keywords": [
88 | "bitcoin",
89 | "lnd",
90 | "lightning",
91 | "lightning-network",
92 | "fullnode",
93 | "dashboard",
94 | "admin",
95 | "blitzbank"
96 | ],
97 | "license": "MIT",
98 | "lint-staged": {
99 | "*.js": [
100 | "npm run lint -- --fix",
101 | "git add"
102 | ],
103 | "*.vue": [
104 | "npm run lint -- --fix",
105 | "git add"
106 | ]
107 | },
108 | "publishConfig": {
109 | "access": "public"
110 | },
111 | "funding": "https://dennisreimann.de/donate.html",
112 | "repository": "dennisreimann/blitzbank-dashboard"
113 | }
114 |
--------------------------------------------------------------------------------
/src/vue/components/ChannelForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
91 |
92 | To create a channel you need to
93 |
94 | connect a peer
95 | .
96 |
97 |
98 |
99 |
164 |
165 |
179 |
--------------------------------------------------------------------------------
/src/vue/components/PaymentForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
32 |
37 |
38 |
44 |
45 |
49 | {{ ' ' }}
50 | {{ peerNameForPublicKey(request.destination) }}
51 |
52 |
56 |
60 |
64 |
65 | {{ request.expiresAt }}
66 |
70 |
71 |
72 |
73 |
79 |
80 |
81 |
82 |
83 |
171 |
172 |
182 |
--------------------------------------------------------------------------------
/server/api/lnd/service.js:
--------------------------------------------------------------------------------
1 | const qrcode = require('qrcode')
2 | const btcUnits = require('bitcoin-units')
3 | const camelizeKeys = require('camelize-keys')
4 | const { format: formatConnectionUrl, encodeMacaroon, encodeCert } = require('lndconnect')
5 | const { formatDistanceToNow, format: formatDate, parseISO: parseDate } = require('date-fns')
6 | const { lnd, lnService } = require('../../services/lnd')
7 | const { PUBLIC_HOST, LND_RPC_PORT, LND_MACAROON_BASE64, LND_CERT_BASE64 } = require('../../env')
8 |
9 | btcUnits.setDisplay('satoshi', { format: '{amount} sats' })
10 |
11 | const base64Decode = b64 => Buffer.from(b64, 'base64')
12 | const formatAt = dateString => formatDate(parseDate(dateString), 'yyyy-MM-dd HH:MM:SS')
13 | const connectionUrl = formatConnectionUrl({
14 | host: `${PUBLIC_HOST}:${LND_RPC_PORT}`,
15 | cert: encodeCert(base64Decode(LND_CERT_BASE64)),
16 | macaroon: encodeMacaroon(base64Decode(LND_MACAROON_BASE64))
17 | })
18 | let connectionQRCode
19 | (async function () { connectionQRCode = await qrcode.toDataURL(connectionUrl) })()
20 |
21 | const decorate = async (result, fnName) => {
22 | result = camelizeKeys(result)
23 |
24 | switch (fnName) {
25 | case 'getWalletInfo':
26 | console.log(result.latestBlockAt)
27 | result.latestBlockRelative = formatDistanceToNow(parseDate(result.latestBlockAt))
28 | result.connectionUrl = connectionUrl
29 | result.connectionQRCode = connectionQRCode
30 | break
31 |
32 | case 'getChainBalance':
33 | const satsChain = btcUnits(result.chainBalance, 'satoshi')
34 | result.chainBalanceSats = satsChain.format()
35 | result.chainBalanceBtc = satsChain.to('btc').format()
36 | break
37 |
38 | case 'getPendingChainBalance':
39 | const satsChainPending = btcUnits(result.pendingChainBalance, 'satoshi')
40 | result.pendingChainBalanceSats = satsChainPending.format()
41 | result.pendingChainBalanceBtc = satsChainPending.to('btc').format()
42 | break
43 |
44 | case 'getChannelBalance':
45 | const satsChannel = btcUnits(result.channelBalance, 'satoshi')
46 | result.channelBalanceSats = satsChannel.format()
47 | result.channelBalanceBtc = satsChannel.to('btc').format()
48 |
49 | const satsChannelPending = btcUnits(result.pendingBalance, 'satoshi')
50 | result.pendingChannelBalanceSats = satsChannelPending.format()
51 | result.pendingChannelBalanceBtc = satsChannelPending.to('btc').format()
52 | break
53 |
54 | case 'getInvoices':
55 | result.invoices = result.invoices.map(invoice => {
56 | return {
57 | ...invoice,
58 | createdDate: invoice.createdAt && formatAt(invoice.createdAt),
59 | expiresDate: invoice.expiresAt && formatAt(invoice.expiresAt),
60 | confirmedDate: invoice.confirmedAt && formatAt(invoice.confirmedAt)
61 | }
62 | })
63 | break
64 |
65 | case 'getPayments':
66 | result.payments = result.payments.map(payment => {
67 | return {
68 | ...payment,
69 | createdDate: payment.createdAt && formatAt(payment.createdAt)
70 | }
71 | })
72 | break
73 |
74 | case 'getPeers':
75 | result = await Promise.all(
76 | result.peers.map(async peer => {
77 | let node = {}
78 | try {
79 | node = await rpc('getNode', { public_key: peer.publicKey })
80 | } catch (err) {
81 | console.error(err)
82 | }
83 |
84 | return {
85 | ...peer,
86 | ...node
87 | }
88 | })
89 | )
90 | break
91 | }
92 |
93 | return result
94 | }
95 |
96 | const rpc = (fnName, opts = {}) => {
97 | return new Promise((resolve, reject) => {
98 | try {
99 | opts.lnd = lnd
100 | const fn = lnService[fnName]
101 | if (typeof fn === 'function') {
102 | fn(opts, async (error, result) => {
103 | if (error) {
104 | const [status, message, info] = error
105 | const err = new Error(`LND RPC ${fnName} failed. Payload: ${JSON.stringify(opts)}`)
106 |
107 | if (info && info.details) {
108 | const { details } = info
109 | err.details = details.replace(details[0], details[0].toUpperCase())
110 | } else if (message) {
111 | const sentence = message.split(/(?=[A-Z])/).map(s => s.toLowerCase()).join(' ')
112 | err.details = sentence.replace(sentence[0], sentence[0].toUpperCase())
113 | }
114 |
115 | err.status = status
116 | reject(err)
117 | } else {
118 | const res = await decorate(result, fnName)
119 | resolve(res)
120 | }
121 | })
122 | } else {
123 | const err = new Error(`${fnName} is not a LND service function.`)
124 | err.status = 500
125 | reject(err)
126 | }
127 | } catch (err) {
128 | err.status = 500
129 | reject(err)
130 | }
131 | })
132 | }
133 |
134 | module.exports = rpc
135 |
--------------------------------------------------------------------------------
/src/vue/sections/ListChannels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Active
6 |
10 |
11 |
12 | {{ peerNameForPublicKey(channel.partnerPublicKey) }}:
13 | {{ channel.id }} {{ channel.isPrivate ? "🔒" : "" }}
14 |
15 |
16 |
17 |
22 |
28 |
29 |
33 |
34 |
35 |
40 |
45 |
46 |
51 |
56 |
57 |
58 |
59 | No active channels, yet.
60 |
61 |
62 |
63 |
64 |
65 | Pending
66 |
70 |
71 |
75 | {{ peerNameForPublicKey(channel.partnerPublicKey) }}
76 | {{ status(channel) }}
77 |
78 | - {{ channel.localBalance }} sats
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
176 |
177 |
182 |
--------------------------------------------------------------------------------
/src/vue/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
18 |
240 |
241 |
248 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚡️ Blitzbank Dashboard 🏦
2 |
3 | Here‘s to the ***#reckless***! ⚡️
4 | Dashboard for your Bitcoind/LND full node.
5 |
6 | [](https://www.npmjs.com/package/@blitzbank/dashboard)
7 | [](https://snyk.io/test/github/dennisreimann/blitzbank-dashboard)
8 | [](http://standardjs.com/)
9 |
10 | ## 👉 Disclaimer
11 |
12 | This is an early stage project.
13 | Right now this is my personal playground for figuring out how to approach Lightning node management from an UX perspective.
14 | Potentially everything is subject to change.
15 |
16 | Nevertheless: As we are all figuring stuff out, I am putting this project out here and invite feedback.
17 | Let me know in case I can help setting up the dashboard – that's how we can improve the documentation too. 😉
18 |
19 | ## 🗜 Prerequisites
20 |
21 | The app requires at least Node.js 10.13 (tracking the latest active Node.js LTS version).
22 | This guarantees a reasonable level of backwards compatibility.
23 |
24 | You will need a Bitcoin and LND full node to run the app.
25 | For development you can use the [Polar](https://github.com/jamaljsr/polar)
26 | app, which spins up Lightning networks for local app development and testing.
27 |
28 | ## 🖥 Screenshots
29 |
30 | Here are some example screenshots:
31 |
32 | [Home](media/home.png) | [Peers](media/peers.png) | [Channels](media/channels.png) |
33 | [Invoices](media/invoices.png) | [Payments](media/payments.png) | [System](media/system.png)
34 |
35 | ## 📦 Setup
36 |
37 | I will make this easier at some point, but for now …
38 | SSH into your full node and execute the following commands:
39 |
40 | ```bash
41 | # create a new directory for the dashboard
42 | mkdir dashboard
43 | cd dashboard
44 |
45 | # initialize an empty project and install the app
46 | npm init @blitzbank/dashboard
47 |
48 | # edit the .env file in your favorite editor
49 | # (see the list of variables below)
50 | vim .env
51 |
52 | # start the app
53 | npx blitzbank
54 | ```
55 |
56 | You will most likely need to [setup a process manager](https://expressjs.com/en/advanced/best-practice-performance.html#ensure-your-app-automatically-restarts) to keep the app running.
57 | See the start script section below.
58 |
59 | ### ✨ Environment variables
60 |
61 | These env variables should be set:
62 |
63 | - `BITCOIND_RPC_PROTOCOL` - default: `http`
64 | - `BITCOIND_RPC_HOST` - default: `127.0.0.1`
65 | - `BITCOIND_RPC_PORT` - default: `8332`
66 | - `BITCOIND_RPC_USER`
67 | - `BITCOIND_RPC_PASSWORD`
68 | - `LND_RPC_HOST` - default: `localhost`
69 | - `LND_RPC_PORT` - default: `10009`
70 | - `LND_CERT_BASE64` - the base64 encoded string of the `tls.cert` file
71 | - `LND_MACAROON_BASE64` - the base64 encoded string of the macaroon file
72 | - `SERVER_PORT` - default: `4000`
73 | - `SSL_CERT_PATH`
74 | - `SSL_KEY_PATH`
75 | - `PUBLIC_HOST` - public host name that is used for connecting via Zap, Joule, etc.
76 |
77 | You also need to define the credentials for the dashboard and API requests:
78 |
79 | - `AUTH_USERNAME`
80 | - `AUTH_PASSWORD`
81 |
82 | ### 🚀 Start Script
83 |
84 | On a Linux system you can use the service manager Systemd.
85 | Add the following service configuration to a file named `/etc/systemd/system/dashboard.service`:
86 |
87 | ```ini
88 | [Unit]
89 | Description=Full Node Dashboard
90 |
91 | [Service]
92 | Type=simple
93 |
94 | # YOUR ADJUSTMENT START HERE:
95 | ExecStart=/usr/bin/npx blitzbank # the npx path might need adjustment: use `which npx` to find the location
96 | WorkingDirectory=/home/admin/dashboard # absolute path to the dashboard folder
97 | User=admin # your user
98 | Group=admin # your group
99 | # YOUR ADJUSTMENT END HERE.
100 |
101 | Environment=NODE_ENV=production
102 | StandardInput=null
103 | StandardOutput=syslog
104 | StandardError=syslog
105 | Restart=always
106 |
107 | [Install]
108 | WantedBy=multi-user.target
109 | ```
110 |
111 | Note that you have to set the values in the `YOUR ADJUSTMENT` part.
112 |
113 | After having created the file you can enable the service using the following command:
114 |
115 | ```bash
116 | # one time enabling of the service
117 | sudo systemctl enable dashboard.service
118 |
119 | # after that you can use commands like start, stop, restart or status
120 | sudo systemctl start dashboard.service
121 | ```
122 |
123 | ### ✨ Upgrading
124 |
125 | To install the latest version use the following command:
126 |
127 | ```bash
128 | npm install @blitzbank/dashboard@latest
129 | ```
130 |
131 | ## 🛠 Development Setup
132 |
133 | Install dependencies:
134 |
135 | ```bash
136 | npm install
137 | ```
138 |
139 | Use [mkcert](https://github.com/FiloSottile/mkcert) to setup the SSL certificates.
140 |
141 | Create a build and rebuild on file change.
142 |
143 | ```bash
144 | npm start
145 | ```
146 |
147 | See this [blog post](https://d11n.net/bitcoin-lnd-rpc-api-express.html) for details on some of the technicak decisions.
148 |
149 | ## 👛 Tip jar
150 |
151 | [](https://tippin.me/@dennisreimann)
152 |
153 |
154 |
155 |
156 |
157 | ## 🖖 Alternatives
158 |
159 | Here are some other projects with similar goals, you might want to have a look at those too:
160 |
161 | - [RTL – Ride the Lightning](https://github.com/ShahanaFarooqui/RTL)
162 | - [lnd-admin](https://github.com/janoside/lnd-admin)
163 | - [lndash](https://github.com/djmelik/lndash)
164 | - [ln-dashboard](https://github.com/PatrickLemke/ln-dashboard)
165 |
--------------------------------------------------------------------------------