├── 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 |
2 | An empty component!
3 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
9 |
10 |
11 |
26 |
27 |
--------------------------------------------------------------------------------
/src/renderer/components/Network.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Network
4 |
5 |
6 |
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 |
2 |
3 |
General
4 |
5 |
Client Version
6 |
{{ version }}
7 |
8 |
9 |
10 |
Protocol Version
11 |
{{ renderData.subversion }}
12 |
13 |
14 |
Network
15 |
16 |
Name
17 |
{{ renderData.chain }}
18 |
19 |
20 |
21 |
Connections
22 |
{{ renderData.netconnections }}
23 |
24 |
25 |
Blockchain
26 |
27 |
Number of Blocks
28 |
{{ renderData.blocks }}
29 |
30 |
31 |
32 |
Last Block Time
33 |
{{ blocktime }}
34 |
35 |
36 |
Mempool
37 |
38 |
Number of transaction
39 |
{{ renderData.memnum }}
40 |
41 |
42 |
43 |
Memory Usage
44 |
{{ renderData.memusage }}
45 |
46 |
47 |
48 |
49 |
75 |
--------------------------------------------------------------------------------
/.nodetypes/src/bitcoin/BitcoinInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
General
4 |
5 |
Client Version
6 |
{{ version }}
7 |
8 |
9 |
10 |
User Agent
11 |
{{ renderData.subversion }}
12 |
13 |
14 |
15 |
Network
16 |
17 |
Name
18 |
{{ renderData.chain }}
19 |
20 |
21 |
22 |
Connections
23 |
{{ renderData.netconnections }}
24 |
25 |
26 |
Blockchain
27 |
28 |
Number of Blocks
29 |
{{ renderData.blocks }}
30 |
31 |
32 |
33 |
Last Block Time
34 |
{{ blocktime }}
35 |
36 |
37 |
Mempool
38 |
39 |
Number of transaction
40 |
{{ renderData.memnum }}
41 |
42 |
43 |
44 |
Memory Usage
45 |
{{ renderData.memusage }}
46 |
47 |
48 |
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 |
2 |
3 |
Node
4 |
5 |
Alias
6 |
{{ renderData.alias }}
7 |
8 |
9 |
ID
10 |
{{ renderData.id }}
11 |
12 |
13 |
Version
14 |
{{ renderData.version }}
15 |
16 |
17 |
Pending Channels
18 |
{{ renderData.num_pending_channels }}
19 |
20 |
21 |
Active Channels
22 |
{{ renderData.num_active_channels }}
23 |
24 |
25 |
Inactive Channels
26 |
{{ renderData.num_inactive_channels }}
27 |
28 |
29 |
Peers
30 |
{{ renderData.num_peers }}
31 |
32 |
33 |
Block Height
34 |
{{ renderData.blockheight }}
35 |
36 |
37 |
Network
38 |
{{ renderData.network }}
39 |
40 |
41 |
Wallet
42 |
43 |
Balance
44 |
{{ balance }}
45 |
46 |
47 |
Confirmed
48 |
{{ confirmed }}
49 |
50 |
51 |
Unconfirmed
52 |
{{ unconfirmed }}
53 |
54 |
55 |
56 |
57 |
85 |
--------------------------------------------------------------------------------
/.nodetypes/src/lnd/LndInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Node
4 |
5 |
Alias
6 |
{{ renderData.alias }}
7 |
8 |
9 |
Pubkey
10 |
{{ renderData.identity_pubkey }}
11 |
12 |
13 |
Version
14 |
{{ renderData.version }}
15 |
16 |
17 |
Pending Channels
18 |
{{ renderData.num_pending_channels }}
19 |
20 |
21 |
Active Channels
22 |
{{ renderData.num_active_channels }}
23 |
24 |
25 |
Inactive Channels
26 |
{{ renderData.num_inactive_channels }}
27 |
28 |
29 |
Peers
30 |
{{ renderData.num_peers }}
31 |
32 |
33 |
Chain
34 |
35 |
Block Height
36 |
{{ renderData.block_height }}
37 |
38 |
39 |
Block Hash
40 |
{{ renderData.block_hash }}
41 |
42 |
43 |
Synced
44 |
{{ renderData.synced_to_chain }}
45 |
46 |
47 |
Testnet
48 |
{{ renderData.testnet }}
49 |
50 |
51 |
Chains
52 |
{{ JSON.stringify(renderData.chains) }}
53 |
54 |
55 |
Wallet
56 |
57 |
Balance
58 |
{{ renderData.total_balance }}
59 |
60 |
61 |
Confirmed
62 |
{{ renderData.confirmed_balance }}
63 |
64 |
65 |
Unconfirmed
66 |
{{ renderData.unconfirmed_balance }}
67 |
68 |
69 |
70 |
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 |
2 |
3 |
4 |
5 | Peer Connections
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Channels
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 |
2 |
3 |
4 |
5 | Peer Connections
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Channels
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
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 |
2 |
3 |
4 |
5 | Peers
6 |
10 |
11 |
12 |
13 |
Whitelisted
14 |
{{ props.row.whitelisted }}
15 |
16 |
17 |
Direction
18 |
{{ props.row.inbound ? 'in' : 'out' }}
19 |
20 |
21 |
Version
22 |
{{ props.row.version }}
23 |
24 |
25 |
User Agent
26 |
{{ props.row.subver }}
27 |
28 |
33 |
34 |
Starting Block
35 |
{{ props.row.startingheight }}
36 |
37 |
38 |
Synced Headers
39 |
{{ props.row.synced_headers }}
40 |
41 |
42 |
Synced Blocks
43 |
{{ props.row.synced_blocks }}
44 |
45 |
46 |
Ban Score
47 |
{{ props.row.banscore }}
48 |
49 |
50 |
Connection Time
51 |
{{ props.row.howlong }}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Banned Peers
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
140 |
--------------------------------------------------------------------------------
/.nodetypes/src/bitcoin/BitcoinPeers.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Peers
6 |
10 |
11 |
12 |
13 |
Whitelisted
14 |
{{ props.row.whitelisted }}
15 |
16 |
17 |
Direction
18 |
{{ props.row.inbound ? 'in' : 'out' }}
19 |
20 |
21 |
Version
22 |
{{ props.row.version }}
23 |
24 |
25 |
User Agent
26 |
{{ props.row.subver }}
27 |
28 |
33 |
34 |
Starting Block
35 |
{{ props.row.startingheight }}
36 |
37 |
38 |
Synced Headers
39 |
{{ props.row.synced_headers }}
40 |
41 |
42 |
Synced Blocks
43 |
{{ props.row.synced_blocks }}
44 |
45 |
46 |
Ban Score
47 |
{{ props.row.banscore }}
48 |
49 |
50 |
Connection Time
51 |
{{ props.row.howlong }}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Banned Peers
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
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 | 
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 | 
133 | Commands are executed by pressing F5, pressing the codelens helper or from the context menu.
134 |
135 | ### Notification dark theme (BTCD)
136 |
137 | 
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 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
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 |
2 |
3 |
5 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ node.name }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
57 |
58 |
59 |
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 |
--------------------------------------------------------------------------------