├── static └── .gitkeep ├── src ├── renderer │ ├── assets │ │ ├── .gitkeep │ │ └── logo.png │ ├── components │ │ ├── Empty.vue │ │ ├── Peers.vue │ │ ├── Info.vue │ │ ├── Network.vue │ │ ├── common.js │ │ └── Console.vue │ ├── misc.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── index.js │ │ │ └── Nodes.js │ ├── main.js │ ├── css │ │ ├── theme │ │ │ ├── light │ │ │ │ └── styles.css │ │ │ └── dark │ │ │ │ └── styles.css │ │ └── app.css │ ├── menu.js │ └── App.vue ├── main │ ├── index.dev.js │ └── index.js └── index.ejs ├── bug.png ├── image ├── dialog.png ├── notify.gif └── alice_pay_bob.gif ├── test ├── .eslintrc ├── unit │ ├── specs │ │ └── Info.js │ ├── index.js │ └── karma.conf.js └── e2e │ ├── index.js │ ├── utils.js │ └── specs │ └── Launch.spec.js ├── .gitignore ├── .nodetypes ├── src │ ├── clightning │ │ ├── util │ │ │ ├── README.md │ │ │ └── lnetconfig.js │ │ ├── CLightningInfo.vue │ │ ├── CLightningPeers.vue │ │ └── CLightningController.js │ ├── btcd │ │ ├── package.json │ │ ├── BtcdInfo.vue │ │ ├── BtcdPeers.vue │ │ └── BtcdController.js │ ├── lnd │ │ ├── package.json │ │ ├── util │ │ │ ├── lnd.conf │ │ │ ├── test.js │ │ │ └── parseproto.js │ │ ├── config.js │ │ ├── LndInfo.vue │ │ ├── LndPeers.vue │ │ ├── LndController.js │ │ └── monaco.js │ └── bitcoin │ │ ├── BitcoinInfo.vue │ │ ├── BitcoinPeers.vue │ │ └── BitcoinController.js └── index.js ├── .vscode └── launch.json ├── .babelrc ├── .electron-vue ├── dev-client.js ├── webpack.main.config.js ├── webpack.web.config.js ├── build.js ├── webpack.renderer.config.js └── dev-runner.js ├── package.json └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsbondi/nodes-debug/HEAD/bug.png -------------------------------------------------------------------------------- /image/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsbondi/nodes-debug/HEAD/image/dialog.png -------------------------------------------------------------------------------- /image/notify.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsbondi/nodes-debug/HEAD/image/notify.gif -------------------------------------------------------------------------------- /image/alice_pay_bob.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsbondi/nodes-debug/HEAD/image/alice_pay_bob.gif -------------------------------------------------------------------------------- /src/renderer/components/Empty.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsbondi/nodes-debug/HEAD/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/misc.js: -------------------------------------------------------------------------------- 1 | export const empties = { 2 | info: {}, 3 | peers: {peers: [], banned:[]} 4 | } 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "expect": true, 8 | "should": true, 9 | "__static": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/electron/* 3 | dist/build_nodetypes/* 4 | dist/web/* 5 | build/* 6 | !build/icons 7 | coverage 8 | node_modules/ 9 | npm-debug.log 10 | npm-debug.log.* 11 | thumbs.db 12 | !.gitkeep 13 | .vscode/settings.json -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import modules from './modules' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | modules, 10 | strict: process.env.NODE_ENV !== 'production' 11 | }) 12 | -------------------------------------------------------------------------------- /.nodetypes/src/clightning/util/README.md: -------------------------------------------------------------------------------- 1 | ## Using with [lnet](https://github.com/cdecker/lnet) 2 | 3 | 1) Launch lnet 4 | * `lnet-cli start [dotfile]` _see link above_ 5 | 1) Run script 6 | * `node lnetconfig.js` 7 | 1) Launch nodes-debug 8 | 1) From the menu 9 | * `Config -> Load` and browse to the newly created `config.json` -------------------------------------------------------------------------------- /test/unit/specs/Info.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Info from '@/components/Info' 3 | 4 | describe('Info.vue', () => { 5 | it('should be tested at some point, stay tuned', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: h => h('i') 9 | }).$mount() 10 | 11 | expect(true).to.equal(true) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /.nodetypes/src/btcd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodes-debug-btcd", 3 | "version": "0.0.1", 4 | "description": "btcd module for nodes-debug", 5 | "main": "BtcdController.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "bitcoin" 11 | ], 12 | "author": "Richard Bondi", 13 | "license": "MIT", 14 | "dependencies": { 15 | "ws": "^6.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/store/modules/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The file enables `@/store/index.js` to import all vuex modules 3 | * in a one-shot manner. There should not be any reason to edit this file. 4 | */ 5 | 6 | const files = require.context('.', false, /\.js$/) 7 | const modules = {} 8 | 9 | files.keys().forEach(key => { 10 | if (key === './index.js') return 11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 12 | }) 13 | 14 | export default modules 15 | -------------------------------------------------------------------------------- /test/e2e/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Set BABEL_ENV to use proper env config 4 | process.env.BABEL_ENV = 'test' 5 | 6 | // Enable use of ES6+ on required files 7 | require('babel-register')({ 8 | ignore: /node_modules/ 9 | }) 10 | 11 | // Attach Chai APIs to global scope 12 | const { expect, should, assert } = require('chai') 13 | global.expect = expect 14 | global.should = should 15 | global.assert = assert 16 | 17 | // Require all JS files in `./specs` for Mocha to consume 18 | require('require-dir')('./specs') 19 | -------------------------------------------------------------------------------- /src/renderer/components/Peers.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /.nodetypes/src/lnd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodes-debug-lnd", 3 | "version": "0.0.1", 4 | "description": "lnd module for nodes-debug", 5 | "main": "LndController.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "postinstall": "npm install grpc@1.16.0 --runtime=electron --target=3.0.9" 9 | }, 10 | "keywords": [ 11 | "lnd", 12 | "grpc" 13 | ], 14 | "author": "Richard Bondi", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@grpc/proto-loader": "^0.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.nodetypes/src/lnd/util/lnd.conf: -------------------------------------------------------------------------------- 1 | [Application Options] 2 | datadir=/home/dummyuser/alice/data 3 | adminmacaroonpath=/home/dummyuser/alice/data/bitcoin/simnet/admin.macaroon 4 | tlscertpath=/dummypath/.lnd/tls.cert # work with comment 5 | logdir=alice/log 6 | debuglevel=info 7 | debughtlc=true 8 | rpclisten=localhost:10001 9 | listen=localhost:10011 10 | restlisten=localhost:8001 11 | alias=Alice 12 | 13 | [Bitcoin] 14 | bitcoin.simnet=1 15 | bitcoin.active=1 16 | bitcoin.node=btcd 17 | 18 | [btcd] 19 | btcd.rpcuser=user 20 | btcd.rpcpass=pass 21 | 22 | -------------------------------------------------------------------------------- /test/e2e/utils.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import { Application } from 'spectron' 3 | 4 | export default { 5 | afterEach () { 6 | this.timeout(10000) 7 | 8 | if (this.app && this.app.isRunning()) { 9 | return this.app.stop() 10 | } 11 | }, 12 | beforeEach () { 13 | this.timeout(10000) 14 | this.app = new Application({ 15 | path: electron, 16 | args: ['dist/electron/main.js'], 17 | startTimeout: 10000, 18 | waitTimeout: 10000 19 | }) 20 | 21 | return this.app.start() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import App from './App' 4 | import store from './store' 5 | 6 | if (!process.env.IS_WEB) Vue.use(require('vue-electron')) 7 | Vue.config.productionTip = false 8 | 9 | import ElementUI from 'element-ui' 10 | import 'element-ui/lib/theme-chalk/index.css' 11 | import locale from 'element-ui/lib/locale/lang/en' 12 | import './css/app.css' 13 | 14 | Vue.use(ElementUI, { locale }) 15 | 16 | /* eslint-disable no-new */ 17 | new Vue({ 18 | components: { App }, 19 | store, 20 | template: '', 21 | }).$mount('#app') 22 | 23 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.config.devtools = false 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.js$/) 7 | 8 | testsContext.keys().forEach(testsContext) 9 | 10 | // require all src files except main.js for coverage. 11 | // you can also change this to match only the subset of files that 12 | // you want coverage for. 13 | // const srcContext = require.context('../../src/renderer', true, /^\.\/(?!main(\.js)?$)/) 14 | // srcContext.keys().forEach(srcContext) 15 | -------------------------------------------------------------------------------- /src/renderer/components/Info.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | -------------------------------------------------------------------------------- /src/renderer/components/Network.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Current", 11 | "program": "${file}" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Build nodetypes", 17 | "program": "${workspaceFolder}/.nodetypes/index.js", 18 | "args": [], 19 | "cwd": "${workspaceFolder}/.nodetypes" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.nodetypes/src/lnd/util/test.js: -------------------------------------------------------------------------------- 1 | const Lnd = require('../config') 2 | 3 | let config = { 4 | host: '127.0.0.10', 5 | port: 11111, 6 | name: 'test', 7 | config: './lnd.conf' 8 | } 9 | 10 | let lnd = new Lnd(config) 11 | 12 | console.log('test config all 5 parameters', (lnd.user=='user' && lnd.password=='pass'&& lnd.port==11111 && lnd.host=='127.0.0.10' 13 | && lnd.macaroonPath == '/home/dummyuser/alice/data/bitcoin/simnet/admin.macaroon' 14 | && lnd.certPath == '/dummypath/.lnd/tls.cert')) 15 | 16 | delete config.host 17 | lnd = new Lnd(config) 18 | 19 | console.log('test host from file', (lnd.host=='localhost')) 20 | 21 | delete config.port 22 | lnd = new Lnd(config) 23 | 24 | console.log('test port from file', (lnd.port=='10001')) 25 | 26 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "env": { 4 | "test": { 5 | "presets": [ 6 | ["env", { 7 | "targets": { "node": 7 } 8 | }], 9 | "stage-0" 10 | ], 11 | "plugins": ["istanbul"] 12 | }, 13 | "main": { 14 | "presets": [ 15 | ["env", { 16 | "targets": { "node": 7 } 17 | }], 18 | "stage-0" 19 | ] 20 | }, 21 | "renderer": { 22 | "presets": [ 23 | ["env", { 24 | "modules": false 25 | }], 26 | "stage-0" 27 | ] 28 | }, 29 | "web": { 30 | "presets": [ 31 | ["env", { 32 | "modules": false 33 | }], 34 | "stage-0" 35 | ] 36 | } 37 | }, 38 | "plugins": ["transform-runtime"] 39 | } 40 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | // Set environment for development 11 | process.env.NODE_ENV = 'development' 12 | 13 | // Install `electron-debug` with `devtron` 14 | require('electron-debug')({ showDevTools: true }) 15 | 16 | // Install `vue-devtools` 17 | require('electron').app.on('ready', () => { 18 | let installExtension = require('electron-devtools-installer') 19 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 20 | .then(() => {}) 21 | .catch(err => { 22 | console.log('Unable to install `vue-devtools`: \n', err) 23 | }) 24 | }) 25 | 26 | // Require `main` process to boot app 27 | require('./index') 28 | -------------------------------------------------------------------------------- /src/renderer/components/common.js: -------------------------------------------------------------------------------- 1 | import Empty from './Empty' 2 | import { empties } from '../misc' 3 | 4 | 5 | export default { 6 | computed: { 7 | currentView() { 8 | const currentType = this.$store.state.Nodes.currentType 9 | const loaded = this.$store.state.Nodes.loadedTypes 10 | const y = this.$store.state.Nodes.currentPage 11 | return loaded.length && ~loaded.indexOf(currentType) ? currentType + '-' + this.suffix : 'empty' 12 | } 13 | }, 14 | methods: { 15 | emptyPage() { 16 | const page = this.$store.state.Nodes.currentPage 17 | this.$store.commit(this.mutation, empties[page]) 18 | }, 19 | getPage() { return this.$store.state.Nodes.currentPage } 20 | }, 21 | data() { 22 | return { 23 | loading: false 24 | } 25 | }, 26 | components: { Empty } 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/specs/Launch.spec.js: -------------------------------------------------------------------------------- 1 | import utils from '../utils' 2 | 3 | console.log('e2e testing deferred until refactor of nodetypes, stay tuned') 4 | // describe('Launch', function() { 5 | // beforeEach(utils.beforeEach) 6 | // afterEach(utils.afterEach) 7 | 8 | // it('shows the application window', async function() { 9 | // const isVisible = await this.app.browserWindow.isVisible() 10 | // expect(isVisible).to.equal(true) 11 | // }) 12 | // it('shows the proper application title', async function() { 13 | // const title = await this.app.client.getTitle() 14 | // expect(title).to.equal('nodes-rpc') 15 | // }) 16 | // it('loads with no errors', async function() { 17 | // const logs = await this.app.client.getRenderProcessLogs(); 18 | // const errors = logs.filter(log => { 19 | // console.log(log.message); 20 | // console.log(log.source); 21 | // console.log(log.level); 22 | // return log.level == "ERROR" 23 | // }); 24 | // expect(errors.length).to.equal(0) 25 | // }) 26 | // }) 27 | -------------------------------------------------------------------------------- /src/renderer/css/theme/light/styles.css: -------------------------------------------------------------------------------- 1 | /* monoco-theme: 'vs-light' */ 2 | 3 | body, .el-tabs__item,.el-table th, .el-table tr, .el-form-item__label, 4 | .el-dialog__title, .el-input__inner { 5 | color: #000; 6 | } 7 | 8 | body, .el-tabs__nav-wrap::after, .el-table__body-wrapper,.el-table th, 9 | .el-table tr, .el-dialog, .el-input__inner, .el-loading-mask { 10 | background: #fff; 11 | } 12 | 13 | .el-tabs--card>.el-tabs__header .el-tabs__nav, .el-tabs--card>.el-tabs__header .el-tabs__item.is-active, 14 | .el-tabs--card>.el-tabs__header .el-tabs__item, .el-tabs--card>.el-tabs__header, .editor, 15 | .el-tabs__new-tab, .el-input__inner { 16 | border-color: #e4e7ed; 17 | } 18 | 19 | .el-tabs__nav-wrap::after, .el-table::before { background-color: #e4e7ed; } 20 | 21 | .el-table__body tr.current-row>td, .el-table__expanded-cell, 22 | .el-table__row:hover>td { 23 | background-color: #fff; 24 | } 25 | 26 | .el-tabs--card>.el-tabs__header .el-tabs__item.is-active { 27 | border-bottom-color: #fff; 28 | } 29 | 30 | .el-table td, .el-table th.is-leaf { 31 | border-bottom: 1px solid #e4e7ed; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /.nodetypes/src/clightning/util/lnetconfig.js: -------------------------------------------------------------------------------- 1 | const child_process = require("child_process") 2 | const fs = require('fs') 3 | 4 | const nodes = child_process.execSync("lnet-cli alias") 5 | .toString() 6 | .split("\n") 7 | .reduce((o, n, i) => { 8 | let node = /alias lcli-([^=]+)="lightning-cli --lightning-dir=([^"]+)"/.exec(n) 9 | if(node) { 10 | const name = node[1].replace(/"/g,"") 11 | const entry = { 12 | name: name, 13 | type: "clightning", 14 | port: "", 15 | host: "", 16 | config: `${node[2]}/.ndconf`, 17 | index: `lnetnode${i}`, 18 | cfg: `alias=${name} 19 | lightning-dir=${node[2]} 20 | ` 21 | } 22 | o.nodes.push(entry) 23 | } 24 | return o 25 | },{nodes: [], theme:"dark"}) 26 | 27 | nodes.nodes.forEach(n => { 28 | fs.writeFileSync(`${n.config}`, n.cfg) 29 | delete(n.cfg) 30 | }) 31 | 32 | nodes.nodes.push({ 33 | name: "Bitcoin Regtest", 34 | type: "bitcoin", 35 | port: "", 36 | host: "", 37 | config: `${nodes.nodes[0].config.split('/').slice(0, -2).join('/')}/bitcoin.conf`, 38 | index: `bitcoinregtest`, 39 | }) 40 | 41 | fs.writeFileSync("config.json", JSON.stringify(nodes)) 42 | -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === 'compiling') { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | ` 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/renderer/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px; 3 | height: 100%; 4 | } 5 | 6 | h3 { 7 | clear: both; 8 | margin-top: 10px; 9 | } 10 | 11 | .label { 12 | clear: both; 13 | width: 200px; 14 | display: inline-block; 15 | vertical-align: top; 16 | } 17 | 18 | .label2 { 19 | padding-left: 10px; 20 | margin-right: -10px; 21 | } 22 | 23 | .value { 24 | display: inline-block; 25 | } 26 | 27 | .selected-peer { 28 | background-color: yellow; 29 | } 30 | 31 | .peer { 32 | cursor: pointer; 33 | } 34 | 35 | .led { 36 | margin: 0 auto; 37 | width: 12px; 38 | height: 12px; 39 | border-radius: 50%; 40 | } 41 | .led-green { 42 | background-color: #ABFF00; 43 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px; 44 | } 45 | .led-red { 46 | background-color: #F00; 47 | border-radius: 50%; 48 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #441313 0 -1px 9px, rgba(255, 0, 0, 0.5) 0 2px 12px; 49 | } 50 | 51 | .console-container { 52 | display: flex; 53 | flex-direction: column; 54 | position: absolute; 55 | top: 101px; 56 | bottom: 20px; 57 | } 58 | 59 | .editor-container { 60 | display: flex; 61 | flex: 1; 62 | } 63 | 64 | .el-tabs__content { 65 | overflow: visible; 66 | position: inherit; 67 | } 68 | 69 | .node-tabs { 70 | min-height: 30px; 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/renderer/css/theme/dark/styles.css: -------------------------------------------------------------------------------- 1 | /* monoco-theme: 'vs-dark' */ 2 | 3 | body, .el-tabs__item,.el-table th, .el-table tr, .el-form-item__label, 4 | .el-dialog__title, .el-input__inner { 5 | color: #ccc; 6 | } 7 | 8 | body, .el-tabs__nav-wrap::after, .el-table__body-wrapper,.el-table th, 9 | .el-table tr, .el-dialog, .el-input__inner, .el-loading-mask { 10 | background: #383838; 11 | } 12 | 13 | .el-tabs--card>.el-tabs__header .el-tabs__nav, .el-tabs--card>.el-tabs__header .el-tabs__item.is-active, 14 | .el-tabs--card>.el-tabs__header .el-tabs__item, .el-tabs--card>.el-tabs__header, .editor, 15 | .el-tabs__new-tab, .el-input__inner { 16 | border-color: #888; 17 | } 18 | 19 | .el-tabs__nav-wrap::after, .el-table::before { background-color: #888; } 20 | 21 | .el-table__body tr.current-row>td, .el-table__expanded-cell, 22 | .el-table__body .el-table__row:hover>td { 23 | background-color: #383838; 24 | } 25 | 26 | .el-tabs--card>.el-tabs__header .el-tabs__item.is-active { 27 | border-bottom-color: #383838; 28 | } 29 | 30 | .el-table td, .el-table th.is-leaf { 31 | border-bottom: 1px solid #888; 32 | } 33 | 34 | .el-table__body tr.hover-row.current-row>td, 35 | .el-table__body tr.hover-row.el-table__row--striped.current-row>td, 36 | .el-table__body tr.hover-row.el-table__row--striped>td, 37 | .el-table__body tr.hover-row>td { 38 | background-color: #383838; 39 | background: #383838; 40 | } 41 | 42 | .el-table--enable-row-hover .el-table__body tr:hover>td{background-color:#585858} 43 | 44 | -------------------------------------------------------------------------------- /.nodetypes/index.js: -------------------------------------------------------------------------------- 1 | const compiler = require('vueify').compiler 2 | const fs = require('fs') 3 | const path = require('path') 4 | process.env.NODE_ENV = 'production' 5 | const types = process.argv.slice(2) 6 | const child_process = require('child_process'); 7 | 8 | const src = path.resolve('./src') 9 | if (!fs.existsSync(path.resolve('../dist'))) 10 | fs.mkdirSync(path.resolve('../dist')) 11 | if (!fs.existsSync(path.join(path.resolve('../dist'), 'build_nodetypes'))) 12 | fs.mkdirSync(path.join(path.resolve('../dist'), 'build_nodetypes')) 13 | const out = path.resolve('../dist/build_nodetypes') 14 | const nodetypes = types.length && types || fs.readdirSync(src) 15 | nodetypes.forEach(t => { 16 | const files = fs.readdirSync(path.join(src, t)) 17 | if (!fs.existsSync(path.join(out, t))) 18 | fs.mkdirSync(path.join(out, t)) 19 | 20 | files.forEach(f => { 21 | const filePath = path.join(src, t, f) 22 | if (fs.lstatSync(filePath).isDirectory()) return 23 | const fileContent = fs.readFileSync(path.join(src, t, f)).toString() 24 | if (f.slice(-3) != 'vue') 25 | fs.writeFileSync(path.join(out, t, f), fileContent) 26 | else 27 | compiler.compile(fileContent, path.join(src, t, f), (err, result) => { 28 | fs.writeFileSync(path.join(out, t, f.replace('.vue', '.vue.js')), result) 29 | }) 30 | }) 31 | if (fs.existsSync(path.join(out, t, 'package.json')) && !fs.existsSync(path.join(out, t, 'package-lock.json'))) { 32 | child_process.execSync(`cd ${path.join(out, t)} && npm install`) 33 | } 34 | 35 | }) -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nodes-debug 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 11 | <% } %> 12 | 13 | 14 |
15 | 16 | 19 | 24 | <% if (htmlWebpackPlugin.options.environment.production){ %> 25 | 26 | <% } %> 27 | <% if (!htmlWebpackPlugin.options.environment.production){ %> 28 | 29 | <% } %> 30 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.nodetypes/src/lnd/config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const os = require('os') 3 | const path = require('path') 4 | 5 | class Config { 6 | constructor(cfg) { 7 | const tilde = cfg && cfg.config.replace('~', os.homedir()) 8 | const config = tilde && (fs.readFileSync(tilde || `${os.homedir()}/.lnd/lnd.conf`, 'utf8')); 9 | let rpcport, rpchost 10 | config.split('\n').forEach(line => { 11 | let rpcuser = line.match(/^[a-z]+\.rpcuser\s?=\s?([^(#|\s)]+)[#\s)]?/) 12 | if (rpcuser) {this.user = rpcuser[1]; return} 13 | let rpcpass = line.match(/^[a-z]+\.rpcpass(word)?\s?=\s?([^(#|\s)]+)[#\s)]?/) 14 | if (rpcpass) {this.password = rpcpass[2]; return} 15 | let rpclisten = line.match(/^\s?rpclisten\s?=\s?([^:]+):([0-9]+)/) 16 | if (rpclisten) { 17 | rpchost = rpclisten[1] 18 | rpcport = rpclisten[2] 19 | return 20 | } 21 | let macaroonPath = line.match(/^\s?adminmacaroonpath\s?=\s?([^(#|\s)]+)[#\s)]?/) 22 | if (macaroonPath) { this.macaroonPath = macaroonPath[1]; return } 23 | let certPath = line.match(/^\s?tlscertpath\s?=\s?([^(#|\s)]+)[#\s)]?/) 24 | if (certPath) { this.certPath = certPath[1]; return } 25 | }) 26 | this.port = cfg && cfg.port || rpcport || '10009' 27 | this.host = cfg && cfg.host || rpchost || '127.0.0.1' 28 | if(!this.macaroonPath) this.macaroonPath = `${os.homedir()}/.lnd/data/chain/bitcoin/simnet/admin.macaroon` 29 | if(!this.certPath) this.certPath = `${os.homedir()}/.lnd/tls.cert` 30 | } 31 | } 32 | 33 | module.exports = Config -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const merge = require('webpack-merge') 5 | const webpack = require('webpack') 6 | 7 | const baseConfig = require('../../.electron-vue/webpack.renderer.config') 8 | const projectRoot = path.resolve(__dirname, '../../src/renderer') 9 | 10 | // Set BABEL_ENV to use proper preset config 11 | process.env.BABEL_ENV = 'test' 12 | 13 | let webpackConfig = merge(baseConfig, { 14 | devtool: '#inline-source-map', 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': '"testing"' 18 | }) 19 | ] 20 | }) 21 | 22 | // don't treat dependencies as externals 23 | delete webpackConfig.entry 24 | delete webpackConfig.externals 25 | delete webpackConfig.output.libraryTarget 26 | 27 | // apply vue option to apply isparta-loader on js 28 | webpackConfig.module.rules 29 | .find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader' 30 | 31 | module.exports = config => { 32 | config.set({ 33 | browsers: ['visibleElectron'], 34 | client: { 35 | useIframe: false 36 | }, 37 | coverageReporter: { 38 | dir: './coverage', 39 | reporters: [ 40 | { type: 'lcov', subdir: '.' }, 41 | { type: 'text-summary' } 42 | ] 43 | }, 44 | customLaunchers: { 45 | 'visibleElectron': { 46 | base: 'Electron', 47 | flags: ['--show'] 48 | } 49 | }, 50 | frameworks: ['mocha', 'chai'], 51 | files: ['./index.js'], 52 | preprocessors: { 53 | './index.js': ['webpack', 'sourcemap'] 54 | }, 55 | reporters: ['spec', 'coverage'], 56 | singleRun: true, 57 | webpack: webpackConfig, 58 | webpackMiddleware: { 59 | noInfo: true 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | 11 | let mainConfig = { 12 | entry: { 13 | main: path.join(__dirname, '../src/main/index.js') 14 | }, 15 | externals: [ 16 | ...Object.keys(dependencies || {}) 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | use: 'babel-loader', 23 | exclude: /node_modules/ 24 | }, 25 | { 26 | test: /\.node$/, 27 | use: 'node-loader' 28 | } 29 | ] 30 | }, 31 | node: { 32 | __dirname: process.env.NODE_ENV !== 'production', 33 | __filename: process.env.NODE_ENV !== 'production' 34 | }, 35 | output: { 36 | filename: '[name].js', 37 | libraryTarget: 'commonjs2', 38 | path: path.join(__dirname, '../dist/electron') 39 | }, 40 | plugins: [ 41 | new webpack.NoEmitOnErrorsPlugin() 42 | ], 43 | resolve: { 44 | extensions: ['.js', '.json', '.node'] 45 | }, 46 | target: 'electron-main', 47 | devServer: { 48 | headers: { "Access-Control-Allow-Origin": "*" } 49 | } 50 | } 51 | 52 | /** 53 | * Adjust mainConfig for development settings 54 | */ 55 | if (process.env.NODE_ENV !== 'production') { 56 | mainConfig.plugins.push( 57 | new webpack.DefinePlugin({ 58 | '__static': `"${path.join(__dirname, '../').replace(/\\/g, '\\\\')}"` 59 | }) 60 | ) 61 | } 62 | 63 | /** 64 | * Adjust mainConfig for production settings 65 | */ 66 | if (process.env.NODE_ENV === 'production') { 67 | mainConfig.plugins.push( 68 | new BabiliWebpackPlugin(), 69 | new webpack.DefinePlugin({ 70 | 'process.env.NODE_ENV': '"production"' 71 | }) 72 | ) 73 | } 74 | 75 | module.exports = mainConfig 76 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | 3 | /** 4 | * Set `__static` path to static files in production 5 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html 6 | */ 7 | if (process.env.NODE_ENV !== 'development') { 8 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') 9 | } 10 | 11 | let mainWindow 12 | const winURL = process.env.NODE_ENV === 'development' 13 | ? `http://localhost:9080` 14 | : `file://${__dirname}/index.html` 15 | 16 | function createWindow () { 17 | /** 18 | * Initial window options 19 | */ 20 | mainWindow = new BrowserWindow({ 21 | height: 563, 22 | useContentSize: true, 23 | width: 1000, 24 | webPreferences: {webSecurity: false}, 25 | icon: 'bug.png' 26 | }) 27 | 28 | // throw ({message: require('path').join(__static,'bug.png')}) 29 | 30 | // mainWindow.webContents.openDevTools() 31 | mainWindow.maximize() 32 | 33 | mainWindow.loadURL(winURL) 34 | 35 | mainWindow.on('closed', () => { 36 | mainWindow = null 37 | }) 38 | } 39 | 40 | app.on('ready', createWindow) 41 | 42 | app.on('window-all-closed', () => { 43 | if (process.platform !== 'darwin') { 44 | app.quit() 45 | } 46 | }) 47 | 48 | app.on('activate', () => { 49 | if (mainWindow === null) { 50 | createWindow() 51 | } 52 | }) 53 | 54 | /** 55 | * Auto Updater 56 | * 57 | * Uncomment the following code below and install `electron-updater` to 58 | * support auto updating. Code Signing with a valid certificate is required. 59 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 60 | */ 61 | 62 | /* 63 | import { autoUpdater } from 'electron-updater' 64 | 65 | autoUpdater.on('update-downloaded', () => { 66 | autoUpdater.quitAndInstall() 67 | }) 68 | 69 | app.on('ready', () => { 70 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() 71 | }) 72 | */ 73 | -------------------------------------------------------------------------------- /.nodetypes/src/btcd/BtcdInfo.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 75 | -------------------------------------------------------------------------------- /.nodetypes/src/bitcoin/BitcoinInfo.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 76 | -------------------------------------------------------------------------------- /src/renderer/menu.js: -------------------------------------------------------------------------------- 1 | const { remote, webFrame } = require('electron') 2 | const { Menu, MenuItem } = remote 3 | 4 | export class MenuHandler { 5 | constructor(app) { 6 | var app = app 7 | const menu = new Menu() 8 | menu.append(new MenuItem({ 9 | label: 'Config', 10 | submenu: [ 11 | { label: 'Save', click() { app.handleMenu('cfg-save') } }, 12 | { label: 'Load', click() { app.handleMenu('cfg-load') } }, 13 | { label: 'Edit Node', click() { app.handleMenu('cfg-node') } }, 14 | { 15 | label: 'Theme', 16 | submenu: [ 17 | { label: 'Light', click() { app.handleMenu('theme-light') } }, 18 | { label: 'Dark', click() { app.handleMenu('theme-dark') } }, 19 | ] 20 | } 21 | ] 22 | })) 23 | 24 | menu.append(new MenuItem({ 25 | label: 'Command', 26 | submenu: [ 27 | { label: 'Execute at Cursor', click() { app.handleMenu('cmd-exec') } }, 28 | { label: 'Save', click() { app.handleMenu('cmd-save') } }, 29 | { label: 'Load', click() { app.handleMenu('cmd-load') } }, 30 | ] 31 | })) 32 | 33 | menu.append(new MenuItem({ 34 | label: 'Console', 35 | submenu: [ 36 | { label: 'Clear', click() { app.handleMenu('result-clear') } }, 37 | { label: 'Save', click() { app.handleMenu('result-save') } }, 38 | { label: 'Load', click() { app.handleMenu('result-load') } }, 39 | ] 40 | })) 41 | 42 | menu.append(new MenuItem({ 43 | label: 'View', 44 | submenu: [ 45 | { label: 'Zoon +', accelerator: 'CmdOrCtrl+=', click() { webFrame.setZoomFactor(webFrame.getZoomFactor()*1.2) } }, 46 | { label: 'Zoom -', accelerator: 'CmdOrCtrl+-', click() { webFrame.setZoomFactor(webFrame.getZoomFactor()/1.2) } }, 47 | ] 48 | })) 49 | 50 | if(process.env.NODE_ENV == 'development') 51 | menu.append(new MenuItem({ 52 | label: 'Dev', 53 | submenu: [ 54 | { label: 'Reload', click() { remote.BrowserWindow.getFocusedWindow().reload() } }, 55 | ] 56 | })) 57 | 58 | Menu.setApplicationMenu(menu) 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /.nodetypes/src/clightning/CLightningInfo.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 85 | -------------------------------------------------------------------------------- /.nodetypes/src/lnd/LndInfo.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 79 | -------------------------------------------------------------------------------- /src/renderer/store/modules/Nodes.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | window.controllerInstances = { 3 | 0: { // dummy contoller 4 | getInfo: () => new Promise(resolve => resolve({})), 5 | info: {}, 6 | getPeers: () => new Promise(resolve => resolve([])), 7 | peers: {} 8 | } 9 | } 10 | 11 | const state = { 12 | currentType: 'bitcoin', 13 | currentIndex: 0, 14 | loadedTypes: [], 15 | instantiatedTypes: [], 16 | registeredTypes: [], 17 | controllers: {}, 18 | nodes: {}, 19 | currentInfo: {}, 20 | currentPeers: {}, 21 | currentPage: 'info', 22 | loading: false, 23 | consoleReady: false 24 | } 25 | 26 | const mutations = { 27 | node_type_loaded (state, type) { 28 | state.loadedTypes = state.loadedTypes.concat([type]) 29 | }, 30 | node_controller_type_loaded (state, controllerinfo) { 31 | if(!state.controllers[controllerinfo.type]) state.controllers[controllerinfo.type] = controllerinfo.controller 32 | }, 33 | node_controller_type_instantiated (state, type) { 34 | state.instantiatedTypes = state.instantiatedTypes.concat([type]) 35 | }, 36 | node_controller_type_registered (state, type) { 37 | state.registeredTypes = state.registeredTypes.concat([type]) 38 | }, 39 | node_type_changed (state, type) { state.currentType = type }, 40 | node_instantiate_controller (state, instanceInfo) { 41 | if(!window.controllerInstances[instanceInfo.index]) { 42 | window.controllerInstances[instanceInfo.index] = instanceInfo.controller 43 | this.commit('node_controller_type_instantiated', instanceInfo.type) 44 | } 45 | }, 46 | node_add(state, nodeInfo) { Vue.set(state.nodes, nodeInfo.index, nodeInfo.node) }, 47 | node_remove_all (state) { state.nodes =[] }, 48 | node_remove (state, node) { 49 | state.nodes = Object.keys(state.nodes).reduce((o, c) => { 50 | if(c!=node) o[c] = state.nodes[c] 51 | return o 52 | }, {}) 53 | }, 54 | node_update_controller (state, node) { state.nodes[node.index] = node }, 55 | node_set_index (state, index) { state.currentIndex = index }, 56 | node_set_info (state, info) { state.currentInfo = info }, 57 | node_set_peers (state, peers) { state.currentPeers = peers }, 58 | node_set_console (state, con) { // mdels are mutable and mutate on every keystroke, so use window 59 | window.commandEditor.setModel(con.command) 60 | if(window.consoleStates[state.currentIndex]) window.commandEditor.restoreViewState(window.consoleStates[state.currentIndex].command) 61 | window.resultEditor.setModel(con.result) 62 | if(window.consoleStates[state.currentIndex]) window.resultEditor.restoreViewState(window.consoleStates[state.currentIndex].result) 63 | 64 | }, 65 | node_set_page (state, page) { state.currentPage = page }, 66 | node_set_loading (state, loading) { state.loading = loading }, 67 | console_ready (state, val) { state.consoleReady = val } 68 | } 69 | 70 | export default { 71 | state, 72 | mutations 73 | } -------------------------------------------------------------------------------- /.nodetypes/src/clightning/CLightningPeers.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /.nodetypes/src/lnd/util/parseproto.js: -------------------------------------------------------------------------------- 1 | // parses rpc.proto to something useful for code completion, not used in production 2 | 3 | // TODO: handle map - `map AddrToAmount = 1;` used in sendMany 4 | 5 | const fs = require('fs') 6 | 7 | const proto = fs.readFileSync(`${__dirname}/../rpc.proto`).toString('utf8') 8 | 9 | const lines = proto.split("\n") 10 | 11 | let commands = {} 12 | let messages = {} 13 | let inmsg = false 14 | let inenum = false 15 | let enums = {} 16 | let currentEnum = "" 17 | let currentMsg = "" 18 | let description = "" 19 | let incomment = false 20 | 21 | let currentService = '' 22 | 23 | lines.forEach(l => { 24 | const rpc = l.match(/rpc ([a-zA-Z]+)\s?\((stream )?([a-zA-Z]+)\) returns \((stream )?([a-zA-Z]+)\)/) 25 | if(rpc) { 26 | // TODO: if rpc[2] maybe ? save to quick pick for write ? 27 | const key = rpc[1].slice(0, 1).toLowerCase()+rpc[1].slice(1) 28 | commands[key] = { 29 | request: rpc[3], returns: rpc[5], description: description, 30 | service: currentService 31 | } 32 | if(rpc[4]) commands[key].stream = true 33 | } 34 | 35 | const service = l.match(/^service\s+([a-zA-Z_]+)/) 36 | if(service) currentService = service[1] 37 | 38 | const uncom = l.match(/\*\//) 39 | if(uncom) incomment = false 40 | if(incomment) description += l.trimLeft()+"\n" 41 | 42 | if(inenum) { 43 | const e = l.match(/([A-Z_]+)\s=\s([0-9]+)/) 44 | if(e) 45 | enums[currentEnum][e[1]] = parseInt(e[2], 10) 46 | } 47 | 48 | if(inmsg) { 49 | const comment = l.match(/\/\/\/ (.+)/) 50 | if(comment) description = comment[1] 51 | const field = l.match(/([a-zA-Z0-9_]+) ([a-zA-Z_]+)\s?(=|{)/) 52 | if(field && field[1]!='message' && !incomment && !comment) { 53 | if(field[1]=='enum') { 54 | inenum = true 55 | currentEnum = field[2] 56 | enums[currentEnum] = {} 57 | } else if(field[1] != 'oneof') { 58 | let msgObj = {name: field[2], type: field[1], description: ''+description} 59 | if(currentEnum && msgObj.type == currentEnum) { 60 | msgObj.enum = enums[currentEnum] 61 | } 62 | messages[currentMsg].fields.push(msgObj) 63 | } 64 | description = "" 65 | } 66 | } 67 | 68 | const msg = l.match(/message ([a-zA-Z]+)\s?(\{\})?/) 69 | if(msg) { 70 | if(typeof msg[2]== 'undefined') inmsg = true; else inmsg = false 71 | currentMsg = msg[1] 72 | messages[currentMsg] = {fields:[]} 73 | 74 | } 75 | 76 | const com = l.match(/\/\*\*/) 77 | if(com) {incomment = true; description = ""} 78 | 79 | const brace = l.match(/^{/) 80 | if(brace && inmsg) { 81 | if(inenum) inenum = false 82 | else inmsg = false 83 | } 84 | }) 85 | 86 | Object.keys(commands).forEach(k => { 87 | let c = commands[k] 88 | c.args = messages[c.request].fields 89 | c.args.forEach(a => { 90 | if(messages[a.type]) { 91 | a.args = messages[a.type].fields.filter(f => f.type != 'oneof') 92 | } 93 | }) 94 | c.response = messages[c.returns].fields 95 | 96 | delete c.request; delete c.returns 97 | }) 98 | console.log(JSON.stringify(commands, null, 2)) 99 | -------------------------------------------------------------------------------- /.electron-vue/webpack.web.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'web' 4 | 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | 8 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | 13 | let webConfig = { 14 | devtool: '#cheap-module-eval-source-map', 15 | entry: { 16 | web: path.join(__dirname, '../src/renderer/main.js') 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.css$/, 22 | use: ExtractTextPlugin.extract({ 23 | fallback: 'style-loader', 24 | use: 'css-loader' 25 | }) 26 | }, 27 | { 28 | test: /\.html$/, 29 | use: 'vue-html-loader' 30 | }, 31 | { 32 | test: /\.js$/, 33 | use: 'babel-loader', 34 | include: [ path.resolve(__dirname, '../src/renderer') ], 35 | exclude: /node_modules/ 36 | }, 37 | { 38 | test: /\.vue$/, 39 | use: { 40 | loader: 'vue-loader', 41 | options: { 42 | extractCSS: true, 43 | loaders: { 44 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 45 | scss: 'vue-style-loader!css-loader!sass-loader' 46 | } 47 | } 48 | } 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | use: { 53 | loader: 'url-loader', 54 | query: { 55 | limit: 10000, 56 | name: 'imgs/[name].[ext]' 57 | } 58 | } 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | use: { 63 | loader: 'url-loader', 64 | query: { 65 | limit: 10000, 66 | name: 'fonts/[name].[ext]' 67 | } 68 | } 69 | } 70 | ] 71 | }, 72 | plugins: [ 73 | new ExtractTextPlugin('styles.css'), 74 | new HtmlWebpackPlugin({ 75 | filename: 'index.html', 76 | template: path.resolve(__dirname, '../src/index.ejs'), 77 | minify: { 78 | collapseWhitespace: true, 79 | removeAttributeQuotes: true, 80 | removeComments: true 81 | }, 82 | nodeModules: false 83 | }), 84 | new webpack.DefinePlugin({ 85 | 'process.env.IS_WEB': 'true' 86 | }), 87 | new webpack.HotModuleReplacementPlugin(), 88 | new webpack.NoEmitOnErrorsPlugin() 89 | ], 90 | output: { 91 | filename: '[name].js', 92 | path: path.join(__dirname, '../dist/web') 93 | }, 94 | resolve: { 95 | alias: { 96 | '@': path.join(__dirname, '../src/renderer'), 97 | 'vue$': 'vue/dist/vue.esm.js' 98 | }, 99 | extensions: ['.js', '.vue', '.json', '.css'] 100 | }, 101 | target: 'web' 102 | } 103 | 104 | /** 105 | * Adjust webConfig for production settings 106 | */ 107 | if (process.env.NODE_ENV === 'production') { 108 | webConfig.devtool = '' 109 | 110 | webConfig.plugins.push( 111 | new BabiliWebpackPlugin(), 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.join(__dirname, '../static'), 115 | to: path.join(__dirname, '../dist/web/static'), 116 | ignore: ['.*'] 117 | } 118 | ]), 119 | new webpack.DefinePlugin({ 120 | 'process.env.NODE_ENV': '"production"' 121 | }), 122 | new webpack.LoaderOptionsPlugin({ 123 | minimize: true 124 | }) 125 | ) 126 | } 127 | 128 | module.exports = webConfig 129 | -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { say } = require('cfonts') 6 | const chalk = require('chalk') 7 | const del = require('del') 8 | const { spawn } = require('child_process') 9 | const webpack = require('webpack') 10 | const Multispinner = require('multispinner') 11 | 12 | 13 | const mainConfig = require('./webpack.main.config') 14 | const rendererConfig = require('./webpack.renderer.config') 15 | const webConfig = require('./webpack.web.config') 16 | 17 | const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 18 | const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 19 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 20 | const isCI = process.env.CI || false 21 | 22 | if (process.env.BUILD_TARGET === 'clean') clean() 23 | else if (process.env.BUILD_TARGET === 'web') web() 24 | else build() 25 | 26 | function clean () { 27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) 28 | console.log(`\n${doneLog}\n`) 29 | process.exit() 30 | } 31 | 32 | function build () { 33 | greeting() 34 | 35 | del.sync(['dist/electron/*', '!dist/build_nodetypes']) 36 | 37 | const tasks = ['main', 'renderer'] 38 | const m = new Multispinner(tasks, { 39 | preText: 'building', 40 | postText: 'process' 41 | }) 42 | 43 | let results = '' 44 | 45 | m.on('success', () => { 46 | process.stdout.write('\x1B[2J\x1B[0f') 47 | console.log(`\n\n${results}`) 48 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 49 | process.exit() 50 | }) 51 | 52 | pack(mainConfig).then(result => { 53 | results += result + '\n\n' 54 | m.success('main') 55 | }).catch(err => { 56 | m.error('main') 57 | console.log(`\n ${errorLog}failed to build main process`) 58 | console.error(`\n${err}\n`) 59 | process.exit(1) 60 | }) 61 | 62 | pack(rendererConfig).then(result => { 63 | results += result + '\n\n' 64 | m.success('renderer') 65 | }).catch(err => { 66 | m.error('renderer') 67 | console.log(`\n ${errorLog}failed to build renderer process`) 68 | console.error(`\n${err}\n`) 69 | process.exit(1) 70 | }) 71 | } 72 | 73 | function pack (config) { 74 | return new Promise((resolve, reject) => { 75 | webpack(config, (err, stats) => { 76 | if (err) reject(err.stack || err) 77 | else if (stats.hasErrors()) { 78 | let err = '' 79 | 80 | stats.toString({ 81 | chunks: false, 82 | colors: true 83 | }) 84 | .split(/\r?\n/) 85 | .forEach(line => { 86 | err += ` ${line}\n` 87 | }) 88 | 89 | reject(err) 90 | } else { 91 | // loop here and build default nodetypes 92 | resolve(stats.toString({ 93 | chunks: false, 94 | colors: true 95 | })) 96 | } 97 | }) 98 | }) 99 | } 100 | 101 | function web () { 102 | del.sync(['dist/web/*', '!.gitkeep']) 103 | webpack(webConfig, (err, stats) => { 104 | if (err || stats.hasErrors()) console.log(err) 105 | 106 | console.log(stats.toString({ 107 | chunks: false, 108 | colors: true 109 | })) 110 | 111 | process.exit() 112 | }) 113 | } 114 | 115 | function greeting () { 116 | const cols = process.stdout.columns 117 | let text = '' 118 | 119 | if (cols > 85) text = 'lets-build' 120 | else if (cols > 60) text = 'lets-|build' 121 | else text = false 122 | 123 | if (text && !isCI) { 124 | say(text, { 125 | colors: ['yellow'], 126 | font: 'simple3d', 127 | space: false 128 | }) 129 | } else console.log(chalk.yellow.bold('\n lets-build')) 130 | console.log() 131 | } 132 | -------------------------------------------------------------------------------- /.nodetypes/src/lnd/LndPeers.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 87 | 88 | 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodes-debug", 3 | "productName": "nodesdebug", 4 | "version": "0.1.5", 5 | "author": "Richard Bondi", 6 | "description": "debug multiple nodes(rpc, websocket etc.) of varying types(bitcoin, others to come)", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/rsbondi/nodes-debug" 11 | }, 12 | "main": "./dist/electron/main.js", 13 | "scripts": { 14 | "build": "node .electron-vue/build.js && electron-packager --out=build --asar=0 --overwrite=true .", 15 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 16 | "compile": "cd .nodetypes && node index.js", 17 | "dev": "node .electron-vue/dev-runner.js", 18 | "e2e": "mocha test/e2e", 19 | "pack": "npm run pack:main && npm run pack:renderer", 20 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", 21 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", 22 | "test": "npm run unit && npm run e2e", 23 | "unit": "karma start test/unit/karma.conf.js", 24 | "postinstall": "" 25 | }, 26 | "build": { 27 | "productName": "nodesdebug", 28 | "appId": "net.richardbondi.nodesdebug", 29 | "directories": { 30 | "output": "build" 31 | }, 32 | "files": [ 33 | "dist/electron/**/*" 34 | ], 35 | "dmg": { 36 | "contents": [ 37 | { 38 | "x": 410, 39 | "y": 150, 40 | "type": "link", 41 | "path": "/Applications" 42 | }, 43 | { 44 | "x": 130, 45 | "y": 150, 46 | "type": "file" 47 | } 48 | ] 49 | }, 50 | "mac": { 51 | "icon": "build/icons/icon.icns" 52 | }, 53 | "win": { 54 | "icon": "build/icons/icon.ico" 55 | }, 56 | "linux": { 57 | "icon": "build/icons" 58 | } 59 | }, 60 | "dependencies": { 61 | "axios": "^0.16.1", 62 | "element-ui": "^2.2.1", 63 | "monaco-editor": "^0.10.1", 64 | "require-from-string": "^2.0.2", 65 | "vue": "^2.3.3", 66 | "vue-electron": "^1.0.6", 67 | "vue-monaco": "^0.1.6", 68 | "vueify": "^9.4.1", 69 | "vuex": "^2.3.1" 70 | }, 71 | "devDependencies": { 72 | "babel-core": "^6.25.0", 73 | "babel-loader": "^7.1.1", 74 | "babel-plugin-istanbul": "^4.1.1", 75 | "babel-plugin-transform-runtime": "^6.23.0", 76 | "babel-preset-env": "^1.6.0", 77 | "babel-preset-es2015": "^6.24.1", 78 | "babel-preset-stage-0": "^6.24.1", 79 | "babel-register": "^6.24.1", 80 | "babili-webpack-plugin": "^0.1.2", 81 | "cfonts": "^1.1.3", 82 | "chai": "^4.0.0", 83 | "chalk": "^2.1.0", 84 | "copy-webpack-plugin": "^4.0.1", 85 | "cross-env": "^5.0.5", 86 | "css-loader": "^0.28.4", 87 | "del": "^3.0.0", 88 | "devtron": "^1.4.0", 89 | "electron": "^3.0.16", 90 | "electron-debug": "^1.4.0", 91 | "electron-devtools-installer": "^2.2.0", 92 | "electron-rebuild": "^1.8.2", 93 | "extract-text-webpack-plugin": "^3.0.0", 94 | "file-loader": "^0.11.2", 95 | "html-webpack-plugin": "^2.30.1", 96 | "inject-loader": "^3.0.0", 97 | "karma": "^1.3.0", 98 | "karma-chai": "^0.1.0", 99 | "karma-coverage": "^1.1.1", 100 | "karma-electron": "^5.1.1", 101 | "karma-mocha": "^1.2.0", 102 | "karma-sourcemap-loader": "^0.3.7", 103 | "karma-spec-reporter": "^0.0.31", 104 | "karma-webpack": "^2.0.1", 105 | "mocha": "^3.0.2", 106 | "multispinner": "^0.2.1", 107 | "node-loader": "^0.6.0", 108 | "require-dir": "^0.3.0", 109 | "spectron": "^3.7.1", 110 | "style-loader": "^0.18.2", 111 | "url-loader": "^0.5.9", 112 | "vue-html-loader": "^1.2.4", 113 | "vue-loader": "^13.0.5", 114 | "vue-style-loader": "^3.0.1", 115 | "vue-template-compiler": "^2.4.2", 116 | "webpack": "^3.5.2", 117 | "webpack-dev-server": "^2.7.1", 118 | "webpack-hot-middleware": "^2.18.2", 119 | "webpack-merge": "^4.1.0" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.electron-vue/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'renderer' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 12 | const HtmlWebpackPlugin = require('html-webpack-plugin') 13 | 14 | /** 15 | * List of node_modules to include in webpack bundle 16 | * 17 | * Required for specific packages like Vue UI libraries 18 | * that provide pure *.vue files that need compiling 19 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 20 | */ 21 | let whiteListedModules = ['vue'] 22 | 23 | let rendererConfig = { 24 | devtool: '#cheap-module-eval-source-map', 25 | entry: { 26 | renderer: path.join(__dirname, '../src/renderer/main.js') 27 | }, 28 | externals: [ 29 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)), 30 | /^dist\/build_nodetypes\/.+\.js/ 31 | ], 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.css$/, 36 | use: ExtractTextPlugin.extract({ 37 | fallback: 'style-loader', 38 | use: 'css-loader' 39 | }) 40 | }, 41 | { 42 | test: /\.html$/, 43 | use: 'vue-html-loader' 44 | }, 45 | { 46 | test: /\.js$/, 47 | use: 'babel-loader', 48 | exclude: [/node_modules/,/build_nodetypes/], 49 | }, 50 | { 51 | test: /\.node$/, 52 | use: 'node-loader' 53 | }, 54 | { 55 | test: /\.vue$/, 56 | use: { 57 | loader: 'vue-loader', 58 | options: { 59 | extractCSS: process.env.NODE_ENV === 'production', 60 | loaders: { 61 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 62 | scss: 'vue-style-loader!css-loader!sass-loader' 63 | } 64 | } 65 | } 66 | }, 67 | { 68 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 69 | use: { 70 | loader: 'url-loader', 71 | query: { 72 | limit: 10000, 73 | name: 'imgs/[name]--[folder].[ext]' 74 | } 75 | } 76 | }, 77 | { 78 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 79 | loader: 'url-loader', 80 | options: { 81 | limit: 10000, 82 | name: 'media/[name]--[folder].[ext]' 83 | } 84 | }, 85 | { 86 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 87 | use: { 88 | loader: 'url-loader', 89 | query: { 90 | limit: 10000, 91 | name: 'fonts/[name]--[folder].[ext]' 92 | } 93 | } 94 | } 95 | ] 96 | }, 97 | node: { 98 | __dirname: process.env.NODE_ENV !== 'production', 99 | __filename: process.env.NODE_ENV !== 'production' 100 | }, 101 | plugins: [ 102 | new ExtractTextPlugin('styles.css'), 103 | new HtmlWebpackPlugin({ 104 | filename: 'index.html', 105 | template: path.resolve(__dirname, '../src/index.ejs'), 106 | minify: { 107 | collapseWhitespace: true, 108 | removeAttributeQuotes: true, 109 | removeComments: true 110 | }, 111 | environment: { 112 | production: process.env.NODE_ENV 113 | }, 114 | nodeModules: process.env.NODE_ENV !== 'production' 115 | ? path.resolve(__dirname, '../node_modules') 116 | : false 117 | }), 118 | new webpack.HotModuleReplacementPlugin(), 119 | new webpack.NoEmitOnErrorsPlugin(), 120 | ], 121 | output: { 122 | filename: '[name].js', 123 | libraryTarget: 'commonjs2', 124 | path: path.join(__dirname, '../dist/electron') 125 | }, 126 | resolve: { 127 | alias: { 128 | '@': path.join(__dirname, '../src/renderer'), 129 | 'vue$': 'vue/dist/vue.esm.js' 130 | }, 131 | extensions: ['.js', '.vue', '.json', '.css', '.node'] 132 | }, 133 | target: 'electron-renderer' 134 | } 135 | 136 | /** 137 | * Adjust rendererConfig for development settings 138 | */ 139 | if (process.env.NODE_ENV !== 'production') { 140 | rendererConfig.plugins.push( 141 | new webpack.DefinePlugin({ 142 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 143 | }) 144 | ) 145 | } 146 | 147 | /** 148 | * Adjust rendererConfig for production settings 149 | */ 150 | if (process.env.NODE_ENV === 'production') { 151 | rendererConfig.devtool = '' 152 | 153 | rendererConfig.plugins.push( 154 | new BabiliWebpackPlugin(), 155 | new webpack.DefinePlugin({ 156 | 'process.env.NODE_ENV': '"production"' 157 | }), 158 | new webpack.LoaderOptionsPlugin({ 159 | minimize: true 160 | }), 161 | // new webpack.IgnorePlugin(/build_nodetypes/) 162 | ) 163 | } 164 | 165 | module.exports = rendererConfig 166 | -------------------------------------------------------------------------------- /.electron-vue/dev-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { say } = require('cfonts') 7 | const { spawn } = require('child_process') 8 | const webpack = require('webpack') 9 | const WebpackDevServer = require('webpack-dev-server') 10 | const webpackHotMiddleware = require('webpack-hot-middleware') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | 15 | let electronProcess = null 16 | let manualRestart = false 17 | let hotMiddleware 18 | 19 | function logStats (proc, data) { 20 | let log = '' 21 | 22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 23 | log += '\n\n' 24 | 25 | if (typeof data === 'object') { 26 | data.toString({ 27 | colors: true, 28 | chunks: false 29 | }).split(/\r?\n/).forEach(line => { 30 | log += ' ' + line + '\n' 31 | }) 32 | } else { 33 | log += ` ${data}\n` 34 | } 35 | 36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 37 | 38 | console.log(log) 39 | } 40 | 41 | function startRenderer () { 42 | return new Promise((resolve, reject) => { 43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) 44 | 45 | const compiler = webpack(rendererConfig) 46 | hotMiddleware = webpackHotMiddleware(compiler, { 47 | log: false, 48 | heartbeat: 2500 49 | }) 50 | 51 | compiler.plugin('compilation', compilation => { 52 | compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => { 53 | hotMiddleware.publish({ action: 'reload' }) 54 | cb() 55 | }) 56 | }) 57 | 58 | compiler.plugin('done', stats => { 59 | logStats('Renderer', stats) 60 | }) 61 | 62 | const server = new WebpackDevServer( 63 | compiler, 64 | { 65 | contentBase: path.join(__dirname, '../'), 66 | quiet: true, 67 | headers: { 68 | "Access-Control-Allow-Origin": "*", 69 | "Access-Control-Allow-Methods": "POST, GET, OPTIONS", 70 | }, 71 | 72 | 73 | before (app, ctx) { 74 | app.use(hotMiddleware) 75 | ctx.middleware.waitUntilValid(() => { 76 | resolve() 77 | }) 78 | } 79 | } 80 | ) 81 | server.listen(9080) 82 | }) 83 | } 84 | 85 | function startMain () { 86 | return new Promise((resolve, reject) => { 87 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 88 | 89 | const compiler = webpack(mainConfig) 90 | 91 | compiler.plugin('watch-run', (compilation, done) => { 92 | logStats('Main', chalk.white.bold('compiling...')) 93 | hotMiddleware.publish({ action: 'compiling' }) 94 | done() 95 | }) 96 | 97 | compiler.watch({}, (err, stats) => { 98 | if (err) { 99 | console.log(err) 100 | return 101 | } 102 | 103 | logStats('Main', stats) 104 | 105 | if (electronProcess && electronProcess.kill) { 106 | manualRestart = true 107 | process.kill(electronProcess.pid) 108 | electronProcess = null 109 | startElectron() 110 | 111 | setTimeout(() => { 112 | manualRestart = false 113 | }, 5000) 114 | } 115 | 116 | resolve() 117 | }) 118 | }) 119 | } 120 | 121 | function startElectron () { 122 | electronProcess = spawn(electron, ['--inspect=5858', path.join(__dirname, '../dist/electron/main.js')]) 123 | 124 | electronProcess.stdout.on('data', data => { 125 | electronLog(data, 'blue') 126 | }) 127 | electronProcess.stderr.on('data', data => { 128 | electronLog(data, 'red') 129 | }) 130 | 131 | electronProcess.on('close', () => { 132 | if (!manualRestart) process.exit() 133 | }) 134 | } 135 | 136 | function electronLog (data, color) { 137 | let log = '' 138 | data = data.toString().split(/\r?\n/) 139 | data.forEach(line => { 140 | log += ` ${line}\n` 141 | }) 142 | if (/[0-9A-z]+/.test(log)) { 143 | console.log( 144 | chalk[color].bold('┏ Electron -------------------') + 145 | '\n\n' + 146 | log + 147 | chalk[color].bold('┗ ----------------------------') + 148 | '\n' 149 | ) 150 | } 151 | } 152 | 153 | function greeting () { 154 | const cols = process.stdout.columns 155 | let text = '' 156 | 157 | if (cols > 104) text = 'electron-vue' 158 | else if (cols > 76) text = 'electron-|vue' 159 | else text = false 160 | 161 | if (text) { 162 | say(text, { 163 | colors: ['yellow'], 164 | font: 'simple3d', 165 | space: false 166 | }) 167 | } else console.log(chalk.yellow.bold('\n electron-vue')) 168 | console.log(chalk.blue(' getting ready...') + '\n') 169 | } 170 | 171 | function init () { 172 | greeting() 173 | 174 | Promise.all([startRenderer(), startMain()]) 175 | .then(() => { 176 | startElectron() 177 | }) 178 | .catch(err => { 179 | console.error(err) 180 | }) 181 | } 182 | 183 | init() 184 | -------------------------------------------------------------------------------- /.nodetypes/src/btcd/BtcdPeers.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 140 | -------------------------------------------------------------------------------- /.nodetypes/src/bitcoin/BitcoinPeers.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 140 | -------------------------------------------------------------------------------- /.nodetypes/src/btcd/BtcdController.js: -------------------------------------------------------------------------------- 1 | const { BitcoinController } = require('../bitcoin/BitcoinController') 2 | 3 | class BtcdController extends BitcoinController { 4 | static register(editor, resultEditor, store) { 5 | if(window.controllerInstances[store.state.Nodes.currentIndex]._notls) 6 | return super.register(editor, resultEditor, store) 7 | 8 | return new Promise((resolve, reject) => { 9 | BtcdController.registerInfo = { 10 | resolve: resolve, reject: reject, editor: editor, resultEditor: resultEditor, store: store} 11 | }) 12 | } 13 | 14 | static _register() { 15 | const inf = BtcdController.registerInfo 16 | if(!inf) return 17 | super.register(inf.editor, inf.resultEditor, inf.store).then(r => { 18 | BtcdController.emitter.emit('controller-ready') 19 | inf.resolve() 20 | }) 21 | } 22 | 23 | _interval() { 24 | const self = this 25 | return new Promise((resolve, reject) => { 26 | function doInterval() { 27 | if(BtcdController.registered) 28 | Promise.all([ 29 | self._getBlock(), 30 | self._getMempool(), 31 | self._getNetInfo(), 32 | //self._getBanned(), 33 | self._getPeerInfo()] 34 | ).then(() => { 35 | self._infoTime = new Date().getTime() 36 | resolve(Object.assign({}, self._info)) // assign to isolate from store 37 | }).catch(reject) 38 | else 39 | setTimeout(doInterval, 100) 40 | } 41 | doInterval() 42 | }) 43 | } 44 | 45 | getPeers() { 46 | return new Promise((resolve, reject) => { 47 | Promise.all( 48 | [this._postRPC({ method: 'getpeerinfo' }) 49 | 50 | ] 51 | ).then((arr) => resolve({ peers: arr[0].data.result})) 52 | .catch(reject) 53 | }) 54 | } 55 | 56 | _getNetInfo() { 57 | const self = this 58 | return new Promise(async (resolve, reject) => { 59 | try { 60 | const js = await self._postRPC({ 61 | method: "getinfo" 62 | }) 63 | try { 64 | 65 | this._info.version = js.data.result.version 66 | this._info.subversion = js.data.result.protocolversion 67 | resolve() 68 | } catch(wtf) {resolve('network error')} 69 | } catch (e) { resolve() } 70 | }) 71 | 72 | } 73 | 74 | _handleNotification (text) { 75 | const model = this.constructor.models[this.id] 76 | if(!model) return 77 | const lineCount = model.result.getLineCount(); 78 | const lastLineLength = model.result.getLineMaxColumn(lineCount); 79 | 80 | const range = new monaco.Range(lineCount, lastLineLength, lineCount, lastLineLength); 81 | 82 | model.result.pushEditOperations([new monaco.Selection(1, 1, 1, 1)], 83 | [{ range: range, text: "/* NOTIFICATION */\n"+text }], 84 | () => [new monaco.Selection(model.result.getLineCount(),0,model.result.getLineCount(),0)]) 85 | if(this.constructor._store.state.Nodes.currentIndex == this.id) 86 | this.constructor.resultEditor.revealPosition({ lineNumber: this.constructor.resultEditor.getModel().getLineCount(), column: 0 }) 87 | } 88 | 89 | update(cfg) { 90 | fs = require('fs') 91 | const os = require('os') 92 | this._host = cfg && cfg.host || '127.0.0.1' 93 | this._info = {} 94 | this._infoTime = 0 95 | this._notls = 0 96 | this.id = cfg.index 97 | const config = fs.readFileSync(cfg && cfg.config.replace('~', os.homedir()) || `${os.homedir()}/.btcd/btcd.conf`, 'utf8'); 98 | let rpcport 99 | config.split('\n').forEach(line => { 100 | let rpcuser = line.match(/^\s?rpcuser\s?=\s?([^#]+)$/) 101 | if (rpcuser) this._user = rpcuser[1] 102 | let rpcpass = line.match(/^\s?rpcpass\s?=\s?([^#]+)$/) 103 | if (rpcpass) this._password = rpcpass[1] 104 | let port = line.match(/^\s?rpcport\s?=\s?([^#]+)$/) 105 | if (port) rpcport = port[1] 106 | let notls = line.match(/^\s?notls\s?=\s?([^#]+)$/) 107 | if (notls) this._notls = notls[1] 108 | }) 109 | this._port = cfg && cfg.port || rpcport || '8332' 110 | 111 | if(!this._notls) { 112 | var fs = require('fs'); 113 | var cert = fs.readFileSync(`${os.homedir()}/.btcd/rpc.cert`); 114 | var WebSocket = require('ws'); 115 | let self = this 116 | 117 | function setupWebsocket() { 118 | 119 | const ws = new WebSocket(`wss://${self._host}:${self._port}/ws`, { 120 | headers: { 121 | 'Authorization': 'Basic '+new Buffer.from(`${self._user}:${self._password}`).toString('base64') 122 | }, 123 | cert: cert, 124 | ca: [cert] 125 | }); 126 | ws.on('open', () => { 127 | if(!BtcdController.registered) { 128 | BtcdController._register() 129 | BtcdController.registered = true 130 | } 131 | }); 132 | 133 | function _resolve(key, obj) { 134 | self.wspromises[key].resolve({data: obj}) 135 | self.wspromises[key] = undefined 136 | } 137 | ws.on('message', (data, flags) => { 138 | const obj = JSON.parse(data) 139 | const key = obj.id 140 | if(self.wspromises && self.wspromises[key]) { 141 | _resolve(key, obj) 142 | } else if(self.constructor.resultEditor) { 143 | self._handleNotification(JSON.stringify(JSON.parse(data), null, 2)+"\n\n") 144 | } 145 | }); 146 | ws.on('error', (derp) => { 147 | console.log('ERROR:' + derp); 148 | }) 149 | ws.on('close', (data) => { 150 | console.log('DISCONNECTED'); 151 | setTimeout(setupWebsocket, 5000) 152 | }) 153 | 154 | self._ws = ws 155 | self.wspromises = {} 156 | } 157 | 158 | setupWebsocket() 159 | } 160 | 161 | } 162 | 163 | _postRPC(payload) { 164 | if(this._notls) return super._postRPC(payload) 165 | else { 166 | var self = this 167 | 168 | function promiseFunction(resolve, reject) { 169 | payload.jsonrpc = "1.0" 170 | payload.params = payload.params || [] 171 | payload.id = payload.method 172 | self._ws.send(JSON.stringify(payload)) 173 | self.wspromises[payload.method] = {resolve: resolve, reject: reject} 174 | setTimeout(() => { 175 | if(self.wspromises[payload.method]) { 176 | self.wspromises[payload.method].reject('CONNECTION ERROR') 177 | self.wspromises[payload.method] = undefined 178 | self.online = false 179 | } 180 | }, 5000) 181 | } 182 | 183 | let promise = new Promise(promiseFunction) 184 | .then(d => { this.online = true; return d}) 185 | .catch(e => { 186 | self.online = false 187 | self.wspromises[payload.method] = undefined 188 | return e.response 189 | }) 190 | 191 | return promise 192 | } 193 | } 194 | } 195 | 196 | BtcdController.lang = 'btcd-rpc' 197 | const EventEmitter = require('events'); 198 | class MyEmitter extends EventEmitter {} 199 | BtcdController.emitter = new MyEmitter() 200 | 201 | module.exports = { 202 | type: 'btcd', 203 | controller: BtcdController 204 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodes-debug 2 | 3 | ## Description 4 | 5 | This is intended to be a general purpose tool to aid in the development of bitcoin or other similar applications(currently only bitcoin core, btcd lnd and clightning supported). 6 | 7 | It provides a way to interact with multiple rpc nodes. 8 | 9 | The integrated console lets you talk to multiple nodes from a single interface, the commands feature code completion, signature help, syntax highlighting and folding. Also, help is provided on hover for all commands. Command results provide folding for better focus and insertion of result parameters into successive commands. 10 | 11 | The project is designed such that additional node types can be added in the future. 12 | 13 | #### Install - linux-x64 14 | 15 | [download](https://moonbreeze.richardbondi.net/nodesdebug-0.1.5-linux-x64.zip), unzip and add to path. 16 | 17 | [verify signature](https://moonbreeze.richardbondi.net/SHA256SUM.asc) 18 | 19 | #### Build Setup 20 | 21 | ``` bash 22 | # install dependencies 23 | npm install 24 | 25 | # compile all node types, run once before first run and after edits to files under .nodetypes 26 | npm run compile 27 | 28 | # optionally compile selective nodetypes 29 | # npm run compile -- [list of node types space separated] 30 | # ex. npm run compile -- btcd lightning 31 | 32 | # serve with hot reload at localhost:9080 33 | npm run dev 34 | 35 | # build electron application for production 36 | npm install -g electron-packager 37 | npm run build 38 | 39 | # to launch built version, add to path 40 | nodesdebug [config=path/to/config] 41 | # config is optional, normally when launched it will load that last saved or 42 | # loaded configuration, the option in commandline will override allowing you to 43 | # specify. 44 | 45 | # run unit & end-to-end tests (not yet implemented) 46 | npm test 47 | 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### Configuring Nodes 53 | 54 | Nodes are configured via the UI and can be saved and retrieved for running multiple configurations. The configurations are saved as JSON so nodes can be manually configured also if desired. When the app starts, it will load last loaded configuration automatically. 55 | 56 | #### Create nodes using the UI 57 | 58 | To add a node to the current configuration, click the + icon to the upper right of the interface. A dialog will appear 59 | 60 | ![dialog](image/dialog.png) 61 | 62 | All parameters are optional, defaults will be used, **Name** is recommended as it appears in the tab, also **port**(or **config** if `rpcport` is set there) since the default is mainnet 63 | 64 | * **Node Type**: The type of the node (bitcoin/btcd/third party) 65 | * **Name**: What you want to call it, this will appear it it's tab 66 | * **Host**: Node's IP address 67 | * **Port**: The RPC port of the node 68 | * **Config**: Path to configuration file, this is where the node's RPC authentication information is located in format of standard `bitcoin.conf`(btcd.conf, lnd.conf or format of third party's choosing) 69 | 70 | After one or more nodes are added, you can save the configuration by selecting form the menu `Config -> Save` 71 | 72 | #### Manually creating 73 | Saving from the UI will create a JSON file like this, you can create manually and load rather than using the UI if preferred. 74 | 75 | ``` JSON 76 | [ 77 | { 78 | "name": "Alice", 79 | "type": "bitcoin", 80 | "port": "18654", 81 | "host": "127.0.0.1", 82 | "config": "~/regtest/alice/bitcoin.conf", 83 | "index": "n1521588alice" 84 | }, 85 | { 86 | "name": "Bob", 87 | "type": "bitcoin", 88 | "port": "18554", 89 | "host": "127.0.0.1", 90 | "config": "~/regtest/bob/bitcoin.conf", 91 | "index": "n15bob" 92 | } 93 | ] 94 | ``` 95 | 96 | The index must be unique, the UI will use the timestamp but this can be anything. 97 | 98 | To load saved or manually created configuration, from the menu choose `Config -> Load` 99 | 100 | ### Using the console 101 | 102 | Commands are entered in the left pane and when executed results display in the right. 103 | 104 | For the default node type of 'bitcion', a single command may be multi line. Leave whitespace for argument separation, JSON objects are parsed as a single argument. 105 | 106 | example: 107 | 108 | ``` 109 | addmultisigaddress 2 110 | [ 111 | "key1", 112 | "key2" 113 | ] 114 | ``` 115 | 116 | ### Keyboard Shortcuts 117 | | Command | Description | 118 | | ------- | ----------- | 119 | | F5 | execute command at command editor cursor postion | 120 | | CtrlCmd+r | reverse bytes or change endian of selection | 121 | | CtrlCmd+i | Insert from result cursor position to command editior selection, this allows easy reuse of results as command arguments 122 | | Alt+r | Insert last result(`result` field in JSON response, string only) | 123 | 124 | #### Using with lnd 125 | 126 | Currently streaming subscriptions are supported, but streaming requests are not yet supported, you can use the "Sync" version instead for now, `openChannelSync`, `sendPaymentSync`, and `sendToRouteSync` respectively. 127 | 128 | Also, the convention for the console is differnet here. All parameters are passed as json objects in key/value format since the grpc interface differs from conventional and there is no positional relationship, all parameters are sent by name. 129 | 130 | ### Features example, light theme 131 | 132 | ![bob and alice](image/alice_pay_bob.gif) 133 | Commands are executed by pressing F5, pressing the codelens helper or from the context menu. 134 | 135 | ### Notification dark theme (BTCD) 136 | 137 | ![dialog](image/notify.gif) 138 | 139 | # Development 140 | 141 | Notice, developing third party node types is encouraged, but please check with me before doing so, the architecture is likely to change as more insight is gained in development. 142 | 143 | This utility supports multiple node types. It ships with `bitcoin`(bitcoin core) and `btcd` for bitcoin and `lnd` and `clightning` for the lightning network. The node types are defined in the `.nodetypes` directory. To create a node of a different type, follow the example in the `bitcoin` directory. The `${your_node_type}Controller.js` file needs to implement the methods that do not begin with an underscore. For questions feel free contact me directly. 144 | 145 | Also, if your module uses external packages that are not in the root `package.json` file of this repository, you will need to include your own `package.json` file, you only need to include packages that are not already covered in the root file. See `.nodetypes/src/btcd/package.json` as an example, which uses `ws` package. 146 | 147 | Probably a good module development strategy would be to fork this repository, create your own branch and build and test there. When satisfied, you can release just the source and the build to your own repo. Changes made during development in this directory need to be compiled with `npm run compile`. This will copy all javascript files and compile the `.vue` files to the `dist/build_nodetypes` directory. Also, it will run `npm install` on the target directory if additional dependencies are included, this only runs if the `package-lock.json` is not found. If you add dependencies during development, you will need to remove the `package-lock.json` file to trigger this action again. 148 | 149 | So for example, if you want to create for ethereum, add the `ethereum` directory to the `.nodetypes/src` directory of the branch on your fork. Add files for the console and peers, allong with the controller file. So the directory should look something like `EthereumController.js`, `EthereumInfo.vue` and `EthereumPeers.vue`. The controller must end in Controller.js, for the others the naming is not important but best to stick to the convention. The info and peers files are flexible as to how you want to display the data, just so it coordinates with the controller. 150 | 151 | To add a third pary node type to an existing installation, add the subdirectory to `resources/app/dist/build_nodetypes` in the packaged app. 152 | 153 | # Disclaimer 154 | This is meant as a development tool and suggested to use on testnet or regtest, use on mainnet at your own risk. 155 | 156 | --- 157 | 158 | This project was generated with [electron-vue](https://github.com/SimulatedGREG/electron-vue) using [vue-cli](https://github.com/vuejs/vue-cli). Documentation about the original structure can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/index.html). 159 | -------------------------------------------------------------------------------- /src/renderer/components/Console.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 248 | 249 | -------------------------------------------------------------------------------- /.nodetypes/src/clightning/CLightningController.js: -------------------------------------------------------------------------------- 1 | const { BitcoinController } = require('../bitcoin/BitcoinController') 2 | const net = require('net'); 3 | 4 | class CLightningController extends BitcoinController { 5 | static register(editor, resultEditor, store) { 6 | return new Promise((resolve, reject) => { 7 | CLightningController.registerInfo = { 8 | resolve: resolve, reject: reject, editor: editor, resultEditor: resultEditor, store: store} 9 | }) 10 | } 11 | 12 | static _register() { 13 | const inf = CLightningController.registerInfo 14 | if(!inf) return 15 | super.register(inf.editor, inf.resultEditor, inf.store).then(r => { 16 | CLightningController.emitter.emit('controller-ready') 17 | inf.resolve() 18 | }) 19 | } 20 | 21 | static _setHelpers(response) { 22 | this._helpers = response.data.result.help.reduce((o, c, i) => { 23 | const pieces = c.command.split(' ') 24 | o.push({ command: pieces[0], help: pieces.length > 1 ? pieces.slice(1).join(' ') : '' , description: c.description}) 25 | return o 26 | }, []) 27 | } 28 | 29 | static _setHoverHelp() { 30 | monaco.languages.registerHoverProvider(this.lang, { 31 | provideHover: (model, position) => { 32 | let word = '' 33 | const wordAtPos = model.getWordAtPosition(position) 34 | if (wordAtPos) word = wordAtPos.word 35 | 36 | if (word && ~this._helpers.map(h => h.command).indexOf(word)) { 37 | return { 38 | contents: [ 39 | `**${word}**`, 40 | { language: 'text', value: this._helpers.filter(h => h.command == word)[0].description } 41 | ] 42 | } 43 | } 44 | } 45 | }); 46 | } 47 | 48 | static _setSignatureHelp() { 49 | monaco.languages.registerSignatureHelpProvider(this.lang, { 50 | provideSignatureHelp: (model, position) => { 51 | const getBlockIndex = (block) => { 52 | let index = -1 53 | let lineindex = block.reduce((o, c, i) => c.offset === 0 ? i : o, -1) 54 | const tokens = monaco.editor.tokenize(block.map(b => b.text).join('\n'), this.lang) 55 | let brackets = [] 56 | for (let i = 0; i <= lineindex; i++) { 57 | const token = tokens[i] 58 | token.forEach((t, ti) => { 59 | const prevToken = ti === 0 ? i === 0 ? null : tokens[i - 1][tokens[i - 1].length - 1] : token[ti - 1] 60 | switch (t.type) { 61 | case `white.${this.lang}`: 62 | if (prevToken.type == `keyword.${this.lang}`) index = 0 63 | if (~[`number.${this.lang}`, `string.${this.lang}`, `identifier.${this.lang}`].indexOf(prevToken.type) && !brackets.length) index++ 64 | break 65 | case `bracket.square.open.${this.lang}`: 66 | brackets.unshift('square') 67 | break 68 | case `bracket.square.close.${this.lang}`: 69 | brackets.shift('square') 70 | index++ 71 | break 72 | 73 | } 74 | }); 75 | } 76 | return index 77 | } 78 | 79 | const block = this._getCommandBlock(model, position) 80 | let word = '' 81 | if (block.length) word = block[0].text.split(' ')[0] 82 | 83 | 84 | if (word) { 85 | if (block.length) word = block[0].text.split(' ')[0] 86 | const index = getBlockIndex(block, position.column) 87 | 88 | const helpItem = this._helpers.filter(h => h.command == word) 89 | const helpParams = helpItem.length && helpItem[0].help.split(' ') || [] 90 | const params = helpParams.map(p => { return {label: p}}) 91 | if (index > -1 && index < params.length && helpItem.length) 92 | return { 93 | activeSignature: 0, 94 | activeParameter: index, 95 | signatures: [ 96 | { 97 | label: `${helpItem[0].command} ${helpItem[0].help}`, 98 | parameters: params 99 | } 100 | ] 101 | } 102 | else return {} 103 | 104 | } 105 | 106 | else return {} 107 | 108 | }, 109 | signatureHelpTriggerCharacters: [' ', '\t', '\n'] 110 | }) 111 | } 112 | 113 | _interval() { 114 | return new Promise(async (resolve, reject) => { 115 | try { 116 | const inf = await this._postRPC({"method": "getinfo"}) 117 | this._info = inf.data.result 118 | const funds = await this._postRPC({"method": "listfunds"}) 119 | this._info = Object.assign(this._info, funds.data.result) 120 | resolve(this._info) 121 | } catch(e) {reject(e)} 122 | }) 123 | } 124 | 125 | getPeers() { 126 | if(!this._aliases) this._aliases = {} 127 | 128 | return new Promise(async (resolve, reject) => { 129 | try { 130 | const p = await this._postRPC({"method": "listpeers"}) 131 | const peers = p.data.result 132 | const chan = await this._postRPC({"method": "listfunds"}) 133 | for(let i=0; i [new monaco.Selection(model.result.getLineCount(),0,model.result.getLineCount(),0)]) 163 | if(this.constructor._store.state.Nodes.currentIndex == this.id) 164 | this.constructor.resultEditor.revealPosition({ lineNumber: this.constructor.resultEditor.getModel().getLineCount(), column: 0 }) 165 | } 166 | 167 | update(cfg) { 168 | const fs = require('fs') 169 | const os = require('os') 170 | this._info = {} 171 | this._infoTime = 0 172 | this.id = cfg.index 173 | let rpcFile, lightningDir 174 | let config 175 | 176 | try { 177 | config = fs.readFileSync(cfg && cfg.config.replace('~', os.homedir()) || `${os.homedir()}/.lightning/config`, 'utf8'); 178 | } catch (e) {config = ''} 179 | config.split('\n').forEach(line => { 180 | let rpcfile = line.match(/^\s?rpc-file\s?=\s?([^#]+)$/) 181 | if (rpcfile) rpcFile = rpcfile[1] 182 | let ldir = line.match(/^\s?lightning-dir\s?=\s?([^#]+)$/) 183 | if (ldir) lightningDir = ldir[1] 184 | }) 185 | 186 | if(!rpcFile) rpcFile = `lightning-rpc` 187 | if(!lightningDir) lightningDir = `${os.homedir()}/.lightning` 188 | rpcFile =`${lightningDir}/${rpcFile}` 189 | 190 | const setupSocket = () => { 191 | 192 | const sock = new net.createConnection(rpcFile); 193 | sock.on('connect', () => { 194 | if(!CLightningController.registered) { 195 | CLightningController._register() 196 | CLightningController.registered = true 197 | } 198 | }); 199 | 200 | var _resolve = (key, obj) => { 201 | this.sockpromises[key].resolve({data: obj}) 202 | this.sockpromises[key] = undefined 203 | } 204 | 205 | let jsonBuild = "" 206 | sock.on('data', (data) => { 207 | const response = typeof data == 'string' ? data : data.toString('utf8') 208 | jsonBuild += response 209 | if (jsonBuild.slice(-2) === "\n\n") { 210 | const obj = JSON.parse(jsonBuild) 211 | const key = obj.id 212 | if(this.sockpromises && this.sockpromises[key]) { 213 | _resolve(key, obj) 214 | } else if(this.constructor.resultEditor) { 215 | this._handleNotification(JSON.stringify(JSON.parse(data), null, 2)+"\n\n") 216 | } 217 | jsonBuild = "" 218 | } 219 | }); 220 | sock.on('error', (derp) => { 221 | jsonBuild = "" 222 | console.log('ERROR:' + derp); 223 | }) 224 | sock.on('close', (data) => { 225 | console.log('DISCONNECTED'); 226 | setTimeout(setupSocket, 5000) 227 | }) 228 | 229 | this._sock = sock 230 | this.sockpromises = {} 231 | } 232 | 233 | setupSocket() 234 | 235 | } 236 | 237 | _postRPC(payload) { 238 | const promiseFunction = (resolve, reject) => { 239 | payload.jsonrpc = "2.0" 240 | payload.params = payload.params || [] 241 | payload.id = payload.method 242 | this._sock.write(JSON.stringify(payload)) 243 | this.sockpromises[payload.method] = {resolve: resolve, reject: reject} 244 | setTimeout(() => { 245 | if(this.sockpromises[payload.method]) { 246 | this.sockpromises[payload.method].reject('CONNECTION ERROR') 247 | this.sockpromises[payload.method] = undefined 248 | this.online = false 249 | } 250 | }, 5000) 251 | } 252 | 253 | let promise = new Promise(promiseFunction) 254 | .then(d => { this.online = true; return d}) 255 | .catch(e => { 256 | this.online = false 257 | this.sockpromises[payload.method] = undefined 258 | return e.response 259 | }) 260 | 261 | return promise 262 | } 263 | } 264 | 265 | CLightningController.lang = 'clightning-rpc' 266 | const EventEmitter = require('events'); 267 | class MyEmitter extends EventEmitter {} 268 | CLightningController.emitter = new MyEmitter() 269 | 270 | module.exports = { 271 | type: 'clightning', 272 | controller: CLightningController 273 | } -------------------------------------------------------------------------------- /.nodetypes/src/lnd/LndController.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const Config = require('./config') 3 | const grpc = require('grpc'); 4 | const protoLoader = require('@grpc/proto-loader') 5 | //process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA' 6 | 7 | process.env.GRPC_SSL_CIPHER_SUITES = 8 | 'ECDHE-RSA-AES128-GCM-SHA256:' + 9 | 'ECDHE-RSA-AES128-SHA256:' + 10 | 'ECDHE-RSA-AES256-SHA384:' + 11 | 'ECDHE-RSA-AES256-GCM-SHA384:' + 12 | 'ECDHE-ECDSA-AES128-GCM-SHA256:' + 13 | 'ECDHE-ECDSA-AES128-SHA256:' + 14 | 'ECDHE-ECDSA-AES256-SHA384:' + 15 | 'ECDHE-ECDSA-AES256-GCM-SHA384'; 16 | 17 | const MonacoHandler = require('./monaco') 18 | 19 | class LndController { 20 | constructor(cfg) { 21 | this.update(cfg) 22 | this.noservice = false 23 | } 24 | 25 | static register(editor, resultEditor, store) { 26 | this._store = store 27 | let handler = MonacoHandler.register(editor, resultEditor, store, this.lang) 28 | handler.then(() => this.registered = true) 29 | return handler 30 | } 31 | 32 | getInfo() { 33 | return new Promise((resolve, reject) => { 34 | const postit = () => { 35 | this._postRPC('getInfo').then(inf => { 36 | this._info = inf 37 | this._postRPC('walletBalance').then(infw => { 38 | this._info = Object.assign(this._info, infw) 39 | resolve(this._info) 40 | }) 41 | }).catch(reject) 42 | } 43 | if(!this.constructor.registered) setTimeout(postit, 500); else postit() 44 | }) 45 | } 46 | 47 | getPeers() { 48 | return new Promise((resolve, reject) => { 49 | const postit = () => { 50 | this._postRPC('listPeers').then(peers => { 51 | this._postRPC('listChannels').then(chans => { 52 | resolve({peers, chans}) 53 | }) 54 | }).catch(reject) 55 | } 56 | if(!this.constructor.registered) setTimeout(postit, 500); else postit() 57 | }) 58 | } 59 | 60 | update(cfg) { 61 | this.id = cfg.index 62 | this._config = new Config(cfg) 63 | this._info = {} 64 | 65 | const setupGrpc = () => { 66 | this.instance = this._createInstance() 67 | } 68 | 69 | setupGrpc() 70 | 71 | } 72 | 73 | getConsole() { 74 | return new Promise((resolve, reject) => { 75 | if(!MonacoHandler.models[this.id]) this._createConsole() 76 | resolve(MonacoHandler.models[this.id]) 77 | }) 78 | } 79 | 80 | ping() { return this._postRPC('getInfo')} 81 | 82 | execute(ed) { 83 | if(this.noservice) { 84 | this.instance = this._createInstance() 85 | setTimeout(() => { 86 | this.execute(ed) 87 | }, 1000) 88 | return 89 | } 90 | const val = this.constructor._getCommandBlock(ed.getModel(), ed.getPosition()).map(b => b.text).join(' ') 91 | let chunks = val.split(/\s/) 92 | const method = chunks[0] 93 | if(!~Object.keys(MonacoHandler._commands).indexOf(method)) { 94 | this.constructor._appendToEditor(`unknown method, ${method}`) 95 | return 96 | } 97 | const service = MonacoHandler._commands[method].service 98 | let params 99 | try { 100 | params = chunks.length > 1 ? JSON.parse(val.slice(chunks[0].length)) : {} 101 | } catch (e) { 102 | const checkEnum = MonacoHandler._commands[method] 103 | const enums = checkEnum.args.filter(a => a.enum) 104 | let commandString = val.slice(chunks[0].length) 105 | enums.forEach(en => { 106 | Object.keys(en.enum).forEach(k => { 107 | commandString = commandString.replace(k, en.enum[k]) 108 | }) 109 | }) 110 | try {params = JSON.parse(commandString)} catch(ohwell) {} 111 | if(!params) { 112 | this.constructor._appendToEditor(e+"\n") 113 | return 114 | } 115 | } 116 | 117 | this._postRPC(method, params, service).then(response => { 118 | let content = '// '+method+' '+(params ? JSON.stringify(params):'') + '\n' 119 | content += JSON.stringify(response, null, 2) + '\n\n' 120 | this.constructor._appendToEditor(content) 121 | }).catch(err => this.constructor._appendToEditor(err)) 122 | return null; 123 | } 124 | 125 | _createConsole() { 126 | MonacoHandler.models[this.id] = { 127 | command: monaco.editor.createModel('', this.constructor.lang), 128 | result: monaco.editor.createModel('', 'javascript') 129 | } 130 | } 131 | 132 | _createInstance() { 133 | const m = fs.readFileSync(this._config.macaroonPath); 134 | const macaroon = m.toString('hex'); 135 | 136 | let metadata = new grpc.Metadata() 137 | metadata.add('macaroon', macaroon) 138 | const macaroonCreds = grpc.credentials.createFromMetadataGenerator((_args, callback) => { 139 | callback(null, metadata); 140 | }); 141 | 142 | const lndCert = fs.readFileSync(this._config.certPath); 143 | const sslCreds = grpc.credentials.createSsl(lndCert); 144 | 145 | const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds); 146 | 147 | const packageDefinition = protoLoader.loadSync(`${__dirname}/rpc.proto`,{keepCase: true, 148 | longs: String, 149 | enums: String, 150 | defaults: true, 151 | oneofs: true 152 | }); 153 | 154 | let lnrpcDescriptor = grpc.loadPackageDefinition(packageDefinition); 155 | 156 | const lnrpc = lnrpcDescriptor.lnrpc; 157 | const instance = new lnrpc.Lightning(`${this._config.host}:${this._config.port}`, credentials); 158 | 159 | lnrpcDescriptor = grpc.loadPackageDefinition(packageDefinition); 160 | const wlnrpc = lnrpcDescriptor.lnrpc; 161 | const winstance = new wlnrpc.WalletUnlocker(`${this._config.host}:${this._config.port}`, sslCreds); 162 | 163 | this.noservice = false 164 | return {Lightning: instance, WalletUnlocker: winstance} 165 | 166 | } 167 | 168 | _postRPC(method, options, service) { 169 | if(!service) service='Lightning' 170 | let encoding = service == 'Lightning' ? 'hex': 'utf8' // TODO: sign/verifyMessage utf8 171 | var promiseFunction = (resolve, reject) => { 172 | let opts = Object.assign({}, options || {}) 173 | Object.keys(opts).forEach(k => { 174 | const opt = MonacoHandler._commands[method].args.filter(a => a.name == k) 175 | if(opt.length && opt[0].type == 'bytes') opts[k] = Buffer.from(opts[k], encoding) 176 | }) 177 | try { 178 | if(MonacoHandler._commands[method].stream) { 179 | var call = this.instance[service][method](opts) 180 | const callstr = '// '+method+' '+(opts ? JSON.stringify(opts):'') 181 | this.constructor._appendToEditor(callstr + '\n\n') 182 | call.on('data', (response) => { 183 | this._handleNotification(`${callstr} STREAM RESPONSE\n${JSON.stringify(response,null,2)}\n\n`) 184 | }); 185 | call.on('status', (status) => { 186 | console.log('status', status) 187 | this._handleNotification(`${callstr} STREAM RESPONSE\n${JSON.stringify(status,null,2)}\n\n`) 188 | }); 189 | call.on('end', () => { 190 | // The server has closed the stream. 191 | }); 192 | } else { 193 | this.instance[service][method](opts, (err, result) => { 194 | if (err) { 195 | 196 | if(err.details == "unknown service lnrpc.Lightning") { 197 | this.noservice = true 198 | } 199 | reject(err) 200 | } 201 | else resolve(result) 202 | }); 203 | } 204 | } catch(e) {reject(e.message)} 205 | } 206 | 207 | let promise = new Promise(promiseFunction) 208 | .then(d => { this.online = true; return d}) 209 | .catch(e => { 210 | this.online = false 211 | return e 212 | }) 213 | 214 | return promise 215 | 216 | 217 | } 218 | 219 | static _getCommandBlock (model, position) { 220 | let line = position.lineNumber, wordAtPos, word = '' 221 | let block = model.getLineContent(line) ? [] : [{text:''}] // keep block alive on enter 222 | let tmpline 223 | while(tmpline = model.getLineContent(line)) { 224 | wordAtPos = model.getWordAtPosition({lineNumber: line, column: 1}) 225 | block.unshift({text: model.getLineContent(line), offset: line - position.lineNumber}) 226 | if(wordAtPos) word = wordAtPos.word 227 | if(word) { 228 | if(~Object.keys(MonacoHandler._commands).indexOf(word)) break; 229 | } 230 | line-- 231 | if(line===0) break 232 | } 233 | line = position.lineNumber + 1 234 | if(line > model.getLineCount()) return block 235 | while(tmpline = model.getLineContent(line)) { 236 | wordAtPos = model.getWordAtPosition({lineNumber: line, column: 1}) 237 | if(wordAtPos && ~Object.keys(MonacoHandler._commands).indexOf(wordAtPos.word)) break; 238 | tmpline = tmpline.replace(/^\s+/,'') 239 | if(!tmpline) break; 240 | block.push({text: model.getLineContent(line), offset: line - position.lineNumber}) 241 | line++ 242 | if(line > model.getLineCount()) break 243 | } 244 | return block 245 | } 246 | 247 | static _appendToEditor (text) { 248 | const lineCount = MonacoHandler.resultEditor.getModel().getLineCount(); 249 | const lastLineLength = MonacoHandler.resultEditor.getModel().getLineMaxColumn(lineCount); 250 | 251 | const range = new monaco.Range(lineCount, lastLineLength, lineCount, lastLineLength); 252 | 253 | MonacoHandler.resultEditor.updateOptions({ readOnly: false }) 254 | MonacoHandler.resultEditor.executeEdits('', [ 255 | { range: range, text: text } 256 | ]) 257 | MonacoHandler.resultEditor.updateOptions({ readOnly: true }) 258 | MonacoHandler.resultEditor.setSelection(new monaco.Range(1, 1, 1, 1)) 259 | MonacoHandler.resultEditor.revealPosition({ lineNumber: MonacoHandler.resultEditor.getModel().getLineCount(), column: 0 }) 260 | 261 | } 262 | 263 | _handleNotification (text) { 264 | const model = MonacoHandler.models[this.id] 265 | if(!model) return 266 | const lineCount = model.result.getLineCount(); 267 | const lastLineLength = model.result.getLineMaxColumn(lineCount); 268 | 269 | const range = new monaco.Range(lineCount, lastLineLength, lineCount, lastLineLength); 270 | 271 | model.result.pushEditOperations([new monaco.Selection(1, 1, 1, 1)], 272 | [{ range: range, text: text }], 273 | () => [new monaco.Selection(model.result.getLineCount(),0,model.result.getLineCount(),0)]) 274 | if(MonacoHandler._store.state.Nodes.currentIndex == this.id) 275 | MonacoHandler.resultEditor.revealPosition({ lineNumber: MonacoHandler.resultEditor.getModel().getLineCount(), column: 0 }) 276 | } 277 | 278 | 279 | 280 | 281 | } 282 | 283 | LndController.lang = 'lnd-rpc' 284 | 285 | module.exports = { 286 | type: 'lnd', 287 | controller: LndController 288 | } -------------------------------------------------------------------------------- /.nodetypes/src/lnd/monaco.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | class MonacoHandler { 4 | static register(editor, resultEditor, store, lang) { 5 | return new Promise((resolve, reject) => { 6 | monaco.languages.register({ id: lang }) 7 | monaco.editor.tokenize("", 'json') // tokenizer not ready first time if not this 8 | this.lang = lang 9 | this._store = store 10 | this.helpContent = {} // cache 11 | this.models = {} // models mutate on every keystroke and do not play well with vuex 12 | this.commandEditor = editor 13 | this.resultEditor = resultEditor 14 | fs.readFile(`${__dirname}/rpc.json`, (err, response) => { 15 | if(err) { reject(err); return } 16 | this._commands = JSON.parse(response.toString('utf8')) 17 | 18 | monaco.languages.setMonarchTokensProvider(this.lang, { 19 | tokenizer: { 20 | root: [ 21 | [/([a-zA-Z_\$][\w\$]*)(\s*)(:?)/, { 22 | cases: { '$1@keywords': ['keyword', 'white', 'delimiter'], '@default': ['identifier', 'white', 'delimiter'] } 23 | }], 24 | [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string 25 | [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string 26 | [/"/, 'string', '@string."'], 27 | [/'/, 'string', '@string.\''], 28 | [/\d+\.\d*(@exponent)?/, 'number.float'], 29 | [/\.\d+(@exponent)?/, 'number.float'], 30 | [/\d+@exponent/, 'number.float'], 31 | [/0[xX][\da-fA-F]+/, 'number.hex'], 32 | [/0[0-7]+/, 'number.octal'], 33 | [/\d+/, 'number'], 34 | // [/[{}\[\]]/, '@brackets'], 35 | [/\[/, 'bracket.square.open'], 36 | [/\]/, 'bracket.square.close'], 37 | [/{/, 'bracket.curly.open'], 38 | [/}/, 'bracket.curly.close'], 39 | [/[ \t\r\n]+/, 'white'], 40 | [/[;,.]/, 'delimiter'], 41 | [/null/, 'null'], 42 | ], 43 | string: [ 44 | [/[^\\"']+/, 'string'], 45 | [/@escapes/, 'string.escape'], 46 | [/\\./, 'string.escape.invalid'], 47 | [/["']/, { 48 | cases: { 49 | '$#==$S2': { token: 'string', next: '@pop' }, 50 | '@default': 'string' 51 | } 52 | }] 53 | ], 54 | }, 55 | keywords: Object.keys(this._commands), //.concat('true', 'false', 'null',), 56 | exponent: /[eE][\-+]?[0-9]+/, 57 | escapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/, 58 | brackets: [ 59 | ['{', '}', 'bracket.curly'], 60 | ['[', ']', 'bracket.square'] 61 | ], 62 | }); 63 | 64 | monaco.languages.registerHoverProvider(this.lang, { 65 | provideHover: (model, position) => { 66 | let word = '' 67 | const wordAtPos = model.getWordAtPosition(position) 68 | if (wordAtPos) word = wordAtPos.word 69 | 70 | if (word && ~Object.keys(this._commands).indexOf(word)) { 71 | const cmd = this._commands[word] 72 | let contents = [`**${word}**`].concat(cmd.description) 73 | contents.push('### Options\n\n') 74 | cmd.args.forEach(a => { 75 | contents.push(`**${a.name}**: _${a.type}_\n\n${a.description.split("\n").join('\n')}\n`) 76 | }) 77 | if(!cmd.args.length) contents.push('_none_') 78 | contents.push('### Returns\n\n') 79 | cmd.response.forEach(a => { 80 | contents.push(`**${a.name}**: _${a.type}_\n\n${a.description.split("\n").join('\n')}\n`) 81 | }) 82 | if(!cmd.response.length) contents.push('_none_') 83 | return { 84 | contents: contents 85 | } 86 | } 87 | 88 | } 89 | }); 90 | 91 | const execCommandId = editor.addCommand(0, function (wtf, line) { // don't knnow what first argument is??? 92 | const pos = editor.getPosition() 93 | editor.setPosition({ lineNumber: line, column: 1 }) 94 | editor.getAction('action-execute-command').run() 95 | editor.setPosition(pos) 96 | }, ''); 97 | monaco.languages.registerCodeLensProvider(this.lang, { 98 | provideCodeLenses: (model, token) => { 99 | return model.getLinesContent().reduce((o, c, i) => { 100 | let word = '' 101 | const lineNumber = i + 1 102 | const wordAtPos = model.getWordAtPosition({ lineNumber: lineNumber, column: 1 }) 103 | if (wordAtPos) word = wordAtPos.word 104 | if (word && ~Object.keys(this._commands).indexOf(word)) 105 | o.push( 106 | { 107 | range: { 108 | startLineNumber: lineNumber, 109 | startColumn: 1, 110 | endLineNumber: lineNumber + 1, 111 | endColumn: 1 112 | }, 113 | id: "lens item" + lineNumber, 114 | command: { 115 | id: execCommandId, 116 | title: "Execute", 117 | arguments: [lineNumber] 118 | } 119 | } 120 | 121 | ) 122 | return o 123 | }, []) 124 | }, 125 | resolveCodeLens: function (model, codeLens, token) { 126 | return codeLens; 127 | } 128 | }); 129 | monaco.languages.registerCompletionItemProvider(this.lang, { 130 | provideCompletionItems: (model, position) => { 131 | var tokens = monaco.editor.tokenize(model.getLineContent(position.lineNumber), 'json') 132 | var token = tokens[0].filter(t => t.offset == (position.column-2)) 133 | const block = model.getValueInRange({ 134 | startColumn: 1, startLineNumber: l, endColumn: position.column, endLineNumber: position.lineNumber 135 | }) 136 | const re = /("([^"]|"")*")/g 137 | let stringsMatch = block.match(re) 138 | let word 139 | const keys = Object.keys(this._commands) 140 | for(var l=position.lineNumber; l>0; l--) { 141 | word = model.getWordAtPosition({ 142 | lineNumber: l, column: 1}) 143 | if(word && ~keys.indexOf(word.word)) break; 144 | } 145 | if(token.length && token[0].type == "string.key.json") { 146 | if(word && ~keys.indexOf(word.word)) { 147 | 148 | const argargs = this._commands[word.word].args.filter(a => a.args) 149 | 150 | if(argargs.length) { 151 | if(stringsMatch) { 152 | const argnames = argargs.map(a => a.name) 153 | stringsMatch = stringsMatch.filter(f => ~argnames.indexOf(f.replace(/"/g, ''))) 154 | if(stringsMatch.length) { 155 | const key = stringsMatch[stringsMatch.length-1].replace(/"/g, '') 156 | const keyIndex = block.indexOf(stringsMatch[stringsMatch.length-1]) 157 | const lastBrace = block.lastIndexOf("}") 158 | if((lastBrace==-1 || keyIndex > lastBrace) && ~argnames.indexOf(key)) 159 | return this._commands[word.word].args.filter(a => a.name == key)[0].args.map(a => { 160 | return { 161 | label: a.name, 162 | insertText: a.name, 163 | detail: a.type, 164 | documentation: a.description 165 | } 166 | }) 167 | } 168 | } 169 | } 170 | 171 | return this._commands[word.word].args.map(a => { 172 | return { 173 | label: a.name, 174 | insertText: a.name, 175 | detail: a.type, 176 | documentation: a.description 177 | } 178 | }) 179 | } 180 | } 181 | 182 | if(token.length && token[0].type == "delimiter.colon.json") { 183 | if(stringsMatch) { 184 | const argenum = this._commands[word.word].args.filter(a => a.enum && a.name == stringsMatch[stringsMatch.length-1].replace(/"/g, '')) 185 | if(argenum.length) { 186 | return argenum.reduce((o, a) => { 187 | Object.keys(a.enum).forEach(k => { 188 | o.push( { 189 | label: k, 190 | insertText: k, 191 | kind: monaco.languages.CompletionItemKind.Enum 192 | }) 193 | }) 194 | return o 195 | },[]) 196 | } 197 | } 198 | } 199 | 200 | if(tokens[0].length==1 && tokens[0][0].offset == 0 ){ 201 | return Object.keys(this._commands).map(k => { 202 | return { 203 | label: k, 204 | insertText: k, 205 | documentation: this._commands[k].description, 206 | kind: monaco.languages.CompletionItemKind.Function 207 | } 208 | }) 209 | } 210 | return [] 211 | 212 | }, 213 | triggerCharacters: ['"', ":"] 214 | }); 215 | monaco.languages.setLanguageConfiguration(this.lang, { 216 | autoClosingPairs: [ 217 | {open: '"', close: '"'}, 218 | {open: '{', close: '}'}, 219 | {open: '[', close: ']'}, 220 | ] 221 | }) 222 | resolve() 223 | }) 224 | 225 | }).catch(e => reject(e)) 226 | } 227 | } 228 | 229 | module.exports = MonacoHandler -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 510 | 511 | 514 | -------------------------------------------------------------------------------- /.nodetypes/src/bitcoin/BitcoinController.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') // ajax 2 | const fs = require('fs') 3 | const os = require('os') 4 | 5 | class BitcoinController { 6 | 7 | /** 8 | * Initialize instance with configuration 9 | * @param {object} cfg 10 | * @property {string} cfg.host RPC host address, default 127.0.0.1 11 | * @property {number} cfg.port RPC port number, default 8332 12 | * @property {string} cfg.config Path to config file with user credential, may also constain port and host, default $HOME/.bitcoin/bitcoin.conf 13 | */ 14 | constructor(cfg) { 15 | this.update(cfg) 16 | } 17 | 18 | _interval() { 19 | return new Promise((resolve, reject) => { 20 | Promise.all([ 21 | this._getBlock(), 22 | this._getMempool(), 23 | this._getBanned(), 24 | this._getPeerInfo()] 25 | ).then(() => { 26 | this._infoTime = new Date().getTime() 27 | resolve(Object.assign({}, this._info)) // assign to isolate from store 28 | }).catch(reject) 29 | 30 | }) 31 | } 32 | 33 | _getNetInfo() { 34 | return new Promise(async (resolve, reject) => { 35 | try { 36 | const js = await this._postRPC({ 37 | method: "getnetworkinfo" 38 | }) 39 | try { 40 | this._info.version = js.data.result.version 41 | this._info.subversion = js.data.result.subversion 42 | resolve() 43 | } catch(wtf) {resolve('network error')} 44 | } catch (e) { resolve() } 45 | }) 46 | 47 | } 48 | 49 | _getMempool() { 50 | return new Promise(async (resolve, reject) => { 51 | try { 52 | const pool = await this._postRPC({ 53 | method: "getmempoolinfo", 54 | }) 55 | this._info.memusage = pool.data.result.bytes 56 | this._info.memnum = pool.data.result.size 57 | resolve() 58 | } catch (e) { resolve() } 59 | }) 60 | } 61 | 62 | _getBanned() { 63 | return new Promise(async (resolve, reject) => { 64 | try { 65 | let response = await this._postRPC({ method: 'listbanned' }) 66 | let tbody = '' 67 | const peers = response.data.result 68 | this._banned = { banned: peers } 69 | resolve() 70 | } catch (e) { resolve() } 71 | }) 72 | } 73 | 74 | _getPeerInfo() { 75 | return new Promise(async (resolve, reject) => { 76 | try { 77 | let response = await this._postRPC({ method: 'getpeerinfo' }) 78 | const peers = response.data.result 79 | let con = { in: 0, out: 0 } 80 | peers.forEach(peer => { 81 | if (peer.inbound) con.in++; else con.out++ 82 | }) 83 | this._info.netconnections = `${con.in + con.out} (in: ${con.in} / out: ${con.out})` 84 | this._peers = peers 85 | resolve() 86 | } catch (e) { resolve() } 87 | }) 88 | } 89 | 90 | _getHelp() { // called once to load completion providers 91 | return this._postRPC({ method: 'help' }) 92 | } 93 | 94 | _getBlock() { 95 | return new Promise(async (resolve, reject) => { 96 | try { 97 | const js = await this._postRPC({ 98 | method: "getblockchaininfo" 99 | }) 100 | const block = await this._postRPC({ 101 | method: "getblock", 102 | params: [js.data.result.bestblockhash] 103 | }) 104 | this._info.chain = js.data.result.chain 105 | this._info.blocks = js.data.result.blocks 106 | this._info.blocktime = block.data.result.time 107 | resolve() 108 | 109 | } catch (e) { resolve() } 110 | }) 111 | } 112 | 113 | _postRPC(payload) { 114 | payload.jsonrpc = "1.0" 115 | payload.id = payload.id || "" 116 | payload.params = payload.params || [] 117 | return axios({ 118 | url: `http://${this._host}:${this._port}`, 119 | method: 'post', 120 | withCredentials: true, 121 | auth: { 122 | username: this._user, 123 | password: this._password 124 | }, 125 | data: payload 126 | }) 127 | .then(d => { this.online = true; return d}).catch(e => { 128 | this.online = false 129 | return e.response 130 | }) 131 | } 132 | 133 | _createConsole() { 134 | this.constructor.models[this.id] = { 135 | command: monaco.editor.createModel('', this.constructor.lang), 136 | result: monaco.editor.createModel('', 'javascript') 137 | } 138 | } 139 | 140 | /** 141 | * Parse the command editor and send command to service 142 | * @param {monaco.Editor} ed The command editor instance to perform command 143 | */ 144 | execute(ed) { 145 | const val = this.constructor._getCommandBlock(ed.getModel(), ed.getPosition()).map(b => b.text).join(' ') 146 | const tokens = monaco.editor.tokenize(val, this.constructor.lang)[0] 147 | let chunks = val.split(' ') 148 | const method = chunks[0] 149 | let params = [], brackets = [] 150 | if (chunks.length > 1) { 151 | try { 152 | tokens.forEach((t, ti) => { 153 | if(ti===0) return 154 | const prevToken = tokens[ti-1] 155 | const tokenVal = val.slice(t.offset, ti == tokens.length-1 ? val.length : tokens[ti+1].offset) 156 | if(prevToken.type ==`white.${this.constructor.lang}` || prevToken.type==`bracket.square.open.${this.constructor.lang}`) { 157 | if((t.type==`bracket.square.open.${this.constructor.lang}` || t.type==`bracket.curly.open.${this.constructor.lang}`)) { 158 | brackets.unshift('') 159 | } else if(!brackets.length) { 160 | try { 161 | params.push(JSON.parse(tokenVal)) 162 | } catch(e) {console.log('invalid JSON', tokenVal)} 163 | } 164 | } 165 | if(brackets.length && t.type != `white.${this.constructor.lang}`) { 166 | brackets[0]+= tokenVal 167 | if((t.type==`bracket.square.close.${this.constructor.lang}` || t.type==`bracket.curly.close.${this.constructor.lang}`)) { 168 | if(brackets.length == 1) { 169 | const done = brackets.shift() 170 | try { 171 | params.push(JSON.parse(done)) 172 | } catch(e) {console.log('invalid JSON', done)} 173 | } else { 174 | const raw = brackets.shift() 175 | brackets[0] += raw 176 | } 177 | } 178 | 179 | } 180 | }); 181 | 182 | } catch (err) { 183 | this.constructor._appendToEditor(`${err}\n\n`) 184 | return 185 | } 186 | } 187 | this._postRPC({ method: method, params: params }).then(response => { 188 | let content = '// '+method+' '+params.map(p => JSON.stringify(p)).join(' ') + '\n' 189 | content += JSON.stringify(response.data || response.error, null, 2) + '\n\n' 190 | this.constructor._appendToEditor(content) 191 | }).catch(err => console.log) 192 | return null; 193 | } 194 | 195 | /** 196 | * check to see if node is still online 197 | * @returns {Promise} what the promise resolves to is very important 198 | */ 199 | ping() { return this._postRPC({method: 'ping'})} 200 | 201 | /** 202 | * Gets the models for the command and result editor 203 | * @returns {Promise>} an object containing both models {command: model, result: model} 204 | */ 205 | getConsole() { 206 | return new Promise((resolve, reject) => { 207 | if(!this.constructor.models[this.id]) this._createConsole() 208 | resolve(this.constructor.models[this.id]) 209 | }) 210 | } 211 | 212 | /** 213 | * returns required information for the info tab 214 | * @returns {Promise} an object that matches the info tab componet requirements 215 | */ 216 | getInfo() { 217 | return this._interval() 218 | } 219 | 220 | /** 221 | * returns required information for the peers tab 222 | * @returns {Promise} an object that matches the peers tab componet requirements 223 | */ 224 | getPeers() { 225 | return new Promise((resolve, reject) => { 226 | Promise.all( 227 | [this._postRPC({ method: 'getpeerinfo' }) 228 | , this._postRPC({ method: 'listbanned' }) 229 | ] 230 | ).then((arr) => resolve({ peers: arr[0].data.result, banned: arr[1].data.result })) 231 | .catch(reject) 232 | }) 233 | } 234 | 235 | /** 236 | * instantiates or refreshes values for an instance 237 | * @param {object} cfg information needed to instantiate instance, see constructor 238 | */ 239 | update(cfg) { 240 | this._host = cfg && cfg.host || '127.0.0.1' 241 | this._info = {} 242 | this._infoTime = 0 243 | this.id = cfg.index 244 | const config = fs.readFileSync(cfg && cfg.config.replace('~', os.homedir()) || `${os.homedir()}/.bitcoin/bitcoin.conf`, 'utf8'); 245 | let rpcport 246 | config.split('\n').forEach(line => { 247 | let rpcuser = line.match(/^\s?rpcuser\s?=\s?([^#]+)$/) 248 | if (rpcuser) this._user = rpcuser[1] 249 | let rpcpass = line.match(/^\s?rpcpassword\s?=\s?([^#]+)$/) 250 | if (rpcpass) this._password = rpcpass[1] 251 | let port = line.match(/^\s?rpcport\s?=\s?([^#]+)$/) 252 | if (port) rpcport = port[1] 253 | }) 254 | this._port = cfg && cfg.port || rpcport || '8332' 255 | 256 | this._getNetInfo() 257 | } 258 | 259 | static _getHelpContent (key) { 260 | if (!~this._helpers.map(h => h.command).indexOf(key)) { 261 | return new Promise(resolve => resolve({ results: [] })) 262 | } 263 | if (this.helpContent[key]) { 264 | let promise = new Promise((resolve, reject) => { 265 | resolve(this.helpContent[key]) 266 | }) 267 | return promise 268 | } else return window.controllerInstances[this._store.state.Nodes.currentIndex]._postRPC({ method: 'help', params: [key] }).then(resp => { 269 | this.helpContent[key] = resp 270 | return resp 271 | }) 272 | } 273 | 274 | static _getCommandBlock (model, position) { 275 | let line = position.lineNumber, wordAtPos, word = '' 276 | let block = model.getLineContent(line) ? [] : [{text:''}] // keep block alive on enter 277 | let tmpline 278 | while(tmpline = model.getLineContent(line)) { 279 | wordAtPos = model.getWordAtPosition({lineNumber: line, column: 1}) 280 | block.unshift({text: model.getLineContent(line), offset: line - position.lineNumber}) 281 | if(wordAtPos) word = wordAtPos.word 282 | if(word) { 283 | if(~this._helpers.map(w => w.command).indexOf(word)) break; 284 | } 285 | line-- 286 | if(line===0) break 287 | } 288 | line = position.lineNumber + 1 289 | if(line > model.getLineCount()) return block 290 | while(tmpline = model.getLineContent(line)) { 291 | wordAtPos = model.getWordAtPosition({lineNumber: line, column: 1}) 292 | if(wordAtPos && ~this._helpers.map(w => w.command).indexOf(wordAtPos.word)) break; 293 | tmpline = tmpline.replace(/^\s+/,'') 294 | if(!tmpline) break; 295 | block.push({text: model.getLineContent(line), offset: line - position.lineNumber}) 296 | line++ 297 | if(line > model.getLineCount()) break 298 | } 299 | return block 300 | } 301 | 302 | static _appendToEditor (text) { 303 | const lineCount = this.resultEditor.getModel().getLineCount(); 304 | const lastLineLength = this.resultEditor.getModel().getLineMaxColumn(lineCount); 305 | 306 | const range = new monaco.Range(lineCount, lastLineLength, lineCount, lastLineLength); 307 | 308 | this.resultEditor.updateOptions({ readOnly: false }) 309 | this.resultEditor.executeEdits('', [ 310 | { range: range, text: text } 311 | ]) 312 | this.resultEditor.updateOptions({ readOnly: true }) 313 | this.resultEditor.setSelection(new monaco.Range(1, 1, 1, 1)) 314 | this.resultEditor.revealPosition({ lineNumber: this.resultEditor.getModel().getLineCount(), column: 0 }) 315 | 316 | } 317 | 318 | static _setHelpers(response) { 319 | this._helpers = response.data.result.split('\n').reduce((o, c, i) => { 320 | if (c && !c.indexOf('==') == 0) { 321 | const pieces = c.split(' ') 322 | o.push({ command: pieces[0], help: pieces.length > 1 ? pieces.slice(1).join(' ') : '' }) 323 | } 324 | return o 325 | }, []) 326 | } 327 | 328 | static _setSignatureHelp() { 329 | monaco.languages.registerSignatureHelpProvider(this.lang, { 330 | provideSignatureHelp: (model, position) => { 331 | const getBlockIndex = (block, col) => { 332 | let index = -1 333 | let lineindex = block.reduce((o, c, i) => c.offset === 0 ? i : o, -1) 334 | const tokens = monaco.editor.tokenize(block.map(b => b.text).join('\n'), this.lang) 335 | let brackets = [] 336 | for (let i = 0; i <= lineindex; i++) { 337 | const token = tokens[i] 338 | token.forEach((t, ti) => { 339 | const prevToken = ti === 0 ? i === 0 ? null : tokens[i - 1][tokens[i - 1].length - 1] : token[ti - 1] 340 | switch (t.type) { 341 | case `white.${this.lang}`: 342 | if (prevToken.type == `keyword.${this.lang}`) index = 0 343 | if (~[`number.${this.lang}`, `string.${this.lang}`, `identifier.${this.lang}`].indexOf(prevToken.type) && !brackets.length) index++ 344 | break 345 | case `bracket.square.open.${this.lang}`: 346 | brackets.unshift('square') 347 | break 348 | case `bracket.square.close.${this.lang}`: 349 | brackets.shift('square') 350 | index++ 351 | break 352 | 353 | } 354 | }); 355 | } 356 | return index 357 | } 358 | 359 | const block = this._getCommandBlock(model, position) 360 | let word = '' 361 | if (block.length) word = block[0].text.split(' ')[0] 362 | if (word) return this._getHelpContent(word).then(response => { 363 | if(!response.data) return {} 364 | let lines = response.data.result.split("\n") 365 | let args = false, desc = false 366 | const obj = lines.reduce((o, c, i) => { 367 | if (!c && args) { 368 | args = false 369 | } 370 | else if (c.match(/Arguments/)) args = true 371 | else if (args) { 372 | let ltokens = c.split(/\s+/) 373 | if (ltokens[0].match(/[0-9]+\./)) 374 | o.params[ltokens[1].replace(/"/g, '')] = ltokens.slice(2).join(' ') 375 | } 376 | else if (i > 1 && !c) desc = true 377 | else if (i > 0 && !desc) o.desc += c + "\n" 378 | return o 379 | }, { params: {}, desc: '' }) 380 | obj.desc = obj.desc.replace(/(^\n|\n$)/, '') 381 | const index = getBlockIndex(block, position.column) 382 | const params = Object.keys(obj.params).map(k => { return { label: k, documentation: obj.params[k] } }) 383 | if (index > -1 && index < params.length) 384 | return { 385 | activeSignature: 0, 386 | activeParameter: index, 387 | signatures: [ 388 | { 389 | label: lines[0], 390 | parameters: params 391 | } 392 | ] 393 | } 394 | else return {} 395 | }) 396 | else return {} 397 | 398 | }, 399 | signatureHelpTriggerCharacters: [' ', '\t', '\n'] 400 | }) 401 | } 402 | 403 | static _setHoverHelp() { 404 | monaco.languages.registerHoverProvider(this.lang, { 405 | provideHover: (model, position) => { 406 | let word = '' 407 | const wordAtPos = model.getWordAtPosition(position) 408 | if (wordAtPos) word = wordAtPos.word 409 | 410 | if (word && ~this._helpers.map(h => h.command).indexOf(word)) { 411 | return this._getHelpContent(word).then(response => { 412 | return { 413 | contents: [ 414 | `**${word}**`, 415 | { language: 'text', value: response.data.result } 416 | ] 417 | } 418 | }) 419 | } 420 | } 421 | }); 422 | } 423 | 424 | /** 425 | * This is called only once for each node type and sets up static level data for the type 426 | * @param {monaco.editor} editor the command editor 427 | * @param {monaco.editor} resultEditor the result editor 428 | * @param {vuex.store} store the state of the application 429 | */ 430 | static register(editor, resultEditor, store) { 431 | return new Promise((resolve, reject) => { 432 | monaco.languages.register({ id: this.lang }) 433 | this._store = store 434 | this.helpContent = {} // cache 435 | this.models = {} // models mutate on every keystroke and do not play well with vuex 436 | this.commandEditor = editor 437 | this.resultEditor = resultEditor 438 | window.controllerInstances[store.state.Nodes.currentIndex]._getHelp().then(response => { 439 | if(!response) { reject(); return } 440 | this._setHelpers(response) 441 | 442 | // TODO: refactor out each provider section for better estension 443 | monaco.languages.setMonarchTokensProvider(this.lang, { 444 | tokenizer: { 445 | root: [ 446 | [/([a-zA-Z_\$][\w\$]*)(\s*)(:?)/, { 447 | cases: { '$1@keywords': ['keyword', 'white', 'delimiter'], '@default': ['identifier', 'white', 'delimiter'] } 448 | }], 449 | [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string 450 | [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string 451 | [/"/, 'string', '@string."'], 452 | [/'/, 'string', '@string.\''], 453 | [/\d+\.\d*(@exponent)?/, 'number.float'], 454 | [/\.\d+(@exponent)?/, 'number.float'], 455 | [/\d+@exponent/, 'number.float'], 456 | [/0[xX][\da-fA-F]+/, 'number.hex'], 457 | [/0[0-7]+/, 'number.octal'], 458 | [/\d+/, 'number'], 459 | // [/[{}\[\]]/, '@brackets'], 460 | [/\[/, 'bracket.square.open'], 461 | [/\]/, 'bracket.square.close'], 462 | [/{/, 'bracket.curly.open'], 463 | [/}/, 'bracket.curly.close'], 464 | [/[ \t\r\n]+/, 'white'], 465 | [/[;,.]/, 'delimiter'], 466 | [/null/, 'null'], 467 | ], 468 | string: [ 469 | [/[^\\"']+/, 'string'], 470 | [/@escapes/, 'string.escape'], 471 | [/\\./, 'string.escape.invalid'], 472 | [/["']/, { 473 | cases: { 474 | '$#==$S2': { token: 'string', next: '@pop' }, 475 | '@default': 'string' 476 | } 477 | }] 478 | ], 479 | }, 480 | keywords: this._helpers.map(h => h.command), //.concat('true', 'false', 'null',), 481 | exponent: /[eE][\-+]?[0-9]+/, 482 | escapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/, 483 | brackets: [ 484 | ['{', '}', 'bracket.curly'], 485 | ['[', ']', 'bracket.square'] 486 | ], 487 | }); 488 | 489 | this._setHoverHelp() 490 | 491 | this._setSignatureHelp() 492 | 493 | const execCommandId = editor.addCommand(0, function (wtf, line) { // don't knnow what first argument is??? 494 | const pos = editor.getPosition() 495 | editor.setPosition({ lineNumber: line, column: 1 }) 496 | editor.getAction('action-execute-command').run() 497 | editor.setPosition(pos) 498 | }, ''); 499 | monaco.languages.registerCodeLensProvider(this.lang, { 500 | provideCodeLenses: (model, token) => { 501 | return model.getLinesContent().reduce((o, c, i) => { 502 | let word = '' 503 | const lineNumber = i + 1 504 | const wordAtPos = model.getWordAtPosition({ lineNumber: lineNumber, column: 1 }) 505 | if (wordAtPos) word = wordAtPos.word 506 | if (word && ~this._helpers.map(h => h.command).indexOf(word)) 507 | o.push( 508 | { 509 | range: { 510 | startLineNumber: lineNumber, 511 | startColumn: 1, 512 | endLineNumber: lineNumber + 1, 513 | endColumn: 1 514 | }, 515 | id: "lens item" + lineNumber, 516 | command: { 517 | id: execCommandId, 518 | title: "Execute", 519 | arguments: [lineNumber] 520 | } 521 | } 522 | 523 | ) 524 | return o 525 | }, []) 526 | }, 527 | resolveCodeLens: function (model, codeLens, token) { 528 | return codeLens; 529 | } 530 | }); 531 | monaco.languages.registerCompletionItemProvider(this.lang, { 532 | provideCompletionItems: (model, position) => { 533 | return this._helpers.reduce((o, c) => { 534 | o.push({ 535 | label: c.command, 536 | kind: monaco.languages.CompletionItemKind.Function, 537 | detail: c.help 538 | }) 539 | return o 540 | }, []) 541 | } 542 | }); 543 | monaco.languages.setLanguageConfiguration(this.lang, { 544 | autoClosingPairs: [ 545 | {open: '"', close: '"'}, 546 | {open: '{', close: '}'}, 547 | {open: '[', close: ']'}, 548 | ] 549 | }) 550 | resolve() 551 | }).catch(e => reject(e)) 552 | }) 553 | 554 | } 555 | } 556 | 557 | BitcoinController.lang = 'bitcoin-rpc' 558 | module.exports = { 559 | type: 'bitcoin', 560 | controller: BitcoinController, 561 | BitcoinController: BitcoinController 562 | } 563 | --------------------------------------------------------------------------------