├── 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 | 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 | 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 | 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 | 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 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /src/vue/components/AttributeList.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 13 | 14 | 53 | -------------------------------------------------------------------------------- /src/vue/components/Attribute.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /src/vue/components/Info.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | 55 | -------------------------------------------------------------------------------- /src/vue/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 54 | -------------------------------------------------------------------------------- /src/vue/pages/Payments.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 15 | 16 | 38 | 39 | 73 | -------------------------------------------------------------------------------- /src/vue/components/Dot.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 45 | 46 | 66 | -------------------------------------------------------------------------------- /src/vue/pages/Channels.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 65 | -------------------------------------------------------------------------------- /src/vue/components/BtcPanel.vue: -------------------------------------------------------------------------------- 1 | 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 | 17 | 18 | 43 | 44 | 89 | -------------------------------------------------------------------------------- /src/vue/components/LndPanel.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 68 | -------------------------------------------------------------------------------- /src/vue/components/Nav.vue: -------------------------------------------------------------------------------- 1 | 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 | 14 | 15 | 35 | 36 | 84 | -------------------------------------------------------------------------------- /src/vue/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 83 | -------------------------------------------------------------------------------- /src/vue/sections/NewPeer.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 70 | 71 | 80 | -------------------------------------------------------------------------------- /src/vue/components/FormSwitch.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 69 | 70 | 97 | -------------------------------------------------------------------------------- /src/vue/sections/ListPayments.vue: -------------------------------------------------------------------------------- 1 | 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 | 40 | 41 | 84 | -------------------------------------------------------------------------------- /src/vue/components/InvoiceForm.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 98 | 99 | 108 | -------------------------------------------------------------------------------- /src/vue/sections/BtcInfo.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 98 | -------------------------------------------------------------------------------- /src/vue/sections/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 107 | 108 | 113 | -------------------------------------------------------------------------------- /src/vue/sections/ListPeers.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 100 | 101 | 106 | -------------------------------------------------------------------------------- /src/vue/sections/ListInvoices.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 98 | 99 | 105 | -------------------------------------------------------------------------------- /src/vue/sections/NewAddress.vue: -------------------------------------------------------------------------------- 1 | 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 | 30 | 31 | 56 | 57 | 78 | 79 | 163 | -------------------------------------------------------------------------------- /src/vue/sections/SysInfo.vue: -------------------------------------------------------------------------------- 1 | 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 | 98 | 99 | 164 | 165 | 179 | -------------------------------------------------------------------------------- /src/vue/components/PaymentForm.vue: -------------------------------------------------------------------------------- 1 |