├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .sass-lint.yml ├── Dockerfile ├── Dockerfile-bcoin ├── README.md ├── configs ├── constants.js ├── karma.conf.js ├── webpack.common.config.js ├── webpack.config.js ├── webpack.dll.config.js └── webpack.prod.js ├── docker-compose.yml ├── etc ├── regtest.bcoin.env ├── sample.bcoin.conf ├── sample.client.conf └── sample.wallet.conf ├── license.md ├── package-lock.json ├── package.json ├── pkg.js ├── scripts ├── bcoin-init.js ├── createSecrets.js ├── funded-dummy-wallets.js ├── pagination-test-wallets.js ├── preinstall.js ├── setup-coinbase-address.js └── version.js ├── securityc.env ├── server ├── build-plugins.js ├── clear-plugins.js ├── endpoints │ ├── clients.js │ ├── index.js │ └── methods.js ├── handlers │ └── clients.js ├── index.js ├── logger.js ├── socketManager.js ├── test │ ├── configHelpers-test.js │ ├── socketManager-test.js │ └── utils │ │ ├── helpers.js │ │ └── regtest.js └── utils │ ├── apiFilters.js │ ├── attach.js │ ├── clients.js │ ├── configs.js │ ├── index.js │ ├── loadConfig.js │ ├── npm-exists.js │ └── plugins.js ├── vendor └── semver.js └── webapp ├── assets ├── favicon.ico └── logo.png ├── components ├── Footer.js ├── Header.js ├── Panel.js └── Sidebar.js ├── config ├── appConfig.js └── themeConfig │ ├── index.js │ ├── themeCreator.js │ └── themeVariables.js ├── containers ├── App │ └── App.js ├── Footer.js ├── Header.js ├── Panel.js ├── Sidebar.js └── ThemeProvider.js ├── index.js ├── index.template.ejs ├── loading.css ├── plugins ├── local │ └── .gitkeep ├── plugins.js └── utils.js ├── store ├── actions │ ├── appActions.js │ ├── chainActions.js │ ├── clientActions.js │ ├── index.js │ ├── navActions.js │ ├── nodeActions.js │ ├── socketActions.js │ └── themeActions.js ├── constants │ ├── app.js │ ├── chain.js │ ├── clients.js │ ├── index.js │ ├── nav.js │ ├── node.js │ ├── plugins.js │ ├── sockets.js │ ├── theme.js │ └── wallets.js ├── index.js ├── middleware.js ├── propTypes │ ├── index.js │ └── pluginMetadata.js ├── reducers │ ├── app.js │ ├── chain.js │ ├── clients.js │ ├── index.js │ ├── nav.js │ ├── node.js │ ├── pluginMetadata.js │ ├── theme.js │ └── wallets.js ├── rootReducer.js └── selectors │ ├── index.js │ └── nav.js ├── test ├── index.js ├── navSelector-test.js ├── pluginMetadata-test.js ├── utils-test.js └── utilsHelpers-test.js └── utils ├── createCss.js ├── helpers.js └── index.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | logs 3 | dist 4 | secrets.env 5 | node_modules 6 | webapp/version.json 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const config = require('./configs/webpack.config.js'); 2 | 3 | module.exports = { 4 | parser: 'babel-eslint', 5 | plugins: ['prettier', 'react'], 6 | env: { 7 | node: true, 8 | es6: true 9 | }, 10 | overrides: [ 11 | { 12 | files: ['webapp/**/*.js'], 13 | excludedFiles: '**/node_modules', 14 | env: { 15 | node: false, 16 | browser: true, 17 | commonjs: true 18 | }, 19 | globals: { 20 | NODE_ENV: true, 21 | BPANEL_SOCKET_PORT: true, 22 | SECRETS: true 23 | } 24 | }, 25 | { 26 | files: ['webapp/test/**/*.js', 'server/test/**/*.js'], 27 | env: { 28 | mocha: true 29 | } 30 | } 31 | ], 32 | rules: { 33 | 'prettier/prettier': 'error', 34 | 'no-empty': ['error', { allowEmptyCatch: true }], 35 | 'no-param-reassign': 'warn' 36 | }, 37 | settings: { 38 | react: { 39 | version: '^16.3.0' 40 | }, 41 | 'import/resolver': { 42 | webpack: { 43 | config: config({}) 44 | } 45 | } 46 | }, 47 | extends: [ 48 | 'prettier', 49 | 'eslint:recommended', 50 | 'plugin:react/recommended', 51 | 'plugin:import/errors' 52 | ] 53 | }; 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # swap files 55 | *.swp 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # Build packages 64 | dist/* 65 | 66 | # bcoin config 67 | bcoin.config.json 68 | 69 | # script generated files 70 | /webapp/version.json 71 | /webapp/plugins/index.js 72 | /webapp/plugins/local/index.js 73 | 74 | # Secrets 75 | secrets.env 76 | 77 | # Mac OS X 78 | .DS_Store 79 | 80 | # tls keys and certs 81 | **/*.crt 82 | **/*.key 83 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | leading-zero: 3 | - 1 4 | - include: true 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM node:alpine AS base 2 | FROM mhart/alpine-node:latest AS base 3 | 4 | # temporarily use this fork of node:alpine 5 | # because it has a newer version of npm 6 | # temporarily update npm manually 7 | # because of bug introduced in npm 6.0.0 8 | 9 | EXPOSE 5000 10 | RUN mkdir -p /usr/src/app/dist 11 | 12 | WORKDIR /usr/src/app 13 | 14 | ENTRYPOINT [ "node" ] 15 | CMD [ "server" ] 16 | 17 | ARG NPM_VERSION=6.3.0 18 | 19 | # Install updates 20 | RUN apk upgrade --no-cache && \ 21 | apk add --no-cache git python make g++ bash && \ 22 | npm install -g npm@$NPM_VERSION 23 | 24 | # install dependencies for node-hid 25 | RUN apk add --no-cache linux-headers eudev-dev libusb-dev 26 | # install handshake deps 27 | RUN apk add --no-cache unbound-dev 28 | 29 | COPY package.json \ 30 | package-lock.json \ 31 | /usr/src/app/ 32 | 33 | # Install dependencies 34 | FROM base AS build 35 | 36 | # dont run preinstall scripts here 37 | # by omitting --unsafe-perm 38 | RUN npm install 39 | 40 | # this is a grandchild dependency of hsd that gets skipped for some reason 41 | # and needs to be installed manually 42 | RUN npm install budp 43 | 44 | # Bundle app 45 | FROM base 46 | COPY --from=build /usr/src/app/node_modules /usr/src/app/node_modules 47 | COPY pkg.js /usr/src/app/pkg.js 48 | COPY vendor /usr/src/app/vendor 49 | COPY scripts /usr/src/app/scripts 50 | COPY configs /usr/src/app/configs 51 | COPY server /usr/src/app/server 52 | COPY webapp /usr/src/app/webapp 53 | RUN npm run build:dll && \ 54 | npm run preinstall --unsafe-perm && \ 55 | touch /root/.bpanel/clients/_docker.conf 56 | -------------------------------------------------------------------------------- /Dockerfile-bcoin: -------------------------------------------------------------------------------- 1 | FROM nfnty/arch-mini:latest AS base 2 | 3 | RUN mkdir -p /code/node_modules/bcoin /data 4 | WORKDIR /code 5 | 6 | RUN pacman -Sy --noconfirm archlinux-keyring && \ 7 | pacman -Syu --noconfirm nodejs-lts-carbon npm && \ 8 | rm /var/cache/pacman/pkg/* 9 | 10 | FROM base AS build 11 | 12 | # Install build dependencies 13 | # Note: node-gyp needs python 14 | RUN pacman -Syu --noconfirm base-devel unrar git python2 \ 15 | && ln -s /usr/bin/python2 /usr/bin/python 16 | 17 | ARG repo=bpanel-org/bcoin#experimental 18 | 19 | # use this to bust the build cache 20 | ARG rebuild=0 21 | 22 | # Install bcoin, bmultisig, blgr, bclient 23 | RUN npm init -y &>/dev/null \ 24 | && npm install $repo \ 25 | bcoin-org/bmultisig \ 26 | bcoin-org/blgr \ 27 | bcoin-org/bclient 28 | 29 | # TODO: Inherit from official image 30 | FROM base 31 | 32 | COPY --from=build /code/node_modules /code/node_modules 33 | COPY ./scripts/ /code/scripts 34 | 35 | ENTRYPOINT [ "node" ] 36 | 37 | # In order to have some predictability w/ docker instances 38 | # set the prefix via args to avoid any unexpected inconsistencies 39 | CMD ["/code/scripts/bcoin-init.js", "--prefix=/data"] 40 | -------------------------------------------------------------------------------- /configs/constants.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const ROOT_DIR = path.resolve(__dirname, '../'); 4 | const DIST_DIR = path.resolve(ROOT_DIR, 'dist'); 5 | const SRC_DIR = path.resolve(ROOT_DIR, 'webapp'); 6 | const SERVER_DIR = path.resolve(ROOT_DIR, 'server'); 7 | const MODULES_DIR = path.resolve(ROOT_DIR, 'node_modules'); 8 | 9 | exports.ROOT_DIR = ROOT_DIR; 10 | exports.DIST_DIR = DIST_DIR; 11 | exports.SRC_DIR = SRC_DIR; 12 | exports.SERVER_DIR = SERVER_DIR; 13 | exports.MODULES_DIR = MODULES_DIR; 14 | -------------------------------------------------------------------------------- /configs/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./webpack.config.js'); 2 | 3 | module.exports = config => { 4 | config.set({ 5 | basePath: '', 6 | frameworks: ['mocha', 'chai', 'sinon'], 7 | files: [ 8 | '../node_modules/babel-polyfill/dist/polyfill.js', 9 | '../webapp/test/index.js' 10 | ], 11 | preprocessors: { 12 | '../webapp/test/index.js': ['webpack'] 13 | }, 14 | webpack: { ...webpackConfig(), mode: 'development' }, 15 | webpackServer: { 16 | noInfo: true, 17 | quiet: true 18 | }, 19 | reporters: ['nyan'], 20 | port: 9876, 21 | colors: true, 22 | autoWatch: true, 23 | concurrency: Infinity, 24 | browsers: ['Firefox'] 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /configs/webpack.common.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const os = require('os'); 3 | const webpack = require('webpack'); 4 | const WebpackShellPlugin = require('webpack-synchronizable-shell-plugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const { MODULES_DIR, SRC_DIR, SERVER_DIR, ROOT_DIR } = require('./constants'); 8 | 9 | // can be passed by server process via bcfg interface 10 | // or passed manually when running webpack from command line 11 | // defaults to `~/.bpanel` 12 | const bpanelPrefix = 13 | process.env.BPANEL_PREFIX || path.resolve(os.homedir(), '.bpanel'); 14 | 15 | // socket port hard coded in since running on a different port from 16 | // file server and http proxy 17 | const BPANEL_SOCKET_PORT = process.env.BPANEL_SOCKET_PORT || 8000; 18 | let SECRETS = {}; 19 | try { 20 | SECRETS = require(path.resolve(bpanelPrefix, 'secrets.json')); 21 | } catch (e) { 22 | // eslint-disable-next-line no-console 23 | console.error( 24 | `Couldn't find secrets.json in ${bpanelPrefix}. Make sure to run npm install to create boilerplate files` 25 | ); 26 | } 27 | 28 | module.exports = () => ({ 29 | node: { 30 | fs: 'empty', 31 | }, 32 | resolve: { 33 | symlinks: false, 34 | extensions: ['-browser.js', '.js', '.json', '.jsx'], 35 | // list of aliases are what packages plugins can list as peerDeps 36 | // this helps simplify plugin packages and ensures that parent classes 37 | // all point to the same instance, e.g bcoin.TX will be same for all plugins 38 | alias: { 39 | '@bpanel/bpanel-utils': `${MODULES_DIR}/@bpanel/bpanel-utils`, 40 | '@bpanel/bpanel-ui': `${MODULES_DIR}/@bpanel/bpanel-ui`, 41 | bcash$: `${MODULES_DIR}/bcash/lib/bcoin-browser`, 42 | bcoin$: `${MODULES_DIR}/bcoin/lib/bcoin-browser`, 43 | bcrypto: `${MODULES_DIR}/bcrypto`, 44 | bledger: `${MODULES_DIR}/bledger/lib/bledger-browser`, 45 | bmultisig: `${MODULES_DIR}/bmultisig/lib/bmultisig-browser`, 46 | bsert: `${MODULES_DIR}/bsert`, 47 | hsd$: `${MODULES_DIR}/hsd/lib/hsd-browser`, 48 | react: `${MODULES_DIR}/react`, 49 | '&bpanel/pkg': `${ROOT_DIR}/pkg`, 50 | 'react-dom': `${MODULES_DIR}/react-dom`, 51 | 'react-router': `${MODULES_DIR}/react-router`, 52 | 'react-router-dom': `${MODULES_DIR}/react-router-dom`, 53 | 'react-redux': `${MODULES_DIR}/react-redux`, 54 | redux: `${MODULES_DIR}/redux`, 55 | reselect: `${MODULES_DIR}/reselect`, 56 | '&local': path.resolve(bpanelPrefix, 'local_plugins'), 57 | tinycolor: 'tinycolor2' 58 | } 59 | }, 60 | plugins: [ 61 | new HtmlWebpackPlugin({ 62 | title: 'bPanel - A Blockchain Management System', 63 | template: `${path.join(SRC_DIR, 'index.template.ejs')}`, 64 | inject: 'body' 65 | }), 66 | new WebpackShellPlugin({ 67 | onBuildStart: { 68 | scripts: [ 69 | `node ${path.resolve(SERVER_DIR, 'clear-plugins.js')}`, 70 | `node ${path.resolve( 71 | SERVER_DIR, 72 | 'build-plugins.js' 73 | )} --prefix=${bpanelPrefix}` 74 | ] 75 | } 76 | }), 77 | new webpack.DefinePlugin({ 78 | SECRETS: JSON.stringify(SECRETS), 79 | BPANEL_SOCKET_PORT: JSON.stringify(BPANEL_SOCKET_PORT), 80 | NODE_ENV: `"${process.env.NODE_ENV}"`, 81 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 82 | 'process.env.BROWSER': JSON.stringify(true) 83 | }) 84 | ] 85 | }); 86 | -------------------------------------------------------------------------------- /configs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const os = require('os'); 3 | const webpack = require('webpack'); 4 | 5 | const merge = require('webpack-merge'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | 8 | const config = require('./webpack.common.config.js'); 9 | const { ROOT_DIR, DIST_DIR, SRC_DIR, MODULES_DIR } = require('./constants'); 10 | 11 | // can be passed by server process via bcfg interface 12 | // or passed manually when running webpack from command line 13 | // defaults to `~/.bpanel` 14 | const bpanelPrefix = 15 | process.env.BPANEL_PREFIX || path.resolve(os.homedir(), '.bpanel'); 16 | 17 | module.exports = function(env = {}) { 18 | const plugins = []; 19 | 20 | const vendorManifest = path.join(DIST_DIR, 'vendor-manifest.json'); 21 | 22 | let dllPlugin = null; 23 | try { 24 | dllPlugin = new webpack.DllReferencePlugin({ 25 | manifest: require(vendorManifest), 26 | name: 'vendor_lib', 27 | scope: 'mapped' 28 | }); 29 | if (dllPlugin) plugins.push(dllPlugin); 30 | } catch (e) { 31 | // eslint-disable-next-line no-console 32 | console.error('There was an error building DllReferencePlugin:', e); 33 | } 34 | 35 | return merge.smart(config(), { 36 | context: ROOT_DIR, 37 | mode: 'development', 38 | optimization: { 39 | runtimeChunk: 'single', 40 | splitChunks: { 41 | // include all types of chunks 42 | // cache bcoin vendor files 43 | cacheGroups: { 44 | vendor: { 45 | test: /[\\/]node_modules\/(bcoin|bcash|hsd)[\\/]/, 46 | name: 'bcoin-vendor', 47 | chunks: 'all' 48 | } 49 | } 50 | } 51 | }, 52 | entry: [`${path.resolve(SRC_DIR, 'index.js')}`], 53 | node: { __dirname: true }, 54 | target: 'web', 55 | devtool: 'eval-source-map', 56 | output: { 57 | filename: '[name].[contenthash].js', 58 | path: DIST_DIR, 59 | libraryTarget: 'umd' 60 | }, 61 | watchOptions: { 62 | // generally use poll for mac environments 63 | poll: env.poll && (parseInt(env.poll) || 1000) 64 | }, 65 | module: { 66 | rules: [ 67 | { 68 | test: /\.css$/, 69 | use: [ 70 | { 71 | loader: MiniCssExtractPlugin.loader 72 | }, 73 | 'css-loader' 74 | ] 75 | }, 76 | { 77 | test: /\.jsx?$/, 78 | exclude: [MODULES_DIR, path.resolve(bpanelPrefix, 'local_plugins')], 79 | loader: 'babel-loader', 80 | query: { 81 | presets: ['env', 'react', 'stage-0'], 82 | plugins: [ 83 | [ 84 | 'syntax-dynamic-import', 85 | 'transform-object-rest-spread', 86 | 'transform-runtime', 87 | { 88 | helpers: true, 89 | polyfill: true, 90 | regenerator: true, 91 | modules: false 92 | } 93 | ] 94 | ] 95 | } 96 | }, 97 | { 98 | test: /\.(ttf|eot|svg|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 99 | loader: 'file-loader' 100 | }, 101 | { 102 | test: /\.(png|jpg|gif|ico)$/, 103 | loader: 'file-loader', 104 | options: { 105 | name(file) { 106 | const { name } = path.parse(file); 107 | // this lets us keep name for favicon use 108 | if (name === 'logo' || name === 'favicon') { 109 | return '[name].[ext]?[hash]'; 110 | } 111 | 112 | return '[hash].[ext]'; 113 | } 114 | } 115 | } 116 | ] 117 | }, 118 | plugins: plugins.concat( 119 | new MiniCssExtractPlugin({ 120 | filename: '[name].css', 121 | chunkFilename: '[id].css' 122 | }) 123 | ) 124 | }); 125 | }; 126 | -------------------------------------------------------------------------------- /configs/webpack.dll.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const { DIST_DIR, MODULES_DIR } = require('./constants'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const vendorManifest = path.join(DIST_DIR, '[name]-manifest.json'); 7 | 8 | module.exports = { 9 | target: 'web', 10 | mode: 'development', 11 | entry: { 12 | vendor: [ 13 | 'bcoin/lib/bcoin-browser', 14 | 'bcash/lib/bcoin-browser', 15 | 'hsd/lib/hsd-browser', 16 | 'bledger/lib/bledger-browser', 17 | 'bmultisig/lib/bmultisig-browser', 18 | 'hs-client', 19 | 'bclient', 20 | 'react', 21 | 'react-redux', 22 | 'react-dom', 23 | '@bpanel/bpanel-ui', 24 | '@bpanel/bpanel-utils' 25 | ] 26 | }, 27 | output: { 28 | libraryTarget: 'umd', 29 | path: DIST_DIR, 30 | library: '[name]_lib', 31 | filename: 'vendor.js' 32 | }, 33 | node: { 34 | fs: 'empty' 35 | }, 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.css$/, 40 | use: [ 41 | { 42 | loader: MiniCssExtractPlugin.loader 43 | }, 44 | 'css-loader' 45 | ] 46 | }, 47 | { 48 | test: /\.(ttf|eot|svg|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 49 | loader: 'file-loader' 50 | } 51 | ] 52 | }, 53 | resolve: { 54 | modules: ['node_modules'], 55 | extensions: ['-browser.js', '.js', '.json'], 56 | // list of aliases are what packages plugins can list as peerDeps 57 | // this helps simplify plugin packages and ensures that parent classes 58 | // all point to the same instance, e.g bcoin.TX will be same for all plugins 59 | alias: { 60 | bcoin$: `${MODULES_DIR}/bcoin/lib/bcoin-browser`, 61 | bcash$: `${MODULES_DIR}/bcash/lib/bcoin-browser`, 62 | hsd$: `${MODULES_DIR}/hsd/lib/hsd-browser`, 63 | 'hs-client': `${MODULES_DIR}/hs-client` 64 | } 65 | }, 66 | plugins: [ 67 | new webpack.DllPlugin({ 68 | name: '[name]_lib', 69 | path: vendorManifest 70 | }), 71 | new MiniCssExtractPlugin({ 72 | filename: '[name].css', 73 | chunkFilename: '[id].css' 74 | }) 75 | ] 76 | }; 77 | -------------------------------------------------------------------------------- /configs/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const os = require('os'); 3 | const webpack = require('webpack'); 4 | 5 | const CompressionPlugin = require('compression-webpack-plugin'); 6 | const autoprefixer = require('autoprefixer'); 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 8 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 9 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 10 | 11 | const merge = require('webpack-merge'); 12 | const common = require('./webpack.config.js'); 13 | 14 | const loaders = { 15 | styleLoader: { 16 | loader: 'style-loader', 17 | options: { includePaths: ['node_modules'] } 18 | }, 19 | css: { 20 | loader: 'css-loader', 21 | options: { includePaths: ['node_modules'] } 22 | }, 23 | postcss: { 24 | loader: 'postcss-loader', 25 | options: { 26 | sourceMap: true, 27 | plugins: function() { 28 | return [autoprefixer]; 29 | } 30 | } 31 | } 32 | }; 33 | 34 | module.exports = function(env = {}) { 35 | const plugins = []; 36 | const config = common(env); 37 | return merge.smart(config, { 38 | mode: 'production', 39 | optimization: { 40 | minimizer: [ 41 | new UglifyJsPlugin({ 42 | cache: true, 43 | parallel: true, 44 | sourceMap: true // set to true if you want JS source maps 45 | }), 46 | new OptimizeCSSAssetsPlugin({}) 47 | ] 48 | }, 49 | module: { 50 | rules: [ 51 | { 52 | test: /\.css$/, 53 | fallback: loaders.styleLoader, 54 | use: [loaders.css, loaders.postcss] 55 | } 56 | ] 57 | }, 58 | plugins: [ 59 | new MiniCssExtractPlugin({ 60 | filename: '[name].[hash].css', 61 | chunkFilename: '[id].[hash].css' 62 | }), 63 | new CompressionPlugin({ 64 | test: /\.js$/, 65 | algorithm: 'gzip', 66 | asset: '[path].gz[query]' 67 | }) 68 | ] 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | bpanel: 4 | ## Build to use local Dockerfile 5 | # build: . 6 | ## Build from image from docker hub 7 | image: bpanel/bpanel:dev 8 | restart: unless-stopped 9 | environment: 10 | # Need this to tell the bpanel client 11 | # how to communicate with the bcoin node 12 | # in docker 13 | _DOCKER_NODE_HOST: bcoin 14 | # Edit this to add/remove plugins! This cannot be edited after startup 15 | # even with bpanel's config.js. Must edit below, and rebuild container. 16 | BPANEL_PLUGINS: "@bpanel/genesis-theme,@bpanel/connection-manager, @bpanel/recent-blocks, @bpanel/mempool-widget" 17 | ports: 18 | - "5000:5000" 19 | - "5001:5001" 20 | - "8000:8000" 21 | volumes: 22 | ## Mapping to local files- Handy for dev 23 | - ./webapp:/usr/src/app/webapp 24 | - ./server:/usr/src/app/server 25 | - ./package.json:/usr/src/app/package.json 26 | - ./package-lock.json:/usr/src/app/package-lock.json 27 | # - ./scripts:/usr/src/app/scripts 28 | # Use below to get configs written by bcoin service 29 | - configs:/root/.bpanel 30 | # Use below to copy local bpanel config files 31 | # - ~/.bpanel:/root/.bpanel 32 | ## Only MacOSX users need --watch-poll 33 | ## the client-id says to use the config created by the 34 | ## bcoin docker services 35 | command: --max_old_space_size=4096 server --dev --watch-poll --clear --client-id=_docker 36 | 37 | securityc: 38 | image: bpanel/securityc 39 | restart: unless-stopped 40 | ports: 41 | - "80:80" 42 | - "443:443" 43 | env_file: 44 | - ./securityc.env 45 | volumes: 46 | # mount local cert directory into container 47 | - certs:/etc/ssl/nginx 48 | 49 | bcoin: 50 | ## Use image to to pull from docker hub 51 | image: bpanel/bcoin 52 | ## Build to use local Dockerfile-bcoin 53 | # build: 54 | # context: . 55 | # dockerfile: Dockerfile-bcoin 56 | # args: 57 | # repo: bpanel-org/bcoin#experimental 58 | # rebuild: 0 59 | restart: unless-stopped 60 | env_file: 61 | # Set arguments to use in bcoin-init.js 62 | - ./etc/regtest.bcoin.env 63 | ports: 64 | ## Comment these if they conflict with something else you're running. 65 | #-- Mainnet 66 | - "8333:8333" 67 | - "8332:8332" # RPC 68 | - "8334:8334" # Wallet 69 | #-- Testnet 70 | - "18333:18333" 71 | - "18332:18332" # RPC 72 | - "18334:18334" # Wallet 73 | #-- Regtest 74 | - "48444:48444" 75 | - "48332:48332" # RPC 76 | - "48334:48334" # Wallet 77 | #-- Simnet 78 | - "18555:18555" 79 | - "18556:18556" # RPC 80 | - "18558:18558" # Wallet 81 | volumes: 82 | - ./scripts:/code/scripts 83 | # # Use below config to persist bcoin data in docker volume 84 | - bcoin:/data 85 | # # Use below config to write bcoin configs to a bpanel client 86 | - configs:/data/.bpanel 87 | # # Use below config instead to use bcoin data from local home dir 88 | # - ~/.bcoin:/data/.bcoin 89 | 90 | volumes: 91 | certs: 92 | bcoin: 93 | configs: 94 | driver_opts: 95 | type: none 96 | device: $HOME/.bpanel 97 | o: bind 98 | -------------------------------------------------------------------------------- /etc/regtest.bcoin.env: -------------------------------------------------------------------------------- 1 | # A sample env file 2 | # This is used by the bcoin docker service 3 | # for setting configs on a bcoin node in docker 4 | BCOIN_NETWORK=regtest 5 | BCOIN_HTTP_HOST=0.0.0.0 6 | BCOIN_WALLET_HTTP_HOST=0.0.0.0 7 | BCOIN_LOG_LEVEL=debug 8 | BCOIN_WORKERS=true 9 | BCOIN_LISTEN=true 10 | BCOIN_MEMORY=false 11 | BCOIN_NO_WALLET=true 12 | BCOIN_INDEX_TX=true 13 | BCOIN_INDEX_ADDRESS=true 14 | BCOIN_NODE_HOST=0.0.0.0 15 | BCOIN_INIT_SCRIPT=pagination-test-wallets.js 16 | -------------------------------------------------------------------------------- /etc/sample.bcoin.conf: -------------------------------------------------------------------------------- 1 | # # Sample bcoin config file (~/.bcoin/bcoin.conf) 2 | # can change this location by passing `prefix` 3 | # option to your node 4 | # If running non-mainnet node, this should be 5 | # in a sub-directory w/ the name of the network 6 | 7 | 8 | # 9 | # Options 10 | # 11 | 12 | # network: main 13 | 14 | # 15 | # Node 16 | # 17 | 18 | prefix: ~/.bcoin 19 | db: leveldb 20 | max-files: 64 21 | cache-size: 100 22 | 23 | # 24 | # Workers 25 | # 26 | 27 | workers: true 28 | # workers-size: 4 29 | # workers-timeout: 5000 30 | 31 | # 32 | # Logger 33 | # 34 | 35 | log-level: debug 36 | log-console: true 37 | log-file: true 38 | 39 | # 40 | # Chain 41 | # 42 | 43 | prune: false 44 | checkpoints: true 45 | coin-cache: 0 46 | entry-cache: 5000 47 | index-tx: false 48 | index-address: false 49 | 50 | # 51 | # Mempool 52 | # 53 | 54 | mempool-size: 100 55 | limit-free: true 56 | limit-free-relay: 15 57 | reject-absurd-fees: true 58 | replace-by-fee: false 59 | persistent-mempool: false 60 | 61 | # 62 | # Pool 63 | # 64 | 65 | selfish: false 66 | compact: true 67 | bip37: false 68 | bip151: true 69 | listen: true 70 | max-outbound: 8 71 | max-inbound: 30 72 | 73 | # Proxy Server (browser=websockets, node=socks) 74 | # proxy: foo:bar@127.0.0.1:9050 75 | # onion: true 76 | # upnp: true 77 | 78 | # Custom list of DNS seeds 79 | # seeds: seed.bitcoin.sipa.be 80 | 81 | # Local Host & Port (to listen on) 82 | host: :: 83 | # port: 8333 84 | 85 | # Public Host & Port (to advertise to peers) 86 | # public-host: 1.2.3.4 87 | # public-port: 8444 88 | 89 | # BIP151 AuthDB and Identity Key 90 | bip150: false 91 | identity-key: 74b4147957813b62cc8987f2b711ddb31f8cb46dcbf71502033da66053c8780a 92 | 93 | # Always try to connect to these nodes. 94 | # nodes: 127.0.0.1,127.0.0.2 95 | 96 | # Only try to connect to these nodes. 97 | # only: 127.0.0.1,127.0.0.2 98 | 99 | # 100 | # Miner 101 | # 102 | 103 | coinbase-flags: mined by bcoin 104 | # coinbase-address: 1111111111111111111114oLvT2,1111111111111111111114oLvT2 105 | preverify: false 106 | max-block-weight: 4000000 107 | reserved-block-weight: 4000 108 | reserved-block-sigops: 400 109 | 110 | # 111 | # HTTP 112 | # 113 | 114 | http-host: :: 115 | # http-port: 8332 116 | # ssl: true 117 | # ssl-cert: @/ssl/cert.crt 118 | # ssl-key: @/ssl/priv.key 119 | api-key: bikeshed 120 | # no-auth: false 121 | # cors: false 122 | 123 | # 124 | # bPanel specific 125 | # 126 | 127 | # init-script is used for starting up a bcoin node w/ 128 | # the bcoin service in bPanel's docker-compose 129 | init-script:setup-coinbase-address.js 130 | -------------------------------------------------------------------------------- /etc/sample.client.conf: -------------------------------------------------------------------------------- 1 | # # Sample bpanel client config file (~/.bpanel/clients/default.conf) 2 | 3 | network: regtest 4 | ## supports: bitcoin, bitcoincash, and handshake 5 | chain: bitcoin 6 | 7 | ## bcoin node information 8 | ## url can/will be created automatically if not set 9 | # url: http://1.2.3.4:48332 10 | 11 | ## url will be composed with port, protocol, and host options 12 | ## falls back to http://localhost and the rpc port for the network 13 | # port: 48332 14 | # protocol: http: 15 | host: 127.0.0.1 16 | 17 | api-key: bikeshed 18 | 19 | # wallet information (if also using wallet client) 20 | # preface with `wallet-` to indicate it's for wallet 21 | # wallet-port: 48334 22 | # wallet-api-key: walletbikeshed 23 | 24 | ## Admin token is a 64 character hex 25 | ## string that allows access to all admin routes. 26 | wallet-token: 3c42ddf505a7bd9579d19109b1c7762c5a8d28f9159fc86d2dc4e729fdd4e5b179c6cb0067f7fe0cf10ebca3af60bb296dd0b5cfe8aaf4a2a6072d34398dad18 27 | -------------------------------------------------------------------------------- /etc/sample.wallet.conf: -------------------------------------------------------------------------------- 1 | # Sample bcoin wallet config file (~/.bcoin/wallet.conf) 2 | # can change this location by passing `prefix` 3 | # option to your node 4 | # If running non-mainnet node, this should be 5 | # in a sub-directory w/ the name of the network 6 | 7 | # 8 | # Options 9 | # 10 | 11 | # network: main 12 | 13 | # 14 | # HTTP 15 | # 16 | 17 | http-host: :: 18 | # http-port: 8334 19 | # ssl: true 20 | # ssl-cert: @/ssl/cert.crt 21 | # ssl-key: @/ssl/priv.key 22 | api-key: bikeshed 23 | # no-auth: false 24 | # cors: false 25 | 26 | # 27 | # Wallet 28 | # 29 | 30 | witness: false 31 | checkpoints: true 32 | wallet-auth: false 33 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2018, The bPanel Devs (https://github.com/bpanel-org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | 24 | --- 25 | ## 3rd Party Licenses 26 | ### MIT License 27 | Copyright (c) 2017 Zeit, Inc. 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bpanel/bpanel", 3 | "version": "1.0.1-alpha", 4 | "homepage": "https://bpanel.org", 5 | "description": "GUI application to interact with a Bcoin Bitcoin node", 6 | "main": "server/index.js", 7 | "scripts": { 8 | "start": "node --max_old_space_size=4096 server --dev --watch-poll", 9 | "start:dev": "node --max_old_space_size=4096 server --dev", 10 | "start:prod": "node --max_old_space_size=4096 server", 11 | "start:poll": "npm run start", 12 | "version": "node scripts/version.js", 13 | "preinstall": "node scripts/preinstall.js", 14 | "clean": "npm run clear:plugins && rm -rf ./dist/*", 15 | "clear:plugins": "node server/clear-plugins.js", 16 | "build:plugins": "npm run clear:plugins && node server/build-plugins.js", 17 | "build:dll": "webpack --config ./configs/webpack.dll.config.js", 18 | "lint": "eslint webapp/ server/ configs/ scripts/", 19 | "test": "karma start configs/karma.conf.js", 20 | "test:watch": "karma start configs/karma.conf.js --autoWatch", 21 | "test:server": "mocha --reporter spec --watch server/test" 22 | }, 23 | "lint-staged": { 24 | "*.{js,json,css,jsx,scss}": [ 25 | "prettier --write", 26 | "git add" 27 | ] 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git@github.com:bpanel-org/bpanel.git" 32 | }, 33 | "keywords": [ 34 | "bcoin", 35 | "bpanel", 36 | "bitcoin", 37 | "GUI" 38 | ], 39 | "author": "bpanel", 40 | "license": "MIT", 41 | "engines": { 42 | "node": ">=8.9.4", 43 | "npm": ">=5.7.1" 44 | }, 45 | "dependencies": { 46 | "@bpanel/bpanel-ui": "0.0.19", 47 | "@bpanel/bpanel-utils": "0.1.9", 48 | "aphrodite": "1.2.5", 49 | "autoprefixer": "9.4.5", 50 | "base64-inline-loader": "1.1.1", 51 | "bcash": "bcoin-org/bcash", 52 | "bcfg": "0.1.4", 53 | "bclient": "0.1.7", 54 | "bcoin": "bcoin-org/bcoin", 55 | "bcrypto": "~4.3.2", 56 | "bcurl": "0.1.4", 57 | "bfile": "~0.1.3", 58 | "binet": "~0.3.3", 59 | "bledger": "~0.1.5", 60 | "blgr": "0.1.4", 61 | "bmultisig": "2.0.0-beta.1", 62 | "body-parser": "1.18.2", 63 | "bootstrap": "4.3.1", 64 | "bsert": "~0.0.5", 65 | "bsock": "bucko13/bsock#paths", 66 | "bsock-middleware": "1.1.5", 67 | "bstring": "~0.3.4", 68 | "bval": "^0.1.5", 69 | "bweb": "~0.1.6", 70 | "compression": "1.7.3", 71 | "cors": "2.8.4", 72 | "css-loader": "2.1.0", 73 | "effects-middleware": "1.0.1", 74 | "express": "4.16.2", 75 | "file-loader": "2.0.0", 76 | "font-awesome": "4.7.0", 77 | "hs-client": "~0.0.4", 78 | "hsd": "handshake-org/hsd#b75e48e5158b2157fb9f7d040dd4951f7d0f47fe", 79 | "husky": "0.15.0-rc.7", 80 | "level-js": "2.2.4", 81 | "lodash": "^4.17.15", 82 | "nodemon": "1.18.10", 83 | "postcss-loader": "3.0.0", 84 | "prettier": "1.7.4", 85 | "prop-types": "15.6.0", 86 | "react": "16.6.3", 87 | "react-dom": "16.6.3", 88 | "react-redux": "5.0.6", 89 | "react-router-dom": "4.3.1", 90 | "redux": "3.7.2", 91 | "redux-devtools-extension": "2.13.2", 92 | "redux-persist": "5.10.0", 93 | "redux-thunk": "2.2.0", 94 | "reselect": "3.0.1", 95 | "semver": "5.5.0", 96 | "style-loader": "0.23.1", 97 | "validate-npm-package-name": "3.0.0" 98 | }, 99 | "devDependencies": { 100 | "babel-cli": "6.26.0", 101 | "babel-core": "6.26.3", 102 | "babel-eslint": "8.0.1", 103 | "babel-loader": "7.1.2", 104 | "babel-plugin-syntax-dynamic-import": "6.18.0", 105 | "babel-plugin-transform-object-rest-spread": "6.26.0", 106 | "babel-plugin-transform-runtime": "6.23.0", 107 | "babel-polyfill": "6.26.0", 108 | "babel-preset-env": "1.7.0", 109 | "babel-preset-react": "6.24.1", 110 | "babel-preset-stage-0": "6.24.1", 111 | "babel-runtime": "6.26.0", 112 | "chai": "4.1.2", 113 | "compression-webpack-plugin": "3.1.0", 114 | "eslint": "4.18.2", 115 | "eslint-config-prettier": "2.6.0", 116 | "eslint-import-resolver-webpack": "0.8.3", 117 | "eslint-plugin-import": "2.8.0", 118 | "eslint-plugin-jsx-a11y": "6.0.2", 119 | "eslint-plugin-prettier": "2.3.1", 120 | "eslint-plugin-react": "7.4.0", 121 | "html-webpack-plugin": "3.2.0", 122 | "karma": "3.0.0", 123 | "karma-babel-preprocessor": "7.0.0", 124 | "karma-chai": "0.1.0", 125 | "karma-firefox-launcher": "1.1.0", 126 | "karma-mocha": "1.3.0", 127 | "karma-nyan-reporter": "0.2.5", 128 | "karma-phantomjs-launcher": "1.0.4", 129 | "karma-sinon": "1.0.5", 130 | "karma-sourcemap-loader": "0.3.7", 131 | "karma-webpack": "4.0.0-rc.5", 132 | "lint-staged": "4.3.0", 133 | "mini-css-extract-plugin": "0.4.2", 134 | "mocha": "4.0.1", 135 | "optimize-css-assets-webpack-plugin": "5.0.1", 136 | "sinon": "4.1.2", 137 | "source-map-loader": "0.2.2", 138 | "uglifyjs-webpack-plugin": "2.1.1", 139 | "webpack": "^4.41.5", 140 | "webpack-cli": "3.1.0", 141 | "webpack-merge": "4.1.4", 142 | "webpack-synchronizable-shell-plugin": "0.0.7" 143 | }, 144 | "peerDependencies": {}, 145 | "husky": { 146 | "hooks": { 147 | "pre-commit": "lint-staged", 148 | "post-commit": "node scripts/version.js" 149 | } 150 | }, 151 | "prettier": { 152 | "singleQuote": true 153 | }, 154 | "eslintIgnore": [ 155 | "**/*.js", 156 | "!server/**/*", 157 | "!webapp/**/*", 158 | "!scripts/**/*", 159 | "!.eslintrc.js", 160 | "!karma.conf.js", 161 | "!configs/*.config.js", 162 | "*node_modules*" 163 | ] 164 | } 165 | -------------------------------------------------------------------------------- /pkg.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * pkg.js - package constants 3 | * Copyright (c) 2018, bPanel Devs (MIT License). 4 | * https://github.com/bcoin-org/bcoin 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const pkg = exports; 10 | 11 | /** 12 | * Package Name 13 | * @const {String} 14 | * @default 15 | */ 16 | 17 | pkg.name = require('./package.json').name; 18 | 19 | /** 20 | * Project Name 21 | * @const {String} 22 | * @default 23 | */ 24 | 25 | pkg.core = 'bpanel'; 26 | 27 | /** 28 | * Organization Name 29 | * @const {String} 30 | * @default 31 | */ 32 | 33 | pkg.organization = 'bpanel-org'; 34 | 35 | /** 36 | * Config file name. 37 | * @const {String} 38 | * @default 39 | */ 40 | 41 | pkg.cfg = `${pkg.core}.conf`; 42 | 43 | /** 44 | * Repository URL. 45 | * @const {String} 46 | * @default 47 | */ 48 | 49 | pkg.url = `https://github.com/${pkg.organization}/${pkg.name}`; 50 | 51 | /** 52 | * Current version string. 53 | * @const {String} 54 | */ 55 | 56 | pkg.version = require('./package.json').version; 57 | 58 | /** 59 | * Supported blockchains 60 | * @const {Array} 61 | */ 62 | 63 | pkg.chains = ['bitcoin', 'bitcoincash', 'handshake']; 64 | 65 | /** 66 | * Supported user agents 67 | * @const {Array} 68 | */ 69 | 70 | pkg.agents = ['bcoin', 'bcash', 'hsd']; 71 | -------------------------------------------------------------------------------- /scripts/bcoin-init.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const bcoin = require('bcoin'); 3 | const fs = require('bfile'); 4 | const crypto = require('crypto'); 5 | const path = require('path'); 6 | const blgr = require('blgr'); 7 | const Config = require('bcfg'); 8 | 9 | // global variables 10 | let logger; 11 | let node; 12 | 13 | (async () => { 14 | try { 15 | logger = new blgr({ 16 | level: 'info' 17 | }); 18 | await logger.open(); 19 | logger.info('LOGGER OPEN'); 20 | 21 | /*** 22 | * Setup Configs 23 | ***/ 24 | const config = new Config('bcoin'); 25 | config.load({ env: true, argv: true, args: true }); 26 | 27 | // can optionally pass in a custom config file name 28 | // in either the environment variables (prefaced with `BCOIN_`) 29 | // or as a command line argument 30 | const file = config.str('config', 'bcoin.conf'); 31 | config.open(file); 32 | const network = bcoin.Network.get(config.str('network', 'main')); 33 | 34 | // set api key variables if none set in config 35 | if (!config.str('api-key')) 36 | config.inject({ apiKey: crypto.randomBytes(40).toString('hex') }); 37 | 38 | // node-api-key is used by wallet server to connect to node 39 | if (!config.str('node-api-key')) { 40 | config.inject({ nodeApiKey: config.str('api-key') }); 41 | } 42 | 43 | // admin token is used for wallet access 44 | if (!config.str('admin-token')) { 45 | config.inject({ adminToken: crypto.randomBytes(32).toString('hex') }); 46 | } 47 | 48 | /*** 49 | * Startup bcoin full node 50 | ***/ 51 | node = new bcoin.FullNode({ 52 | env: true, 53 | args: true, 54 | argv: true, 55 | config: true, 56 | apiKey: config.str('api-key'), 57 | network: config.str('network') 58 | }); 59 | 60 | node.on('error', e => logger.error('There was a node error: ', e)); 61 | logger.info(`Starting bcoin full node on ${config.str('network')} network`); 62 | 63 | await node.ensure(); 64 | await node.open(); 65 | await node.connect(); 66 | node.startSync(); 67 | logger.info('Starting node sync'); 68 | 69 | /*** 70 | * Start up a wallet node with bmultisig 71 | ***/ 72 | let wallet; 73 | if (!config.bool('no-multisig', false)) { 74 | const WalletNode = bcoin.wallet.Node; 75 | const bmultisig = require('bmultisig'); 76 | wallet = new WalletNode({ 77 | config: true, 78 | argv: true, 79 | env: true, 80 | loader: require, 81 | apiKey: config.str('api-key'), 82 | walletAuth: config.str('wallet-auth'), 83 | nodeApiKey: config.str('node-api-key'), 84 | adminToken: config.str('admin-token'), 85 | plugins: [bmultisig] 86 | }); 87 | wallet.on('error', e => logger.error('There was a wallet error: ', e)); 88 | 89 | logger.info('Starting wallet server'); 90 | await wallet.ensure(); 91 | await wallet.open(); 92 | } 93 | 94 | /*** 95 | * Check if there is an init script and run it 96 | ***/ 97 | const initScript = config.str('init-script'); 98 | const initScriptFilePath = 99 | initScript && path.resolve(__dirname, initScript); 100 | const initScriptExists = fs.existsSync(initScriptFilePath); 101 | if (!!initScript && initScriptExists) { 102 | logger.info( 103 | `Running init script ${initScriptFilePath} now to setup environment` 104 | ); 105 | // pass running node and config object 106 | // so script can interact with the node 107 | await require(initScriptFilePath)(node, config, logger, wallet); 108 | } 109 | 110 | /*** 111 | * Setup client configs for bPanel: 112 | * Write and put configs in shared docker volume (`configs`) 113 | ***/ 114 | const bpanelConfigDir = path.resolve(config.prefix, '.bpanel'); 115 | const dockerConfig = path.resolve(bpanelConfigDir, 'clients/_docker.conf'); 116 | if (!fs.existsSync(path.resolve(bpanelConfigDir, 'clients'))) 117 | fs.mkdirSync(path.resolve(bpanelConfigDir, 'clients')); 118 | 119 | // run if there is no config 120 | // skip if a `reset-configs` config is set to false 121 | if (!fs.existsSync(dockerConfig) || config.bool('reset-configs', true)) { 122 | logger.info('Creating client config for bPanel: ', dockerConfig); 123 | 124 | const confText = 125 | `network: ${network.type}\n` + 126 | `api-key:${config.str('api-key')}\n` + 127 | `wallet-api-key:${config.str('api-key')}\n` + 128 | `wallet-token:${config.str('admin-token')}`; 129 | 130 | fs.writeFileSync(dockerConfig, confText); 131 | } 132 | } catch (e) { 133 | logger.error(e.stack); 134 | node.close(); 135 | process.exit(1); 136 | } 137 | })(); 138 | -------------------------------------------------------------------------------- /scripts/createSecrets.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | const crypto = require('crypto'); 3 | 4 | /* 5 | * Use this script to generate secrets that can be used to secure bcoin 6 | */ 7 | 8 | const randomValue = crypto.randomBytes(40).toString('hex'); 9 | // admin token must be 32 bytes 10 | const adminToken = crypto.randomBytes(32).toString('hex'); 11 | 12 | // only log secrets if ran from the command line 13 | if (require.main === module) { 14 | console.log(`Secret Key: ${randomValue}`); 15 | console.log(`Admin Token: ${adminToken}`); 16 | } 17 | 18 | module.exports = { 19 | randomValue, 20 | adminToken 21 | }; 22 | -------------------------------------------------------------------------------- /scripts/funded-dummy-wallets.js: -------------------------------------------------------------------------------- 1 | const { protocol: { consensus } } = require('bcoin'); 2 | 3 | const makeWallets = async (node, config, logger, wallet) => { 4 | const network = node.network.type; 5 | const blocks2Mine = process.env.BLOCKS_2_MINE 6 | ? process.env.BLOCKS_2_MINE 7 | : 10; 8 | const miner = node.miner; 9 | const chain = node.chain; 10 | 11 | if (network === 'main' || network === 'testnet') 12 | logger.warning( 13 | `You probably don't want to be running the miner on the ${network} network. Mining 14 | on a production network can seriously impact performance of your host machine and is generally 15 | not recommended for docker containers.` 16 | ); 17 | 18 | // don't run if already have enough blocks 19 | if (chain.height > blocks2Mine) return; 20 | 21 | consensus.COINBASE_MATURITY = 0; 22 | let wdb; 23 | 24 | if (wallet) wdb = wallet.wdb; 25 | else wdb = node.require('walletdb').wdb; 26 | 27 | const primary = wdb.primary; 28 | 29 | primary.once('balance', async balance => { 30 | // eslint-disable-next-line no-console 31 | console.log('Primary gots some monies!', balance); 32 | }); 33 | 34 | const minerReceive = await primary.receiveAddress(); 35 | // eslint-disable-next-line no-console 36 | console.log('miner receive address: ', minerReceive); 37 | 38 | await miner.addAddress(minerReceive); 39 | let minedBlocks = 0; 40 | while (minedBlocks < blocks2Mine) { 41 | const entry = await chain.getEntry(node.chain.tip.hash); 42 | const block = await miner.mineBlock(entry, minerReceive); 43 | await node.chain.add(block); 44 | // eslint-disable-next-line no-console 45 | console.log('Block mined and added to chain: ', node.chain.tip.hash); 46 | minedBlocks++; 47 | } 48 | }; 49 | 50 | module.exports = makeWallets; 51 | -------------------------------------------------------------------------------- /scripts/pagination-test-wallets.js: -------------------------------------------------------------------------------- 1 | const { protocol: { consensus } } = require('bcoin'); 2 | 3 | module.exports = async (node, config, logger, wallet) => { 4 | consensus.COINBASE_MATURITY = 0; 5 | 6 | const miner = node.miner; 7 | const chain = node.chain; 8 | const network = node.network; 9 | const feeRate = network.minRelay * 10; // for some reason bc segwit??!! 10 | const wdb = wallet.wdb; 11 | 12 | const numInitBlocks = 144 * 3; // Initial blocks mined to activate SegWit. 13 | // Miner primary/default then evenly disperses 14 | // all funds to other wallet accounts 15 | 16 | const numTxBlocks = 10; // How many blocks to randomly fill with txs 17 | const numTxPerBlock = 10; // How many txs to try to put in each block 18 | // (due to the random tx-generation, some txs will fail due to lack of funds) 19 | 20 | const maxOutputsPerTx = 4; // Each tx will have a random # of outputs 21 | const minSend = 50000; // Each tx output will have a random value 22 | const maxSend = 100000; 23 | 24 | // We are going to bend time, and start our blockchain in the past! 25 | let virtualNow = network.now() - 60 * 10 * (numInitBlocks + numTxBlocks + 1); 26 | const blockInterval = 60 * 10; // ten minutes 27 | 28 | const walletNames = [ 29 | 'Powell', 30 | 'Yellen', 31 | 'Bernanke', 32 | 'Greenspan', 33 | 'Volcker', 34 | 'Miller', 35 | 'Burns', 36 | 'Martin', 37 | 'McCabe', 38 | 'Eccles' 39 | ]; 40 | 41 | const accountNames = ['hot', 'cold']; 42 | 43 | const wallets = []; 44 | 45 | const mineRegtestBlock = async function(coinbaseAddr) { 46 | const entry = await chain.getEntry(node.chain.tip.hash); 47 | const block = await miner.mineBlock(entry, coinbaseAddr); 48 | await node.chain.add(block); 49 | }; 50 | 51 | const mineRegtestBlockToPast = async function(coinbaseAddr) { 52 | const entry = await chain.getEntry(node.chain.tip.hash); 53 | const job = await miner.createJob(entry, coinbaseAddr); 54 | job.attempt.time = virtualNow; 55 | virtualNow += blockInterval; 56 | job.refresh(); 57 | const block = await job.mineAsync(); 58 | await node.chain.add(block); 59 | }; 60 | 61 | logger.info('Creating wallets and accounts...'); 62 | for (const wName of walletNames) { 63 | try { 64 | const newWallet = await wdb.create({ 65 | id: wName, 66 | witness: Math.random() < 0.5 67 | }); 68 | 69 | wallets.push(newWallet); 70 | 71 | for (const aName of accountNames) { 72 | await newWallet.createAccount({ 73 | name: aName, 74 | witness: Math.random() < 0.5 75 | }); 76 | } 77 | } catch (e) { 78 | logger.error(`Error creating wallet ${wName}:`, e.message); 79 | } 80 | } 81 | 82 | if (!wallets.length) { 83 | logger.info('No wallets created, likely this script has already been run'); 84 | return; 85 | } 86 | accountNames.push('default'); 87 | 88 | logger.info('Mining initial blocks...'); 89 | const primary = wdb.primary; 90 | const minerReceive = await primary.receiveAddress(); 91 | await miner.addAddress(minerReceive); 92 | for (let i = 0; i < numInitBlocks; i++) { 93 | await mineRegtestBlockToPast(minerReceive); 94 | } 95 | 96 | logger.info('Ensure wallet is caught up before proceeding...'); 97 | await wdb.rescan(0); 98 | 99 | logger.info('Air-dropping funds to the people...'); 100 | const balance = await primary.getBalance(0); 101 | 102 | const totalAmt = balance.confirmed; 103 | const amtPerAcct = Math.floor( 104 | totalAmt / (walletNames.length * accountNames.length) 105 | ); 106 | const outputs = []; 107 | for (const wallet of wallets) { 108 | for (const aName of accountNames) { 109 | const recAddr = await wallet.receiveAddress(aName); 110 | outputs.push({ 111 | value: amtPerAcct, 112 | address: recAddr 113 | }); 114 | } 115 | } 116 | 117 | await primary.send({ 118 | outputs: outputs, 119 | rate: feeRate, 120 | subtractFee: true 121 | }); 122 | 123 | logger.info('Confirming airdrop...'); 124 | await mineRegtestBlockToPast(minerReceive); 125 | 126 | logger.info('Creating a big mess!...'); 127 | for (let b = 0; b < numTxBlocks; b++) { 128 | for (let t = 0; t < numTxPerBlock; t++) { 129 | // Randomly select recipients for this tx 130 | const outputs = []; 131 | const numOutputs = Math.floor(Math.random() * maxOutputsPerTx) + 1; 132 | for (let o = 0; o < numOutputs; o++) { 133 | const recWallet = wallets[Math.floor(Math.random() * wallets.length)]; 134 | const recAcct = 135 | accountNames[Math.floor(Math.random() * accountNames.length)]; 136 | 137 | const recAddr = await recWallet.receiveAddress(recAcct); 138 | const value = Math.floor( 139 | Math.random() * (maxSend - minSend) + minSend / numOutputs 140 | ); 141 | outputs.push({ 142 | value: value, 143 | address: recAddr 144 | }); 145 | } 146 | 147 | // Randomly choose a sender for this tx 148 | const sendWallet = wallets[Math.floor(Math.random() * wallets.length)]; 149 | const sendAcct = accountNames[Math.floor(Math.random() * wallets.length)]; 150 | try { 151 | const tx = await sendWallet.send({ 152 | account: sendAcct, 153 | outputs: outputs, 154 | rate: feeRate, 155 | subtractFee: true 156 | }); 157 | } catch (e) { 158 | logger.error(`Problem sending tx: ${e}`); 159 | } 160 | } 161 | 162 | // CONFIRM 163 | await mineRegtestBlockToPast(minerReceive); 164 | } 165 | 166 | logger.info('All done! Go play.'); 167 | }; 168 | -------------------------------------------------------------------------------- /scripts/preinstall.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // no non-native modules should be imported here 4 | // since this file gets run BEFORE `npm install` 5 | const fs = require('fs'); 6 | const os = require('os'); 7 | const path = require('path'); 8 | const { execSync } = require('child_process'); 9 | 10 | const semver = require('../vendor/semver'); 11 | 12 | const BPANEL_DIR = path.resolve(os.homedir(), '.bpanel'); 13 | const CONFIGS_FILE = path.resolve(BPANEL_DIR, './config.js'); 14 | const SECRETS_FILE = path.resolve(BPANEL_DIR, './secrets.json'); 15 | const CLIENTS_DIR = path.resolve(BPANEL_DIR, 'clients'); 16 | const LOCAL_PLUGINS_DIR = path.resolve(BPANEL_DIR, 'local_plugins'); 17 | 18 | const configText = `module.exports = { 19 | plugins: ['@bpanel/genesis-theme', '@bpanel/settings', '@bpanel/connection-manager'], 20 | localPlugins: [], 21 | }`; 22 | 23 | // simple utility to remove whitespace from string 24 | function trim(string) { 25 | return string.replace(/^\s+|\s+$/g, ''); 26 | } 27 | 28 | try { 29 | // check minimum version of npm 30 | let npmVersion = execSync('npm --version', { 31 | encoding: 'utf8' 32 | }); 33 | let nodeVersion = process.version; 34 | 35 | npmVersion = trim(npmVersion); 36 | 37 | const npmMin = process.env.npm_package_engines_npm; 38 | const nodeMin = process.env.npm_package_engines_node; 39 | 40 | if ( 41 | !semver.satisfies(npmVersion, npmMin) || 42 | !semver.satisfies(nodeVersion, nodeMin) 43 | ) 44 | throw new Error( 45 | `bPanel requires npm version ${npmMin} and node version ${nodeMin}. \ 46 | You are running npm ${npmVersion} and node ${nodeVersion}. Please check your $PATH variable, \ 47 | update and try again.` 48 | ); 49 | 50 | if (!fs.existsSync(BPANEL_DIR)) { 51 | console.log( 52 | `info: No module directory found. Creating one at ${BPANEL_DIR}` 53 | ); 54 | fs.mkdirSync(BPANEL_DIR); 55 | } 56 | 57 | if (!fs.existsSync(CONFIGS_FILE)) { 58 | console.log( 59 | `info: No configuration file found. Initializing one at ${CONFIGS_FILE}` 60 | ); 61 | fs.appendFileSync(CONFIGS_FILE, configText); 62 | } 63 | 64 | if (!fs.existsSync(LOCAL_PLUGINS_DIR)) { 65 | console.log( 66 | `info: No local plugins directory file found. Creating one at ${LOCAL_PLUGINS_DIR}` 67 | ); 68 | fs.mkdirSync(LOCAL_PLUGINS_DIR); 69 | } 70 | 71 | if (!fs.existsSync(SECRETS_FILE)) { 72 | console.log(`info: No secrets file found. Creating one at ${SECRETS_FILE}`); 73 | fs.appendFileSync(SECRETS_FILE, JSON.stringify({})); 74 | } 75 | 76 | if (!fs.existsSync(CLIENTS_DIR)) { 77 | console.log( 78 | `info: No clients directory file found. Creating one at ${CLIENTS_DIR}` 79 | ); 80 | fs.mkdirSync(CLIENTS_DIR); 81 | } 82 | 83 | require('./version.js'); 84 | } catch (e) { 85 | console.error('There was a problem initializing the project: ', e.stack); 86 | process.exit(1); 87 | } 88 | -------------------------------------------------------------------------------- /scripts/setup-coinbase-address.js: -------------------------------------------------------------------------------- 1 | const { WalletClient } = require('bclient'); 2 | const blgr = require('blgr'); 3 | const { Network } = require('bcoin'); 4 | 5 | // setup coinbase addresses for miner 6 | // must be done at runtime, otherwise a 7 | // potential security problem 8 | module.exports = async (node, config) => { 9 | const logger = new blgr({ 10 | level: 'info' 11 | }); 12 | await logger.open(); 13 | const network = Network.get(config.str('network', 'main')); 14 | const walletClient = new WalletClient({ 15 | port: config.int('wallet-port', network.walletPort), 16 | apiKey: config.str('api-key'), 17 | token: config.str('admin-token') 18 | }); 19 | 20 | // allow for runtime configuration of which 21 | // address to use for coinbase transactions 22 | // TODO: come up with generalized way to pass args to runtime scripts 23 | const COINBASE_WALLET_ID = config.str('coinbase-wallet-id', 'primary'); 24 | const COINBASE_ACCOUNT_ID = config.str('coinbase-account-id', 'default'); 25 | 26 | logger.info('Fetching coinbase address'); 27 | const { receiveAddress } = await walletClient.getAccount( 28 | COINBASE_WALLET_ID, 29 | COINBASE_ACCOUNT_ID 30 | ); 31 | 32 | await node.miner.addAddress(receiveAddress); 33 | logger.info(`Set miner coinbase address: ${receiveAddress}`); 34 | }; 35 | -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | // create webapp/version.json file 2 | // Looks at the git tags and sha to output the version. 3 | 4 | let commit, version; 5 | const fs = require('fs'); 6 | const { execSync } = require('child_process'); 7 | 8 | try { 9 | commit = execSync('git rev-parse HEAD', { stdio: [] }).toString(); 10 | } catch (e) { 11 | // eslint-disable-next-line no-console 12 | console.error('Problem getting git commit:', e.message); 13 | } 14 | try { 15 | version = require('../package.json').version; 16 | } catch (e) { 17 | // eslint-disable-next-line no-console 18 | console.error('Problem getting bPanel version:', e.message); 19 | } 20 | 21 | fs.writeFileSync('webapp/version.json', JSON.stringify({ commit, version })); 22 | -------------------------------------------------------------------------------- /securityc.env: -------------------------------------------------------------------------------- 1 | CA_COMMON_NAME=bpanel 2 | CERT_COMMON_NAME=localhost 3 | CERT_IP=127.0.0.1 4 | CERT_DOMAIN=localhost 5 | 6 | CA_IN=/etc/ssl/nginx/ca.crt 7 | CA_KEY_IN=/etc/ssl/nginx/ca.key 8 | 9 | CA_OUT=/etc/ssl/nginx/ca.crt 10 | CA_KEY_OUT=/etc/ssl/nginx/ca.key 11 | 12 | CERT_OUT=/etc/ssl/nginx/tls.crt 13 | KEY_OUT=/etc/ssl/nginx/tls.key 14 | 15 | USE_NGINX=true 16 | NGINX_SSL_CERTIFICATE=/etc/ssl/nginx/tls.crt 17 | NGINX_SSL_CERTIFICATE_KEY=/etc/ssl/nginx/tls.key 18 | NGINX_UPSTREAM_URI=bpanel:5000 19 | -------------------------------------------------------------------------------- /server/build-plugins.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('bfile'); 4 | const os = require('os'); 5 | const assert = require('bsert'); 6 | const { resolve } = require('path'); 7 | const Config = require('bcfg'); 8 | const { format } = require('prettier'); 9 | const { execSync } = require('child_process'); 10 | const validate = require('validate-npm-package-name'); 11 | 12 | const { createLogger } = require('./logger'); 13 | const { npmExists } = require('./utils'); 14 | 15 | const config = new Config('bpanel'); 16 | config.load({ env: true, argv: true, arg: true }); 17 | const PLUGINS_CONFIG = resolve(config.prefix, 'config.js'); 18 | const MODULES_DIRECTORY = resolve(__dirname, '../node_modules'); 19 | const PLUGINS_PATH = resolve(__dirname, '../webapp/plugins'); 20 | 21 | // location of app configs and local plugins 22 | // defaults to ~/.bpanel/ 23 | // this is set using bcfg at runtime with server 24 | // and passed through via webpack to this script 25 | const HOME_PREFIX = 26 | process.env.BPANEL_PREFIX || resolve(os.homedir(), '.bpanel'); 27 | 28 | const getPackageName = name => { 29 | if (name.indexOf('/') !== -1 && name[0] !== '@') { 30 | // this is a GitHub repo 31 | return name.split('/')[1]; 32 | } else { 33 | return name; 34 | } 35 | }; 36 | 37 | async function installRemotePackages(installPackages) { 38 | const logger = createLogger(); 39 | await logger.open(); 40 | const pkgStr = installPackages.reduce((str, name) => { 41 | if (validate(name).validForNewPackages) str = `${str} ${name}`; 42 | return str; 43 | }); 44 | logger.info(`Installing plugin packages: ${pkgStr.split(' ')}`); 45 | 46 | try { 47 | execSync(`npm install --no-save ${pkgStr} --production`, { 48 | stdio: [0, 1, 2], 49 | cwd: resolve(__dirname, '..') 50 | }); 51 | logger.info('Done installing remote plugins'); 52 | } catch (e) { 53 | logger.error( 54 | 'There was an error installing plugins. Sometimes this is because of permissions errors \ 55 | in node_modules. Try deleting the node_modules directory and running `npm install` again.' 56 | ); 57 | logger.error(e.stack); 58 | await logger.close(); 59 | process.exit(1); 60 | } finally { 61 | await logger.close(); 62 | } 63 | } 64 | 65 | /* 66 | * Adds a symlink for a specific package from the local_plugins 67 | * directory in BPANEL_PREFIX to the local app's node_modules. 68 | * This allows webpack to watch for changes. 69 | */ 70 | async function symlinkLocal(packageName) { 71 | const logger = createLogger(); 72 | await logger.open(); 73 | 74 | logger.info(`Creating symlink for local plugin ${packageName}...`); 75 | const pkgDir = resolve(MODULES_DIRECTORY, packageName); 76 | 77 | // remove existing version of plugin 78 | const exists = fs.existsSync(pkgDir); 79 | if (exists) { 80 | logger.info( 81 | `While preparing symlink for ${packageName}, found existing copy. Overwriting with linked local verison` 82 | ); 83 | const stat = await fs.lstat(pkgDir); 84 | 85 | // if it exists but is a symlink 86 | // remove symlink so we can replace with our new one 87 | if (stat.isSymbolicLink()) await fs.unlink(pkgDir); 88 | else 89 | // otherwise remove the old directory 90 | await fs.rimraf(pkgDir); 91 | } 92 | 93 | // for scoped packages, the scope becomes a parent directory 94 | // if the parent directory doesn't exist, we need to create it 95 | if (packageName.startsWith('@')) { 96 | // if scoped, get directory name 97 | const pathIndex = packageName.indexOf('/'); 98 | assert( 99 | pathIndex > -1, 100 | 'Scoped package name should have child path with "/" separator' 101 | ); 102 | const scopeName = packageName.substring(0, pathIndex); 103 | const scopePath = resolve(MODULES_DIRECTORY, scopeName); 104 | const scopeExists = await fs.existsSync(scopePath); 105 | 106 | // make directory if it did not exist 107 | if (!scopeExists) await fs.mkdir(scopePath); 108 | } 109 | 110 | // if the origin does not exist, log an error 111 | const originPath = resolve(HOME_PREFIX, 'local_plugins', packageName); 112 | if (!fs.existsSync(originPath)) 113 | logger.error(`Origin package did not exist at ${originPath}, skipping...`); 114 | else 115 | await fs.symlink( 116 | resolve(HOME_PREFIX, 'local_plugins', packageName), 117 | resolve(MODULES_DIRECTORY, pkgDir) 118 | ); 119 | 120 | await logger.close(); 121 | } 122 | 123 | // a utility method to check if a module exists in node_modules 124 | // useful for confirming if a plugin has already been installed 125 | async function checkForModuleExistence(pkg) { 126 | const pkgPath = resolve(MODULES_DIRECTORY, pkg); 127 | const exists = fs.existsSync(pkgPath); 128 | return exists; 129 | } 130 | 131 | // get names of plugins that are available local to the project 132 | async function getLocalPlugins() { 133 | const localPath = resolve(PLUGINS_PATH, 'local'); 134 | const contents = fs.readdirSync(localPath); 135 | const plugins = []; 136 | for (let i = 0; i < contents.length; i++) { 137 | const name = contents[i]; 138 | const stats = fs.lstatSync(resolve(localPath, name)); 139 | 140 | // only add directories and non system/hidden directories 141 | if (stats.isDirectory() && name[0] !== '.') plugins.push(name); 142 | } 143 | return plugins; 144 | } 145 | 146 | async function prepareModules(plugins = [], local = true, network = false) { 147 | const logger = createLogger(); 148 | await logger.open(); 149 | 150 | let pluginsIndex = local 151 | ? '// exports for all local plugin modules\n\n' 152 | : '// exports for all published plugin modules\n\n'; 153 | 154 | let exportsText = 'export default async function() { \n return Promise.all(['; 155 | let installPackages = []; 156 | 157 | // get any plugins local to the project if building local plugins 158 | if (local) { 159 | const localPlugins = await getLocalPlugins(); 160 | plugins.push(...localPlugins); 161 | } 162 | 163 | // Create the index.js files for exposing the plugins 164 | for (let i = 0; i < plugins.length; i++) { 165 | const name = plugins[i]; 166 | const packageName = getPackageName(name); 167 | try { 168 | const validator = validate(packageName); 169 | 170 | // make sure that we are working with valid package names 171 | // this is important to avoid injecting arbitrary scripts in 172 | // later execSync steps. 173 | assert( 174 | validator.validForNewPackages, 175 | `${packageName} is not a valid package name and will not be installed: ${validator.errors && 176 | validator.errors.join(', ')}` 177 | ); 178 | 179 | let modulePath; 180 | 181 | // check if the plugin exists in webapp/plugins/local 182 | // and import from there if it does 183 | const existsLocal = fs.existsSync( 184 | resolve(PLUGINS_PATH, 'local', packageName) 185 | ); 186 | 187 | // can skip remote check if no nework connection 188 | let existsRemote = true; 189 | if (network) { 190 | // if adding a remote plugin and it doesn't exist on npm, skip 191 | existsRemote = !local && (await npmExists(packageName)); 192 | if (!local && !existsRemote) { 193 | logger.error( 194 | `Remote module ${packageName} does not exist on npm. If developing locally, add to local plugins. Skipping...` 195 | ); 196 | continue; 197 | } 198 | } 199 | 200 | if (existsLocal && local) 201 | // maintain support for plugins in plugins/local dir 202 | modulePath = `./${packageName}`; 203 | else 204 | // set import to webpack's alias for bpanel's local_plugins dir 205 | modulePath = local ? `&local/${packageName}` : packageName; 206 | 207 | // add plugin to list of packages that need to be installed w/ npm 208 | if (!local && existsRemote) installPackages.push(name); 209 | else if (local && !existsLocal) { 210 | // create a symlink for local modules in [PREFIX]/local_plugins to node_modules 211 | // so that webpack can watch for changes 212 | await symlinkLocal(name); 213 | } 214 | 215 | // only add imports for packages that have been installed 216 | if (existsLocal || fs.existsSync(resolve(MODULES_DIRECTORY, packageName))) 217 | exportsText += `import('${modulePath}'),`; 218 | else if (!local && existsRemote) 219 | exportsText += `import('${modulePath}'),`; 220 | } catch (e) { 221 | logger.error(`There was an error preparing ${packageName}`); 222 | logger.error(e.stack); 223 | } 224 | } 225 | 226 | // Installation step 227 | if (installPackages.length) { 228 | try { 229 | if (resolve(process.cwd(), 'server') != __dirname) { 230 | // HACK: When required, we need to install the plugin peer-dependencies 231 | logger.info('Installing base packages...'); 232 | execSync('npm install --production', { 233 | stdio: [0, 1, 2], 234 | cwd: resolve(__dirname, '..') 235 | }); 236 | } 237 | if (!local) { 238 | // check if modules need to be installed by confirming 239 | // if any plugins don't exist yet in our node_modules 240 | let newModules = false; 241 | for (let i = 0; i < installPackages.length; i++) { 242 | const pkg = installPackages[i]; 243 | newModules = !await checkForModuleExistence(pkg); 244 | if (newModules) break; 245 | } 246 | 247 | // if there is no network connection, log message 248 | if (!network) 249 | logger.info( 250 | 'Skipping npm install of remote plugins due to lack of network connection' 251 | ); 252 | else if (newModules) 253 | // if there are new modules, install them with npm 254 | await installRemotePackages(installPackages); 255 | else 256 | logger.info('No new remote plugins to install. Skipping npm install'); 257 | } 258 | } catch (e) { 259 | logger.error('Error installing plugins packages: ', e); 260 | } finally { 261 | await logger.close(); 262 | } 263 | } 264 | 265 | exportsText += ']); \n }'; 266 | pluginsIndex += exportsText; 267 | pluginsIndex = format(pluginsIndex, { singleQuote: true, parser: 'babylon' }); 268 | 269 | const pluginsIndexPath = local ? 'local/index.js' : 'index.js'; 270 | await fs.writeFile(resolve(PLUGINS_PATH, pluginsIndexPath), pluginsIndex); 271 | return true; 272 | } 273 | 274 | (async () => { 275 | const logger = createLogger(); 276 | await logger.open(); 277 | try { 278 | assert( 279 | fs.existsSync(PLUGINS_CONFIG), 280 | 'bPanel config file not found. Please run `npm install` before \ 281 | starting the server and building the app to automatically generate \ 282 | your config file.' 283 | ); 284 | 285 | const { localPlugins, plugins } = require(PLUGINS_CONFIG); 286 | 287 | // CLI & ENV plugins override configuration file 288 | const envPlugins = config.str('plugins'); 289 | 290 | // get network status for dealing with remote plugins 291 | let network = false; 292 | const EXTERNAL_URI = process.env.EXTERNAL_URI || 'npmjs.com'; 293 | require('dns').lookup(EXTERNAL_URI, async err => { 294 | if (err && err.code === 'ENOTFOUND') 295 | logger.error(`No network connection found.`); 296 | else { 297 | network = true; 298 | } 299 | // prepare remote plugins 300 | await prepareModules( 301 | envPlugins ? envPlugins.split(',').map(s => s.trim(s)) : plugins, 302 | false, 303 | network 304 | ); 305 | // prepare local plugins 306 | await prepareModules(localPlugins, true, network); 307 | }); 308 | } catch (err) { 309 | logger.error('There was an error preparing modules: ', err.stack); 310 | } finally { 311 | await logger.close(); 312 | } 313 | })(); 314 | -------------------------------------------------------------------------------- /server/clear-plugins.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('bfile'); 4 | const { createLogger } = require('./logger'); 5 | const { resolve } = require('path'); 6 | 7 | module.exports = async () => { 8 | const indexText = 9 | 'export default async function() { return Promise.all([]); }'; 10 | const pluginsPath = resolve(__dirname, '../webapp/plugins'); 11 | try { 12 | fs.writeFileSync(resolve(pluginsPath, 'local/index.js'), indexText); 13 | fs.writeFileSync(resolve(pluginsPath, 'index.js'), indexText); 14 | } catch (e) { 15 | const logger = createLogger(); 16 | await logger.open(); 17 | logger.error('There was an error clearing plugins: ', e); 18 | await logger.close(); 19 | } 20 | }; 21 | 22 | if (require.main === module) { 23 | module.exports(); 24 | } 25 | -------------------------------------------------------------------------------- /server/endpoints/clients.js: -------------------------------------------------------------------------------- 1 | const { 2 | clientsHandler, 3 | getClientsInfo, 4 | getDefaultClientInfo, 5 | getConfigHandler, 6 | testClientsHandler 7 | } = require('../handlers/clients'); 8 | 9 | const { GET, USE } = require('./methods'); 10 | 11 | const base = '/clients'; 12 | 13 | module.exports = [ 14 | { 15 | method: GET, 16 | path: base.concat('/'), 17 | handler: getClientsInfo 18 | }, 19 | { 20 | method: GET, 21 | path: base.concat('/default'), 22 | handler: getDefaultClientInfo 23 | }, 24 | { 25 | method: USE, 26 | path: base.concat('/:id/:client'), 27 | handler: clientsHandler 28 | }, 29 | { 30 | method: USE, 31 | path: base.concat('/:id'), 32 | handler: testClientsHandler 33 | }, 34 | { 35 | method: GET, 36 | path: base.concat('/:id'), 37 | handler: getConfigHandler 38 | } 39 | ]; 40 | -------------------------------------------------------------------------------- /server/endpoints/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module endpoints 5 | */ 6 | 7 | exports.clients = require('./clients'); 8 | -------------------------------------------------------------------------------- /server/endpoints/methods.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | GET: 'GET', 3 | POST: 'POST', 4 | PUT: 'PUT', 5 | DELETE: 'DELETE', 6 | USE: 'USE' 7 | }; 8 | -------------------------------------------------------------------------------- /server/handlers/clients.js: -------------------------------------------------------------------------------- 1 | const Config = require('bcfg'); 2 | const assert = require('bsert'); 3 | 4 | const { configHelpers, clientFactory } = require('../utils'); 5 | const { 6 | getDefaultConfig, 7 | testConfigOptions, 8 | getConfig, 9 | loadConfig 10 | } = configHelpers; 11 | 12 | // utility to return basic info about a client based on its config 13 | function getClientInfo(config, clientHealth) { 14 | assert(config instanceof Config, 'Must pass a bcfg Config object'); 15 | const info = { 16 | id: config.str('id'), 17 | chain: config.str('chain', 'bitcoin'), 18 | services: { 19 | node: config.bool('node', true), 20 | wallet: config.bool('wallet', true), 21 | multisig: config.bool('multisig', false) 22 | } 23 | }; 24 | if (clientHealth) { 25 | const node = clientHealth.errors ? !clientHealth.errors.node : true; 26 | const wallet = clientHealth.errors ? !clientHealth.errors.wallet : true; 27 | const multisig = clientHealth.errors ? !clientHealth.errors.multisig : true; 28 | info.services = { 29 | node: config.bool('node', node), 30 | wallet: config.bool('wallet', wallet), 31 | multisig: config.bool('multisig', multisig) 32 | }; 33 | } 34 | 35 | return info; 36 | } 37 | 38 | function getClientsInfo(req, res) { 39 | const { logger, clients } = req; 40 | const clientInfo = {}; 41 | 42 | for (let [, client] of clients) { 43 | if (!client.str('chain')) 44 | logger.warning( 45 | `Client config ${client.str( 46 | 'id' 47 | )} had no chain set, defaulting to 'bitcoin'` 48 | ); 49 | clientInfo[client.str('id')] = getClientInfo(client, req.clientHealth); 50 | } 51 | 52 | return res.status(200).json(clientInfo); 53 | } 54 | 55 | async function getDefaultClientInfo(req, res, next) { 56 | const { config } = req; 57 | let defaultClientConfig; 58 | try { 59 | defaultClientConfig = await getDefaultConfig(config); 60 | if (!defaultClientConfig || !config) 61 | return res.status(404).json({ 62 | error: { 63 | message: `Sorry, there was no default client available`, 64 | code: 404 65 | } 66 | }); 67 | const defaultClient = getClientInfo(defaultClientConfig, req.clientHealth); 68 | return res.status(200).json(defaultClient); 69 | } catch (e) { 70 | next(e); 71 | } 72 | } 73 | 74 | async function clientsHandler(req, res) { 75 | let token; 76 | const { method, path, body, query, params, logger, clients } = req; 77 | const { id } = params; 78 | 79 | if (!clients.has(id)) 80 | return res.status(404).json({ 81 | error: { 82 | message: `Sorry, there was no client with the id ${id}`, 83 | code: 404 84 | } 85 | }); 86 | 87 | const config = clients.get(id); 88 | 89 | assert(config instanceof Config, 'client needs bcfg config'); 90 | 91 | const { nodeClient, walletClient, multisigClient } = clientFactory(config); 92 | 93 | const reqClients = { 94 | node: nodeClient, 95 | wallet: walletClient, 96 | multisig: multisigClient 97 | }; 98 | 99 | const client = reqClients[params.client]; 100 | 101 | if (!client) 102 | return res.status(404).json({ 103 | error: { 104 | message: `Requested client ${params.client} for ${id} does not exist`, 105 | code: 404 106 | } 107 | }); 108 | 109 | /* 110 | * this part of the handler is the proxy to the nodes that the clients 111 | * are communicating with 112 | */ 113 | 114 | // use query params for GET request, otherwise use body 115 | const payload = method === 'GET' ? query : body; 116 | try { 117 | logger.debug( 118 | `client: ${client.constructor.name}, method: ${method},`, 119 | `path: ${path}` 120 | ); 121 | logger.debug('query:', query); 122 | logger.debug('body:', body); 123 | 124 | // proxy the token 125 | token = client.token; 126 | if ('token' in payload) { 127 | client.token = payload.token; 128 | logger.debug('Using custom client token'); 129 | } 130 | const response = await client.request(method, path, payload); 131 | logger.debug('server response:', response ? response : 'null'); 132 | if (response) return res.status(200).json(response); 133 | // return 404 when response is null due to 134 | // resource not being found on server 135 | return res.status(404).json({ error: { message: 'resource not found' } }); 136 | } catch (e) { 137 | logger.error(`Error querying ${client.constructor.name}:`, e); 138 | return res 139 | .status(502) 140 | .send({ error: { message: e.message, code: e.code, type: e.type } }); 141 | } finally { 142 | // always reassign the original token 143 | client.token = token; 144 | } 145 | } 146 | 147 | // a middleware to check the health of requested client 148 | // only operates if has query param `health` set to true 149 | // attaches `clientHealth` to req object 150 | async function testClientsHandler(req, res, next) { 151 | const { logger, query, params, body } = req; 152 | let config, configOptions; 153 | if ((query && query.health) || (body && body.health)) { 154 | const { id } = params; 155 | const { options } = req.body; 156 | configOptions = { id }; 157 | 158 | if (options) configOptions = { ...configOptions, ...options }; 159 | 160 | // get original configs to merge any missing items if updating 161 | try { 162 | const { data } = getConfig(id); 163 | configOptions = { ...data, ...configOptions }; 164 | } catch (e) { 165 | // if missing config, can disregard 166 | if (e.code === 'ENOENT') 167 | logger.debug('No existing config with id: %s', id); 168 | else next(e); 169 | } 170 | 171 | const clientHealth = {}; 172 | 173 | try { 174 | config = loadConfig(configOptions.id, configOptions); 175 | config.set('logger', logger); 176 | logger.info('Checking health of client "%s"...', id); 177 | const [err, clientErrors] = await testConfigOptions(config); 178 | if (!err) { 179 | clientHealth.healthy = true; 180 | logger.info('Client "%s" is healthy', id); 181 | } else { 182 | clientHealth.failed = clientErrors.failed; 183 | clientHealth.errors = clientErrors; 184 | clientHealth.healthy = false; 185 | logger.warning('Problem checking configs for client "%s": ', id); 186 | logger.warning(clientErrors.message); 187 | } 188 | // attach clientHealth to request object 189 | req.clientHealth = clientHealth; 190 | } catch (e) { 191 | return next(e); 192 | } 193 | } 194 | next(); 195 | } 196 | 197 | async function getConfigHandler(req, res) { 198 | const { logger, clientHealth } = req; 199 | let config; 200 | try { 201 | config = await getConfig(req.params.id); 202 | } catch (e) { 203 | logger.error(e); 204 | if (e.code === 'ENOENT') 205 | return res.status(404).json({ 206 | error: { 207 | message: `Config for '${req.params.id}' not found`, 208 | code: 404 209 | } 210 | }); 211 | else 212 | return res.status(500).json({ 213 | error: { 214 | message: `There was a problem with your request.`, 215 | code: 500 216 | } 217 | }); 218 | } 219 | 220 | const clientInfo = getClientInfo(config, req); 221 | let info = { 222 | ...clientInfo, 223 | configs: config.data 224 | }; 225 | 226 | if (req.clientHealth) { 227 | info = { ...info, ...clientHealth }; 228 | } 229 | 230 | // scrub apiKeys and tokens 231 | for (let key in config.data) { 232 | if (key.includes('api') || key.includes('token')) 233 | config.data[key] = undefined; 234 | } 235 | 236 | return res.status(200).json(info); 237 | } 238 | 239 | module.exports = { 240 | clientsHandler, 241 | getClientsInfo, 242 | getConfigHandler, 243 | getDefaultClientInfo, 244 | testClientsHandler 245 | }; 246 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // bPanel server -- A Blockchain Management System 3 | // --watch Watch webapp 4 | // --watch-poll Watch webapp in docker on a Mac 5 | // --dev Watch server and webapp 6 | 7 | process.title = 'bpanel'; 8 | 9 | const path = require('path'); 10 | const fs = require('bfile'); 11 | const { execSync } = require('child_process'); 12 | const os = require('os'); 13 | const { createLogger } = require('./logger'); 14 | const chokidar = require('chokidar'); 15 | 16 | const webpackArgs = []; 17 | 18 | let poll = false; 19 | // If run from command line, parse args 20 | if (require.main === module) { 21 | (async function() { 22 | const logger = createLogger(); 23 | await logger.open(); 24 | 25 | // setting up webpack configs 26 | // use default/base config for dev 27 | if ( 28 | process.argv.indexOf('--dev') >= 0 || 29 | process.env.NODE_ENV === 'development' 30 | ) { 31 | webpackArgs.push( 32 | '--config', 33 | path.resolve(__dirname, '../configs/webpack.config.js') 34 | ); 35 | } else { 36 | // otherwise use prod config 37 | webpackArgs.push( 38 | '--config', 39 | path.resolve(__dirname, '../configs/webpack.prod.js') 40 | ); 41 | } 42 | 43 | // environment specific `watch` args 44 | if (process.argv.indexOf('--watch-poll') >= 0) { 45 | poll = true; 46 | webpackArgs.push('--watch', '--env.dev', '--env.poll'); 47 | } else if (process.argv.indexOf('--watch') >= 0) { 48 | webpackArgs.push('--watch', '--env.dev'); 49 | } 50 | 51 | // an option to run an `npm install` which will clear any symlinks 52 | if (process.argv.indexOf('--clear') > -1) { 53 | logger.info('Clearing symlinks in node_modules with `npm install`...'); 54 | execSync('npm install', { 55 | killSignal: 'SIGINT', 56 | stdio: [0, 1, 2], 57 | cwd: path.resolve(__dirname, '..') 58 | }); 59 | } 60 | 61 | if (process.argv.indexOf('--dev') >= 0) { 62 | if (!process.env.NODE_ENV) process.env.NODE_ENV = 'development'; 63 | 64 | // pass args to nodemon process except `--dev` 65 | const args = process.argv 66 | .slice(2) 67 | .filter(arg => arg !== '--dev' && arg !== '--clear'); 68 | 69 | // Watch this server 70 | const nodemon = require('nodemon')({ 71 | script: 'server/index.js', 72 | watch: ['server'], 73 | ignore: ['server/test/**/*.js'], 74 | args, 75 | legacyWatch: poll, 76 | ext: 'js' 77 | }) 78 | .on('crash', () => { 79 | process.exit(1); 80 | }) 81 | .on('quit', process.exit); 82 | 83 | // need to use chokidar to watch for changes outside the working 84 | // directory. Will restart if configs get updated 85 | // should be updated to check bpanelConfig for where the prefix is 86 | const configFile = path.resolve(os.homedir(), '.bpanel/config.js'); 87 | chokidar 88 | .watch([configFile], { usePolling: poll, useFsEvents: poll }) 89 | .on('all', () => { 90 | nodemon.emit('restart'); 91 | }); 92 | await logger.close(); 93 | return; 94 | } 95 | await logger.close(); 96 | })(); 97 | } 98 | 99 | // Init bPanel 100 | module.exports = async (_config = {}) => { 101 | // Import server dependencies 102 | const path = require('path'); 103 | const http = require('http'); 104 | const express = require('express'); 105 | 106 | // network information 107 | const networks = { 108 | bitcoin: require('bcoin/lib/protocol/networks'), 109 | bitcoincash: require('bcash/lib/protocol/networks'), 110 | handshake: require('hsd/lib/protocol/networks') 111 | }; 112 | 113 | // Import express middlewares 114 | const bodyParser = require('body-parser'); 115 | const cors = require('cors'); 116 | const compression = require('compression'); 117 | 118 | // Import app server utilities and modules 119 | const SocketManager = require('./socketManager'); 120 | const { 121 | attach, 122 | apiFilters, 123 | pluginUtils, 124 | clientHelpers, 125 | configHelpers 126 | } = require('./utils'); 127 | const endpoints = require('./endpoints'); 128 | 129 | const { isBlacklisted } = apiFilters; 130 | const { getPluginEndpoints } = pluginUtils; 131 | const { buildClients, getClientsById } = clientHelpers; 132 | const { loadConfig } = configHelpers; 133 | 134 | // get bpanel config 135 | const bpanelConfig = loadConfig('bpanel', _config); 136 | 137 | // build logger from config 138 | const logger = createLogger(bpanelConfig); 139 | bpanelConfig.set('logger', logger); 140 | await logger.open(); 141 | 142 | // check if vendor-manifest has been built otherwise run 143 | // build:dll first to build the manifest 144 | if (!fs.existsSync(path.resolve(__dirname, '../dist/vendor-manifest.json'))) { 145 | logger.info( 146 | 'No vendor manifest. Running webpack dll first. This can take a couple minutes the first time but \ 147 | will increase speed of future builds, so please be patient.' 148 | ); 149 | execSync('npm run build:dll', { 150 | stdio: [0, 1, 2], 151 | cwd: path.resolve(__dirname, '..') 152 | }); 153 | } 154 | 155 | const bsockPort = bpanelConfig.int('bsock-port') || 8000; 156 | 157 | // Always start webpack 158 | require('nodemon')({ 159 | script: './node_modules/.bin/webpack', 160 | watch: [`${bpanelConfig.prefix}/config.js`], 161 | env: { 162 | BPANEL_PREFIX: bpanelConfig.prefix, 163 | BPANEL_SOCKET_PORT: bsockPort, 164 | BPANEL_LOG_LEVEL: bpanelConfig.str('log-level', 'info'), 165 | BPANEL_LOG_FILE: bpanelConfig.bool('log-file', true), 166 | BPANEL_LOG_CONSOLE: bpanelConfig.bool('log-console', true), 167 | BPANEL_LOG_SHRINK: bpanelConfig.bool('log-shrink', true) 168 | }, 169 | args: webpackArgs, 170 | legacyWatch: poll 171 | }) 172 | .on('crash', () => { 173 | process.exit(1); 174 | }) 175 | .on('quit', process.exit); 176 | 177 | // Init app express server 178 | const app = express.Router(); 179 | const port = process.env.PORT || 5000; 180 | app.use(bodyParser.json()); 181 | app.use(cors()); 182 | 183 | // create new SocketManager 184 | 185 | // setting up whitelisted ports for wsproxy 186 | // can add other custom ones via `proxy-ports` config option 187 | const ports = [18444, 28333, 28901].concat( 188 | bpanelConfig.array('proxy-ports', []) 189 | ); 190 | 191 | for (let chain in networks) { 192 | ports.push(networks[chain].main.port); 193 | ports.push(networks[chain].testnet.port); 194 | } 195 | 196 | const socketManager = new SocketManager({ 197 | noAuth: true, 198 | port: bsockPort, 199 | logger, 200 | ports 201 | }); 202 | 203 | // Wait for async part of server setup 204 | const ready = (async function() { 205 | // Set up client config 206 | let { clients, configsMap } = buildClients(bpanelConfig); 207 | 208 | const clientIds = clients.keys(); 209 | 210 | const resolveIndex = (req, res) => { 211 | logger.debug(`Caught request in resolveIndex: ${req.path}`); 212 | res.sendFile(path.resolve(__dirname, '../dist/index.html')); 213 | }; 214 | 215 | // Setup app server 216 | app.use(compression()); 217 | 218 | // black list filter 219 | const forbiddenHandler = (req, res) => 220 | res.status(403).json({ error: { message: 'Forbidden', code: 403 } }); 221 | 222 | app.use((req, res, next) => { 223 | try { 224 | if (isBlacklisted(bpanelConfig, req)) return forbiddenHandler(req, res); 225 | next(); 226 | } catch (e) { 227 | next(e); 228 | } 229 | }); 230 | 231 | app.use( 232 | express.static(path.resolve(__dirname, '../dist'), { 233 | index: 'index.html', 234 | setHeaders: function(res, path) { 235 | if (path.endsWith('.gz')) { 236 | res.setHeader('Content-Encoding', 'gzip'); 237 | res.setHeader('Content-Type', 'application/javascript'); 238 | } 239 | } 240 | }) 241 | ); 242 | 243 | app.get('/', resolveIndex); 244 | 245 | // add utilities to the req object 246 | // for use in the api endpoints 247 | // TODO: Load up client configs and attach to req object here 248 | app.use((req, res, next) => { 249 | req.logger = logger; 250 | req.config = bpanelConfig; 251 | req.clients = configsMap; 252 | next(); 253 | }); 254 | 255 | /* 256 | * Setup backend plugins 257 | */ 258 | 259 | const { beforeMiddleware, afterMiddleware } = getPluginEndpoints( 260 | bpanelConfig, 261 | logger 262 | ); 263 | 264 | // compose endpoints 265 | const apiEndpoints = [...beforeMiddleware]; 266 | for (let key in endpoints) { 267 | apiEndpoints.push(...endpoints[key]); 268 | } 269 | apiEndpoints.push(...afterMiddleware); 270 | 271 | for (let endpoint of apiEndpoints) { 272 | try { 273 | attach(app, endpoint); 274 | } catch (e) { 275 | logger.error(e.stack); 276 | } 277 | } 278 | 279 | // TODO: add favicon.ico file 280 | app.get('/favicon.ico', (req, res) => { 281 | res.send(); 282 | }); 283 | 284 | app.get('/*', resolveIndex); 285 | 286 | // This must be the last middleware so that 287 | // it catches and returns errors 288 | app.use((err, req, res, next) => { 289 | logger.error('There was an error in the middleware: %s', err.message); 290 | logger.error(err.stack); 291 | if (res.headersSent) { 292 | return next(err); 293 | } 294 | res.status(500).json({ error: { status: 500, message: 'Server error' } }); 295 | }); 296 | 297 | // handle the unhandled rejections and exceptions 298 | if (process.listenerCount('unhandledRejection') === 0) { 299 | process.on('unhandledRejection', err => { 300 | logger.error('Unhandled Rejection\n', err); 301 | }); 302 | } 303 | if (process.listenerCount('uncaughtException') === 0) { 304 | process.on('uncaughtException', err => { 305 | logger.error('Uncaught Exception\n', err); 306 | }); 307 | } 308 | 309 | // Crash the process when a service does 310 | const onError = service => { 311 | return e => { 312 | logger.error(`${service} error: ${e.message}`); 313 | process.exit(1); 314 | }; 315 | }; 316 | 317 | // Start bsock server 318 | socketManager.on('error', e => 319 | logger.error(`socketManager error: ${e.message}`) 320 | ); 321 | 322 | try { 323 | // Setup bsock server 324 | for (let id of clientIds) { 325 | const newClients = getClientsById(id, clients); 326 | socketManager.addClients(id, newClients); 327 | } 328 | 329 | // refresh the clients map if the clients directory gets updated 330 | const clientsDir = bpanelConfig.location('clients'); 331 | chokidar 332 | .watch([clientsDir], { usePolling: poll, useFsEvents: poll }) 333 | .on('all', (event, path) => { 334 | logger.info( 335 | 'Change detected in clients directory. Updating clients on server.' 336 | ); 337 | logger.debug('"%s" event on %s', event, path); 338 | const builtClients = buildClients(bpanelConfig); 339 | clients = builtClients.clients; 340 | configsMap = builtClients.configsMap; 341 | 342 | // need to update the socket manager too 343 | // TODO: this isn't ideal (doing two loops) 344 | // but it's better than restarting the whole server 345 | // which also restarts the webpack build 346 | // hopefully this is easier to manage when the socket manager also 347 | // has the full server for all requests and can manage this internally 348 | const ids = clients.keys(); 349 | // add any new clients not in socketManager 350 | for (let id of ids) { 351 | const newClients = getClientsById(id, clients); 352 | 353 | if (!socketManager.hasClient(id)) 354 | socketManager.addClients(id, newClients); 355 | } 356 | 357 | // remove any clients from the socketManager not in our list 358 | const sockets = socketManager.clients.keys(); 359 | for (let id of sockets) 360 | if (!clients.has(id)) socketManager.removeClients(id); 361 | }); 362 | } catch (e) { 363 | logger.error('There was a problem loading clients:', e); 364 | } 365 | 366 | await socketManager.open(); 367 | 368 | // If NOT required from another script... 369 | if (require.main === module) { 370 | http // Start app server 371 | .createServer(express().use(app)) 372 | .on('error', onError('bpanel')) 373 | .listen(port, () => { 374 | logger.info('bpanel app running on port', port); 375 | }); 376 | 377 | // can serve over https 378 | if (bpanelConfig.bool('ssl', false)) { 379 | const fs = require('bfile'); 380 | const https = require('https'); 381 | const httpsPort = bpanelConfig.int('https-port', 5001); 382 | const keyPath = bpanelConfig.str('ssl-key', '/etc/ssl/key.pem'); 383 | const certPath = bpanelConfig.str('ssl-cert', '/etc/ssl/cert.pem'); 384 | 385 | let opts = {}; 386 | try { 387 | opts.key = fs.readFileSync(keyPath); 388 | opts.cert = fs.readFileSync(certPath); 389 | } catch (e) { 390 | logger.error(e); 391 | logger.error('Error reading cert/key pair'); 392 | process.exit(1); 393 | } 394 | 395 | https 396 | .createServer(opts, express().use(app)) 397 | .on('error', onError('bpanel')) 398 | .listen(httpsPort, () => { 399 | logger.info('bpanel https app running on port', httpsPort); 400 | }); 401 | } 402 | } 403 | 404 | return app; 405 | })(); 406 | 407 | // Export app, clients, & utils 408 | return { 409 | app, 410 | ready, 411 | logger, 412 | config: bpanelConfig 413 | }; 414 | }; 415 | 416 | // Start server when run from command line 417 | if (require.main === module) { 418 | (async function() { 419 | try { 420 | module.exports(); 421 | } catch (e) { 422 | const logger = createLogger(); 423 | await logger.open(); 424 | logger.error('There was an error running the server: ', e.stack); 425 | await logger.close(); 426 | process.exit(1); 427 | } 428 | })(); 429 | } 430 | -------------------------------------------------------------------------------- /server/logger.js: -------------------------------------------------------------------------------- 1 | const Logger = require('blgr'); 2 | const Config = require('bcfg'); 3 | const assert = require('bsert'); 4 | 5 | const loadConfig = require('./utils/loadConfig'); 6 | 7 | function createLogger(_config) { 8 | let config = _config; 9 | if (!config) config = loadConfig('bpanel'); 10 | assert(config instanceof Config, 'Must pass Bcfg object to create logger'); 11 | 12 | const logger = new Logger(); 13 | logger.set({ 14 | filename: config.bool('log-file', true) 15 | ? config.location('debug.log') 16 | : null, 17 | level: config.str('log-level', 'info'), 18 | console: config.bool('log-console', true), 19 | shrink: config.bool('log-shrink', true) 20 | }); 21 | return logger; 22 | } 23 | 24 | module.exports.createLogger = createLogger; 25 | -------------------------------------------------------------------------------- /server/test/configHelpers-test.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const { resolve } = require('path'); 3 | const { assert } = require('chai'); 4 | const Config = require('bcfg'); 5 | const Logger = require('blgr'); 6 | const fs = require('bfile'); 7 | 8 | const { initFullNode } = require('./utils/regtest'); 9 | 10 | const { configHelpers } = require('../utils'); 11 | const { 12 | loadConfig, 13 | createClientConfig, 14 | testConfigOptions, 15 | getConfig, 16 | ClientErrors 17 | } = configHelpers; 18 | 19 | // setup tmp directory for testing 20 | const testDir = resolve(os.homedir(), '.bpanel_tmp'); 21 | process.env.BPANEL_PREFIX = testDir; 22 | process.env.BPANEL_CLIENTS_DIR = 'test_clients'; 23 | process.env.BPANEL_LOG_LEVEL = 'error'; 24 | const { BPANEL_PREFIX, BPANEL_CLIENTS_DIR } = process.env; 25 | const clientsDirPath = resolve(BPANEL_PREFIX, BPANEL_CLIENTS_DIR); 26 | 27 | describe('configHelpers', () => { 28 | let node, apiKey, ports, options, id, config, logger; 29 | 30 | before('create and start regtest node', async () => { 31 | id = 'test'; 32 | apiKey = 'foo'; 33 | ports = { 34 | p2p: 49331, 35 | node: 49332, 36 | wallet: 49333 37 | }; 38 | node = await initFullNode({ 39 | ports: ports, 40 | memory: true, 41 | logLevel: 'none', 42 | apiKey 43 | }); 44 | }); 45 | 46 | after('close node and remove testing directory', async function() { 47 | await node.close(); 48 | if (fs.existsSync(testDir)) { 49 | fs.rimrafSync(testDir); 50 | } 51 | }); 52 | 53 | beforeEach(async () => { 54 | config = new Config(id); 55 | logger = new Logger({ level: 'error' }); 56 | await logger.open(); 57 | config.set('logger', logger); 58 | config.set('id', id); 59 | options = { 60 | id, 61 | chain: 'bitcoin', 62 | port: ports.node, 63 | network: 'regtest', 64 | apiKey, 65 | 'wallet-port': ports.wallet, 66 | multisigWallet: false 67 | }; 68 | }); 69 | 70 | afterEach(async () => { 71 | await logger.close(); 72 | }); 73 | 74 | describe('testConfigOptions', () => { 75 | it('should not throw if clients are valid', async () => { 76 | config.inject(options); 77 | await testConfigOptions(config); 78 | }); 79 | 80 | it('should throw error with property for each client(s) that failed', async () => { 81 | options.apiKey = 'bar'; 82 | options.walletport = 123; 83 | 84 | config.inject(options); 85 | 86 | const [err, clientErrors] = await testConfigOptions(config); 87 | assert(err, 'Expected testConfigOptions to have an error'); 88 | assert.includeMembers( 89 | clientErrors.failed, 90 | ['node', 'wallet'], 91 | 'Expected failures for node and wallet clients' 92 | ); 93 | 94 | // since we're testing specific errors, want to make sure the test is 95 | // setup correctly 96 | assert( 97 | options.apiKey !== apiKey && options.walletport !== ports.wallet, 98 | 'api key and wallet port options should not match node when testing failures' 99 | ); 100 | assert.include( 101 | clientErrors.node.message, 102 | 'Unauthorized', 103 | 'Node should have failed because of bad API key' 104 | ); 105 | assert.include( 106 | clientErrors.wallet.message, 107 | 'ECONNREFUSED', 108 | 'Wallet client connection should have failed because of bad port' 109 | ); 110 | }); 111 | }); 112 | 113 | describe('createClientConfig', () => { 114 | it('should test clients', async () => { 115 | const failOpts = { ...options, apiKey: 'bar' }; 116 | let passed = false; 117 | const testConfig = loadConfig('test', failOpts); 118 | testConfig.set('logger', logger); 119 | try { 120 | await createClientConfig(testConfig, false); 121 | passed = true; 122 | } catch (e) { 123 | assert.instanceOf( 124 | e, 125 | ClientErrors, 126 | 'Expected to throw a ClientErrors error' 127 | ); 128 | } 129 | assert(!passed, 'Expected to fail with bad options'); 130 | }); 131 | 132 | it('should create new config file in clients directory with correct configs', async () => { 133 | config.inject(options); 134 | await createClientConfig(config, false); 135 | const { BPANEL_PREFIX, BPANEL_CLIENTS_DIR } = process.env; 136 | const clientPath = resolve( 137 | BPANEL_PREFIX, 138 | BPANEL_CLIENTS_DIR, 139 | `${id}.conf` 140 | ); 141 | 142 | assert( 143 | fs.existsSync(clientPath), 144 | 'Could not find config file at expected path' 145 | ); 146 | 147 | const loadedConfigs = loadConfig(id, { id, prefix: clientsDirPath }); 148 | loadedConfigs.open(`${id}.conf`); 149 | 150 | // create a config just from the loaded data to test against 151 | const testConfig = new Config('test-config'); 152 | testConfig.inject(loadedConfigs.data); 153 | testConfig.set('logger', logger); 154 | await testConfigOptions(testConfig); 155 | }); 156 | }); 157 | 158 | describe('getConfig', () => { 159 | it('should get the config object from a config file', async () => { 160 | const expectedConfigs = new Config('base'); 161 | expectedConfigs.inject(options); 162 | expectedConfigs.set('logger', logger); 163 | await createClientConfig(expectedConfigs, false); 164 | const clientConfig = await getConfig(id); 165 | assert.instanceOf(clientConfig, Config, 'Expected to get a bcfg object'); 166 | 167 | // need to do a custom deep comparison with non-strict comparisons 168 | // because of the way bcfg converts, 169 | // `data` contains the configs loaded from a file 170 | // `options` contains configs injected from options 171 | const actualKeys = Object.keys(clientConfig.data); 172 | const expectedKeys = Object.keys(expectedConfigs.options); 173 | assert.equal( 174 | actualKeys.length, 175 | expectedKeys.length, 176 | 'Wrong number of properties' 177 | ); 178 | 179 | for (let key of actualKeys) { 180 | // not concerned with logger as this gets set elsewhere 181 | if (key === 'logger') continue; 182 | let actual = clientConfig.data[key]; 183 | let expected = expectedConfigs.options[key]; 184 | 185 | // bools don't get inserted consistently to config object 186 | if (actual === 'false' || actual === 'true') 187 | actual = JSON.parse(actual); 188 | if (expected === 'false' || expected === 'true') 189 | expected = JSON.parse(expected); 190 | 191 | assert.equal( 192 | actual, 193 | expected, 194 | `Actual ${key} in clientConfig.data did not match expected ${key}` 195 | ); 196 | } 197 | }); 198 | 199 | it('should throw if config does not exist', async () => { 200 | const failId = 'fail'; 201 | let failed; 202 | try { 203 | await getConfig(failId); 204 | failed = false; 205 | } catch (e) { 206 | failed = true; 207 | } 208 | assert(failed, `Expected getConfig to fail for id "${failId}"`); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /server/test/utils/helpers.js: -------------------------------------------------------------------------------- 1 | function sleep(time = 1000) { 2 | return new Promise(resolve => 3 | setTimeout(() => { 4 | resolve(); 5 | }, time) 6 | ); 7 | } 8 | 9 | module.exports = { 10 | sleep 11 | }; 12 | -------------------------------------------------------------------------------- /server/test/utils/regtest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Utility borrowed from bcoin lib 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const assert = require('bsert'); 8 | const { FullNode } = require('bcoin'); 9 | const { SPVNode } = require('bcoin'); 10 | 11 | const { NodeClient, WalletClient } = require('bclient'); 12 | const walletPlugin = require('bcoin/lib/wallet/plugin'); 13 | 14 | const shared = { 15 | apiKey: 'foo', 16 | network: 'regtest' 17 | }; 18 | 19 | async function initFullNode(options) { 20 | const node = new FullNode({ 21 | prefix: options.prefix || null, 22 | network: shared.network, 23 | apiKey: options.apiKey || shared.apiKey, 24 | walletAuth: true, 25 | workers: true, 26 | listen: true, 27 | bip37: true, 28 | port: options.ports.p2p, 29 | httpPort: options.ports.node, 30 | maxOutbound: 1, 31 | seeds: [], 32 | memory: options.memory ? true : false, 33 | plugins: [walletPlugin], 34 | env: { 35 | BCOIN_WALLET_HTTP_PORT: options.ports.wallet.toString() 36 | }, 37 | logLevel: options.logLevel 38 | }); 39 | await node.ensure(); 40 | await node.open(); 41 | await node.connect(); 42 | return node; 43 | } 44 | 45 | async function initSPVNode(options) { 46 | const node = new SPVNode({ 47 | prefix: options.prefix || null, 48 | network: shared.network, 49 | cors: true, 50 | apiKey: shared.apiKey, 51 | walletAuth: true, 52 | workers: true, 53 | listen: true, 54 | port: options.ports.p2p, 55 | httpPort: options.ports.node, 56 | maxOutbound: 1, 57 | seeds: [], 58 | nodes: [`127.0.0.1:${options.ports.p2p}`], 59 | memory: options.memory ? true : false, 60 | plugins: [walletPlugin], 61 | env: { 62 | BCOIN_WALLET_HTTP_PORT: options.ports.wallet.toString() 63 | }, 64 | logLevel: options.logLevel 65 | }); 66 | await node.ensure(); 67 | await node.open(); 68 | await node.connect(); 69 | await node.startSync(); 70 | return node; 71 | } 72 | 73 | async function initNodeClient(options) { 74 | const nclient = new NodeClient({ 75 | network: shared.network, 76 | port: options.ports.node, 77 | apiKey: shared.apiKey 78 | }); 79 | await nclient.open(); 80 | return nclient; 81 | } 82 | 83 | async function initWalletClient(options) { 84 | const wclient = new WalletClient({ 85 | network: shared.network, 86 | port: options.ports.wallet, 87 | apiKey: shared.apiKey 88 | }); 89 | await wclient.open(); 90 | return wclient; 91 | } 92 | 93 | async function initWallet(wclient) { 94 | const winfo = await wclient.createWallet('test'); 95 | assert.strictEqual(winfo.id, 'test'); 96 | const wallet = wclient.wallet('test', winfo.token); 97 | 98 | // We don't use witness here yet, as there is an activation 99 | // threshold before segwit can be activated. 100 | const info = await wallet.createAccount('blue', { witness: false }); 101 | assert(info.initialized); 102 | assert.strictEqual(info.name, 'blue'); 103 | assert.strictEqual(info.accountIndex, 1); 104 | assert.strictEqual(info.m, 1); 105 | assert.strictEqual(info.n, 1); 106 | 107 | return wallet; 108 | } 109 | 110 | async function generateBlocks(count, nclient, coinbase) { 111 | return await nclient.execute('generatetoaddress', [count, coinbase]); 112 | } 113 | 114 | async function generateTxs(options) { 115 | const { wclient, count } = options; 116 | 117 | await wclient.execute('selectwallet', ['test']); 118 | 119 | for (var i = 0; i < count; i++) { 120 | const addr = await wclient.execute('getnewaddress', ['blue']); 121 | await wclient.execute('sendtoaddress', [addr, 0.11111111]); 122 | } 123 | } 124 | 125 | async function generateInitialBlocks(options) { 126 | const { nclient, wclient, coinbase, genesisTime = 1534965859 } = options; 127 | let { blocks } = options; 128 | 129 | if (!blocks) blocks = 100; 130 | 131 | const blockInterval = 600; 132 | const timewarp = 3200; 133 | 134 | let c = 0; 135 | 136 | // Establish baseline block interval for a median time 137 | for (; c < 11; c++) { 138 | let blocktime = genesisTime + c * blockInterval; 139 | await nclient.execute('setmocktime', [blocktime]); 140 | 141 | const blockhashes = await generateBlocks(1, nclient, coinbase); 142 | const block = await nclient.execute('getblock', [blockhashes[0]]); 143 | 144 | assert(block.time <= blocktime + 1); 145 | assert(block.time >= blocktime); 146 | } 147 | 148 | // Generate time warping blocks that have time previous 149 | // to the previous block 150 | for (; c < blocks; c++) { 151 | let blocktime = genesisTime + c * blockInterval; 152 | if (c % 5) blocktime -= timewarp; 153 | await nclient.execute('setmocktime', [blocktime]); 154 | 155 | // TODO 156 | // Use an event to wait for wallets to catch up so that 157 | // funds can be spent 158 | 159 | // If the wallet client is available and there have been 160 | // enough blocks for coinbase to mature, generate transactions 161 | // for the block. Additionally the wallet may not be in lockstep 162 | // sync with the chain, so it's necessary to wait a few more blocks. 163 | if (wclient && c > 115) await generateTxs({ wclient: wclient, count: 50 }); 164 | 165 | const blockhashes = await generateBlocks(1, nclient, coinbase); 166 | const block = await nclient.execute('getblock', [blockhashes[0]]); 167 | 168 | assert(block.time <= blocktime + 1); 169 | assert(block.time >= blocktime); 170 | } 171 | } 172 | 173 | module.exports = { 174 | initFullNode, 175 | initSPVNode, 176 | initNodeClient, 177 | initWalletClient, 178 | initWallet, 179 | generateBlocks, 180 | generateInitialBlocks 181 | }; 182 | -------------------------------------------------------------------------------- /server/utils/apiFilters.js: -------------------------------------------------------------------------------- 1 | const assert = require('bsert'); 2 | const Config = require('bcfg'); 3 | 4 | function isMatch(src, regexpOrString) { 5 | if (regexpOrString instanceof RegExp) 6 | return Boolean(src.match(regexpOrString)); 7 | else if (typeof regexpOrString === 'string') 8 | return Boolean(src.match(RegExp(regexpOrString))); 9 | throw new TypeError( 10 | `Expected either a string or RegExp, instead received ${typeof regexpOrString}` 11 | ); 12 | } 13 | 14 | function isBlacklisted(config, endpoint) { 15 | assert(config instanceof Config, 'Must pass a config to check blacklist'); 16 | const blacklist = config.array('blacklist', []); 17 | const { path, method } = endpoint; 18 | for (let blacklisted of blacklist) { 19 | // if item in array is just a string, check against the path 20 | // must be an exact match 21 | if ( 22 | (typeof blacklisted === 'string' || blacklisted instanceof RegExp) && 23 | isMatch(path, blacklisted) 24 | ) 25 | return true; 26 | else if (blacklisted.method === method && isMatch(path, blacklisted.path)) 27 | // for objects, will check path and method match 28 | return true; 29 | } 30 | // if no match, confirm not blacklisted 31 | return false; 32 | } 33 | 34 | module.exports = { 35 | isBlacklisted 36 | }; 37 | -------------------------------------------------------------------------------- /server/utils/attach.js: -------------------------------------------------------------------------------- 1 | const methods = require('../endpoints/methods'); 2 | const { GET, POST, PUT, DELETE, USE } = methods; 3 | 4 | function attach(app, endpoint) { 5 | const { method, path, handler } = endpoint; 6 | switch (method) { 7 | case USE: 8 | app.use(path, handler); 9 | break; 10 | case GET: 11 | app.get(path, handler); 12 | break; 13 | case POST: 14 | app.post(path, handler); 15 | break; 16 | case PUT: 17 | app.put(path, handler); 18 | break; 19 | case DELETE: 20 | app.delete(path, handler); 21 | break; 22 | default: 23 | throw new Error( 24 | `Unrecognized endpoint method ${endpoint.method} for path ${endpoint.path}` 25 | ); 26 | } 27 | } 28 | 29 | module.exports = attach; 30 | -------------------------------------------------------------------------------- /server/utils/clients.js: -------------------------------------------------------------------------------- 1 | const { parse: urlParse } = require('url'); 2 | const assert = require('bsert'); 3 | const Config = require('bcfg'); 4 | const { Network: BNetwork } = require('bcoin'); 5 | const { Network: HSNetwork } = require('hsd'); 6 | const { 7 | NodeClient: BNodeClient, 8 | WalletClient: BWalletClient 9 | } = require('bclient'); 10 | const { 11 | NodeClient: HSNodeClient, 12 | WalletClient: HSWalletClient 13 | } = require('hs-client'); 14 | const MultisigClient = require('bmultisig/lib/client'); 15 | 16 | const logClientInfo = (id, type, { ssl, host, port, network }) => 17 | `${id}: Configuring ${type} client with uri: ${ssl 18 | ? 'https' 19 | : 'http'}://${host}:${port}, network: ${network}`; 20 | 21 | /* 22 | * Create clients based on given configs 23 | * @param {Config} config - a bcfg config object 24 | * @returns {Object} clients - an object that includes 25 | * a Node, Wallet, and Multisig clients as available 26 | */ 27 | function clientFactory(config) { 28 | let Network, NodeClient, WalletClient; 29 | assert( 30 | config instanceof Config, 31 | 'Must pass instance of Config class to client composer' 32 | ); 33 | 34 | const logger = config.obj('logger'); 35 | assert(logger, 'No logger attached to config'); 36 | 37 | const id = config.str('id'); 38 | assert(id, 'Client config must have an id'); 39 | 40 | // bitcoin, bitcoincash, handshake 41 | if (!config.str('chain')) 42 | logger.warning( 43 | `No chain set in configs for ${config.str('id')}, defaulting to 'bitcoin'` 44 | ); 45 | 46 | const chain = config.str('chain', 'bitcoin'); 47 | 48 | // set tools based on chain 49 | switch (chain) { 50 | case 'handshake': 51 | Network = HSNetwork; 52 | NodeClient = HSNodeClient; 53 | WalletClient = HSWalletClient; 54 | break; 55 | case 'bitcoin': 56 | case 'bitcoincash': 57 | Network = BNetwork; 58 | NodeClient = BNodeClient; 59 | WalletClient = BWalletClient; 60 | break; 61 | default: 62 | throw new Error(`Unrecognized chain ${chain}`); 63 | } 64 | 65 | const network = Network.get(config.str('network', 'main')); 66 | 67 | // set fallback network configs from `uri` config if set 68 | let port = config.int('port', network.rpcPort); 69 | let hostname = 'localhost'; 70 | if (config.str('node-host')) hostname = config.str('node-host'); 71 | else if (config.str('host')) hostname = config.str('host'); 72 | else if (config.str('hostname')) hostname = config.str('hostname'); 73 | 74 | let protocol = config.str('protocol', 'http:'); 75 | 76 | let url = config.str('url') || config.str('node-uri'); 77 | if (url) { 78 | const nodeUrl = urlParse(url); 79 | port = nodeUrl.port; 80 | hostname = nodeUrl.hostname; 81 | protocol = nodeUrl.protocol; 82 | } else { 83 | url = `${protocol}//${hostname}:${port}`; 84 | } 85 | 86 | const ssl = 87 | config.bool('ssl') || (protocol && protocol.indexOf('https') > -1); 88 | config.inject({ port, hostname, protocol, ssl }); 89 | 90 | const nodeOptions = { 91 | host: config.str('hostname'), 92 | apiKey: config.str('api-key'), 93 | network: config.str('network', 'main'), 94 | port: config.uint('port'), 95 | ssl: config.bool('ssl'), 96 | url: config.str('url') || url 97 | }; 98 | 99 | const walletOptions = { 100 | ...nodeOptions, 101 | apiKey: config.str('wallet-api-key', nodeOptions.apiKey), 102 | port: config.uint('wallet-port', network.walletPort), 103 | ssl: config.bool('wallet-ssl', nodeOptions.ssl), 104 | token: config.str('wallet-token'), 105 | url: config.str('wallet-uri') || config.str('wallet-url') 106 | }; 107 | 108 | // set any options that are empty strings to undefined 109 | for (let options of [nodeOptions, walletOptions]) { 110 | for (let key in options) { 111 | if (typeof options[key] === 'string' && !options[key].length) 112 | options[key] = undefined; 113 | } 114 | } 115 | 116 | let walletClient, nodeClient, multisigClient; 117 | // check if config explicitly sets node config to `false` 118 | // if false, do not instantiate new node client 119 | if (config.bool('node', true)) { 120 | nodeClient = new NodeClient(nodeOptions); 121 | const statement = logClientInfo(id, 'node', nodeOptions); 122 | logger.info(statement); 123 | } 124 | 125 | // check if config explicitly sets wallet config to `false` 126 | // if false, do not instantiate new wallet client 127 | if (config.bool('wallet', true)) { 128 | walletClient = new WalletClient(walletOptions); 129 | 130 | const statement = logClientInfo(id, 'wallet', walletOptions); 131 | logger.info(statement); 132 | } 133 | 134 | if (config.bool('multisig', true)) { 135 | multisigClient = new MultisigClient({ 136 | ...walletOptions, 137 | multisigPath: '/' 138 | }); 139 | const statement = logClientInfo(id, 'multisig wallet', walletOptions); 140 | logger.info(statement); 141 | } 142 | 143 | return { nodeClient, walletClient, multisigClient }; 144 | } 145 | 146 | /* 147 | * Build a map of all clients. 148 | * @param {bcfg.Config} config - the main app config object 149 | * @returns {Object} - a map of the configs and the clients 150 | */ 151 | function buildClients(config) { 152 | const { loadClientConfigs, createConfigsMap } = require('./configs'); 153 | assert(config.has('logger'), 'Config missing logger'); 154 | const logger = config.obj('logger'); 155 | 156 | // loadConfigs uses the bpanelConfig to find the clients and build 157 | // each of their configs. 158 | const clientConfigs = loadClientConfigs(config); 159 | const configsMap = createConfigsMap(clientConfigs); 160 | const clients = clientConfigs.reduce((clientsMap, cfg) => { 161 | const id = cfg.str('id'); 162 | assert(id, 'client config must have id'); 163 | 164 | // give client config the app logger 165 | cfg.set('logger', logger); 166 | 167 | clientsMap.set(id, { ...clientFactory(cfg), config: cfg }); 168 | return clientsMap; 169 | }, new Map()); 170 | return { configsMap, clients }; 171 | } 172 | 173 | /* 174 | * utility function for getting object of clients 175 | * @param {String} id - id of clients to retrieve 176 | * @param {Map} clientsMap - map of all clients to get relevent 177 | * @returns {Object} clients - object of only clients for that id 178 | */ 179 | function getClientsById(id, clientsMap) { 180 | assert(typeof id === 'string', 'Expected an id of type string'); 181 | assert(clientsMap instanceof Map, 'Expected a map of clients'); 182 | const { nodeClient, walletClient, multisigClient } = clientsMap.get(id); 183 | const clients = {}; 184 | if (nodeClient) clients.node = nodeClient; 185 | if (walletClient) clients.wallet = walletClient; 186 | if (multisigClient) clients.multisig = multisigClient; 187 | return clients; 188 | } 189 | 190 | module.exports = { 191 | clientFactory, 192 | buildClients, 193 | getClientsById 194 | }; 195 | -------------------------------------------------------------------------------- /server/utils/configs.js: -------------------------------------------------------------------------------- 1 | // Create a bcfg-compatible config file 2 | 3 | const assert = require('bsert'); 4 | const fs = require('bfile'); 5 | const { resolve, parse, join } = require('path'); 6 | const Config = require('bcfg'); 7 | 8 | const pkg = require('../../pkg'); 9 | const { clientFactory } = require('./clients'); 10 | const loadConfig = require('./loadConfig'); 11 | 12 | /* 13 | * Get an array of configs for each client 14 | * in the module's home directory 15 | * @param {Object} [configs] - Object of custom configs to inject 16 | * into the bcfg config object 17 | * @returns {Config[]} An array of Config object which can be 18 | * used to load clients 19 | */ 20 | function loadClientConfigs(_config) { 21 | // first let's load the parent bpanel config 22 | let config = _config; 23 | 24 | // if not passed bcfg object, create one 25 | if (!(_config instanceof Config)) config = loadConfig('bpanel', _config); 26 | const logger = config.obj('logger'); 27 | 28 | // clientsDir is the folder where all client configs 29 | // should be saved and can be changed w/ custom configs 30 | const clientsDir = config.str('clients-dir', 'clients'); 31 | 32 | // get full path to client configs relative to the project 33 | // prefix which defaults to `~/.bpanel` 34 | const clientsPath = resolve(config.prefix, clientsDir); 35 | const clientFiles = fs.readdirSync(clientsPath); 36 | 37 | // ignore file names that start with a '.' such as 38 | // system files and files without `.conf` extension 39 | // then load config for that client 40 | const files = clientFiles.filter( 41 | name => name[0] !== '.' && /.conf$/.test(name) 42 | ); 43 | 44 | // cancel startup process if there are no clientConfigs 45 | if (!files.length) { 46 | logger.warning( 47 | 'No client configs found. Add one manually to your clients directory \ 48 | or use the connection-manager plugin to add via the UI' 49 | ); 50 | logger.warning( 51 | 'Visit the documentation for more information: https://bpanel.org/docs/configuration.html' 52 | ); 53 | return files; 54 | } 55 | 56 | logger.info('Loading configs for %s clients...', files.length); 57 | return files.map(fileName => { 58 | // After filter, we load bcfg object for each client 59 | 60 | // id is the file name without the extension 61 | const { name: clientId, ext } = parse(fileName); 62 | assert(ext === '.conf', 'client configs must have .conf extension'); 63 | 64 | const options = { 65 | id: clientId, 66 | prefix: join(config.prefix, clientsDir) 67 | }; 68 | 69 | const clientConf = loadConfig(clientId, options); 70 | 71 | // load configs from config file 72 | // files are loaded from the prefix directory 73 | clientConf.open(fileName); 74 | 75 | return clientConf; 76 | }); 77 | } 78 | 79 | /* 80 | * Retrieve a config from clients directory 81 | * @param {string} id - id of client to retrieve 82 | * @returns {bcfg.Config} - bcfg object of config 83 | */ 84 | 85 | function getConfig(id) { 86 | assert(typeof id === 'string', 'Client config must have an id'); 87 | 88 | const appConfig = loadConfig('bpanel'); 89 | const clientsDir = appConfig.str('clients-dir', 'clients'); 90 | 91 | const clientsPath = resolve(appConfig.prefix, clientsDir); 92 | const config = loadConfig(id, { id, prefix: clientsPath }); 93 | 94 | const path = resolve(clientsPath, `${id}.conf`); 95 | if (!fs.existsSync(path)) { 96 | const error = new Error(`File ${id}.conf not found`); 97 | error.code = 'ENOENT'; 98 | throw error; 99 | } 100 | 101 | config.open(`${id}.conf`); 102 | return config; 103 | } 104 | 105 | /* 106 | * create and test clients based on a passed config 107 | * @param {Bcfg} config 108 | * @throws {ClientErrors} - throws if at least one client fails 109 | * @returns {[bool, ClientErrors]} [err, ClientErrors] - bool is true 110 | * if there was an error, false if no error. 111 | */ 112 | 113 | async function testConfigOptions(config) { 114 | assert(config instanceof Config, 'Must pass a bcfg object to test configs'); 115 | 116 | const agents = new Map([ 117 | ['bcoin', 'bitcoin'], 118 | ['bcash', 'bitcoincash'], 119 | ['hsd', 'handshake'] 120 | ]); 121 | 122 | const clientErrors = new ClientErrors(); 123 | const chain = config.str('chain', 'bitcoin'); 124 | 125 | if (!pkg.chains.includes(chain)) 126 | throw new Error(`${chain} is not a recognized chain`); 127 | 128 | const logger = config.obj('logger'); 129 | const clients = clientFactory(config); 130 | 131 | // save the async checks in an array so we can parallelize the 132 | // network call with a `Promise.all` 133 | const clientChecks = []; 134 | 135 | // check each client to see if it can connect 136 | // for each failure, `addFailed` to the error object 137 | for (let key in clients) { 138 | // keys come back of the form "nodeClient" 139 | // to get the type we need to remove "Client" from the string 140 | const type = key.substr(0, key.indexOf('Client')); 141 | const check = new Promise(async resolve => { 142 | try { 143 | logger.info(`Checking ${key} for config "${config.str('id')}"`); 144 | if (config.bool(type, true)) { 145 | const info = await clients[key].getInfo(); 146 | if (info) { 147 | const { pool: { agent } } = info; 148 | // find implementation from user agent 149 | const implementation = agent.match(/([\w\s]*)(?=:)/)[0]; 150 | assert(agents.has(implementation), `Agent ${agent} not supported.`); 151 | if (agents.get(implementation) !== chain) 152 | throw new Error( 153 | `Chain config of "${chain}" did not match the node's chain "${agents.get( 154 | implementation 155 | )}"` 156 | ); 157 | } 158 | } 159 | } catch (e) { 160 | logger.info('%s connection for "%s" failed.', key, config.str('id')); 161 | clientErrors.addFailed(type, e); 162 | } finally { 163 | // resolving all calls so that we can store the failures in the 164 | // clientErrors object 165 | resolve(); 166 | } 167 | }); 168 | clientChecks.push(check); 169 | } 170 | 171 | await Promise.all(clientChecks); 172 | 173 | if (clientErrors.failed.length) { 174 | clientErrors.composeMessage(); 175 | return [true, clientErrors]; 176 | } 177 | 178 | return [false, null]; 179 | } 180 | 181 | /* 182 | * Simple utility for getting a default config with logging 183 | * for certain edge cases 184 | * @param {Config} bpanelConfig 185 | * @returns {Config} 186 | */ 187 | 188 | async function getDefaultConfig(bpanelConfig) { 189 | assert( 190 | bpanelConfig instanceof Config, 191 | 'Need the main bcfg for the app to get default configs' 192 | ); 193 | const clientConfigs = loadClientConfigs(bpanelConfig); 194 | 195 | if (!clientConfigs || !clientConfigs.length) return undefined; 196 | 197 | let defaultClientConfig = clientConfigs.find( 198 | cfg => cfg.str('id') === bpanelConfig.str('client-id', 'default') 199 | ); 200 | 201 | const logger = bpanelConfig.obj('logger'); 202 | 203 | if (!defaultClientConfig) { 204 | logger.warning( 205 | 'Could not find config for %s. Will set to "default" instead.', 206 | bpanelConfig.str('client-id') 207 | ); 208 | defaultClientConfig = clientConfigs.find( 209 | cfg => cfg.str('id') === 'default' 210 | ); 211 | if (!defaultClientConfig) { 212 | logger.warning('Could not find default client config.'); 213 | defaultClientConfig = clientConfigs[0]; 214 | logger.warning(`Setting fallback to ${defaultClientConfig.str('id')}.`); 215 | } 216 | } 217 | return defaultClientConfig; 218 | } 219 | 220 | function createConfigsMap(configs) { 221 | assert(Array.isArray(configs), 'Must pass an array to get map of configs'); 222 | return configs.reduce((clientsMap, cfg) => { 223 | assert(cfg instanceof Config, 'Must pass an array of configs'); 224 | const id = cfg.str('id'); 225 | assert(id, 'client config must have id'); 226 | clientsMap.set(id, cfg); 227 | return clientsMap; 228 | }, new Map()); 229 | } 230 | 231 | /* 232 | * create client config 233 | * Note: This will actually create the file in your bpanel prefix location 234 | * @param {string} id - id for the client 235 | * @param {Object} options object for a bcoin/hsd client 236 | * @param {bool} force - whether or not to force config creation if client 237 | * can't connect 238 | * @returns {bcfg.Config} 239 | */ 240 | async function createClientConfig(config, force = false) { 241 | assert(config instanceof Config, 'Must pass bcfg config to create client'); 242 | assert(config.has('id'), 'Config must have an id set'); 243 | 244 | const logger = config.obj('logger'); 245 | 246 | const appConfig = loadConfig('bpanel'); 247 | const clientsDir = appConfig.str('clients-dir', 'clients'); 248 | 249 | // get full path to client configs relative to the project 250 | // prefix which defaults to `~/.bpanel` 251 | const clientsPath = resolve(appConfig.prefix, clientsDir); 252 | 253 | const [err, clientErrors] = await testConfigOptions(config); 254 | assert(typeof force === 'boolean', 'The force argument must be a bool.'); 255 | if (err && force) { 256 | logger.warning(clientErrors.message); 257 | logger.warning('Creating config file anyway...'); 258 | } else if (err) { 259 | throw clientErrors; 260 | } 261 | 262 | let configTxt = ''; 263 | for (let key in config.options) { 264 | const configKey = key 265 | .replace('-', '') 266 | .replace('_', '') 267 | .toLowerCase(); 268 | const text = `${configKey}: ${config.options[key]}\n`; 269 | configTxt = configTxt.concat(text); 270 | } 271 | if (!fs.existsSync(clientsPath)) { 272 | logger.warning( 273 | 'Could not find requested client directory at %s. Creating new one...', 274 | clientsPath 275 | ); 276 | fs.mkdirpSync(clientsPath); 277 | } 278 | fs.writeFileSync(`${clientsPath}/${config.str('id')}.conf`, configTxt); 279 | return config; 280 | } 281 | 282 | class ClientErrors extends Error { 283 | constructor(...options) { 284 | super(...options); 285 | this.failed = []; 286 | } 287 | 288 | addFailed(clientType, error) { 289 | assert(typeof clientType === 'string'); 290 | this[clientType] = { message: error.message, ...error }; 291 | this.failed.push(clientType); 292 | } 293 | 294 | composeMessage() { 295 | // compose only if there isn't a custom message 296 | if (!this.message.length) { 297 | const prefix = 298 | 'There was a problem connecting with the following clients: '; 299 | this.message = prefix.concat(this.failed.join(', ')); 300 | } 301 | } 302 | } 303 | 304 | module.exports = { 305 | createClientConfig, 306 | createConfigsMap, 307 | getConfig, 308 | getDefaultConfig, 309 | loadConfig, 310 | loadClientConfigs, 311 | ClientErrors, 312 | testConfigOptions 313 | }; 314 | -------------------------------------------------------------------------------- /server/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module server-utils 5 | */ 6 | 7 | exports.configHelpers = require('./configs'); 8 | exports.clientFactory = require('./clients').clientFactory; 9 | exports.buildClients = require('./clients').buildClients; 10 | exports.clientHelpers = require('./clients'); 11 | exports.attach = require('./attach'); 12 | exports.loadConfig = require('./loadConfig'); 13 | exports.apiFilters = require('./apiFilters'); 14 | exports.pluginUtils = require('./plugins'); 15 | exports.npmExists = require('./npm-exists'); 16 | -------------------------------------------------------------------------------- /server/utils/loadConfig.js: -------------------------------------------------------------------------------- 1 | const assert = require('bsert'); 2 | const fs = require('bfile'); 3 | const Config = require('bcfg'); 4 | 5 | /* 6 | * Load up a bcfg object for a given module and set of options 7 | * @params {string} - name - module name 8 | * @params {object} - [options] - optional options object to inject into config 9 | * @returns {Config} - returns a bcfg object 10 | */ 11 | function loadConfig(name, options = {}) { 12 | assert(typeof name === 'string', 'Must pass a name to load config'); 13 | const config = new Config(name); 14 | 15 | // load any custom configs being passed in 16 | config.inject(options); 17 | 18 | config.load({ 19 | env: true 20 | }); 21 | 22 | if (name === 'bpanel') { 23 | config.load({ 24 | argv: true 25 | }); 26 | 27 | const configFile = config.location('config.js'); 28 | if (fs.existsSync(configFile)) { 29 | const fileOptions = require(configFile); 30 | config.inject(fileOptions); 31 | } 32 | } 33 | 34 | return config; 35 | } 36 | 37 | module.exports = loadConfig; 38 | -------------------------------------------------------------------------------- /server/utils/npm-exists.js: -------------------------------------------------------------------------------- 1 | const { URL } = require('url'); 2 | const brq = require('brq'); 3 | 4 | async function npmExists(_packageName) { 5 | const packageName = _packageName.toString(); 6 | const base = 'https://www.npmjs.org/package/'; 7 | const { href: path } = new URL(packageName, base); 8 | const response = await brq({ url: path, method: 'HEAD' }); 9 | if (200 <= response.statusCode && response.statusCode < 400) return true; 10 | return false; 11 | } 12 | 13 | module.exports = npmExists; 14 | -------------------------------------------------------------------------------- /server/utils/plugins.js: -------------------------------------------------------------------------------- 1 | const assert = require('bsert'); 2 | const Config = require('bcfg'); 3 | 4 | function getPluginEndpoints(config, logger) { 5 | assert(config instanceof Config, 'Expected a bcfg object'); 6 | const beforeMiddleware = []; 7 | const afterMiddleware = []; 8 | // load list of plugins and local plugins from config 9 | const plugins = config.array('plugins', []); 10 | plugins.push(...config.array('localPlugins', [])); 11 | 12 | // go through each plugin id in the list 13 | for (let plugin of plugins) { 14 | assert( 15 | typeof plugin === 'string', 16 | `Expected plugin name to be a string instead got a ${typeof plugin}` 17 | ); 18 | 19 | // require the `server` entrypoint for each (skip if no entry) 20 | let module, pkg; 21 | 22 | try { 23 | pkg = require(`${plugin}/package.json`); 24 | module = require(`${plugin}/server`); 25 | } catch (e) { 26 | logger.debug( 27 | 'Problem loading backend plugins for %s: %s', 28 | plugin, 29 | e.message 30 | ); 31 | } 32 | if (!module || !Object.keys(module).length) continue; 33 | 34 | const { 35 | beforeCoreMiddleware = [], 36 | afterCoreMiddleware = [], 37 | endpoints = [] 38 | } = module; 39 | const { name, version } = pkg; 40 | 41 | logger.info('Building endpoints middleware for %s@%s', name, version); 42 | if (beforeCoreMiddleware) 43 | assert( 44 | Array.isArray(beforeCoreMiddleware), 45 | 'Expected an array for beforeCoreMiddleware export.' 46 | ); 47 | if (afterCoreMiddleware) 48 | assert( 49 | Array.isArray(afterCoreMiddleware), 50 | 'Expected an array for afterCoreMiddleware export.' 51 | ); 52 | 53 | beforeMiddleware.push(...endpoints, ...beforeCoreMiddleware); 54 | afterMiddleware.push(...afterCoreMiddleware); 55 | } 56 | return { beforeMiddleware, afterMiddleware }; 57 | } 58 | 59 | module.exports = { 60 | getPluginEndpoints 61 | }; 62 | -------------------------------------------------------------------------------- /webapp/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpanel-org/bpanel/548d42c72bfb6e798dd8aa1b65ae525c6f71fb32/webapp/assets/favicon.ico -------------------------------------------------------------------------------- /webapp/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpanel-org/bpanel/548d42c72bfb6e798dd8aa1b65ae525c6f71fb32/webapp/assets/logo.png -------------------------------------------------------------------------------- /webapp/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { utils, Text, Link } from '@bpanel/bpanel-ui'; 4 | 5 | const { connectTheme } = utils; 6 | 7 | class Footer extends PureComponent { 8 | static get propTypes() { 9 | return { 10 | footerWidgets: PropTypes.oneOfType([ 11 | PropTypes.arrayOf(PropTypes.func), 12 | PropTypes.func 13 | ]), 14 | CustomChildren: PropTypes.node, 15 | theme: PropTypes.object, 16 | hideFooterAttribution: PropTypes.bool 17 | }; 18 | } 19 | 20 | static get defaultProps() { 21 | return { 22 | footerWidgets: [], 23 | hideFooterAttribution: false 24 | }; 25 | } 26 | 27 | render() { 28 | const { 29 | footerWidgets, 30 | CustomChildren, 31 | theme, 32 | hideFooterAttribution 33 | } = this.props; 34 | let FooterWidget; 35 | if (!Array.isArray(footerWidgets)) FooterWidget = footerWidgets; 36 | return ( 37 | 61 | ); 62 | } 63 | } 64 | 65 | export default connectTheme(Footer); 66 | -------------------------------------------------------------------------------- /webapp/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { utils } from '@bpanel/bpanel-ui'; 4 | 5 | const { connectTheme } = utils; 6 | 7 | class Header extends PureComponent { 8 | static get propTypes() { 9 | return { 10 | headerWidgets: PropTypes.oneOfType([ 11 | PropTypes.arrayOf(PropTypes.func), 12 | PropTypes.func 13 | ]), 14 | CustomChildren: PropTypes.node, 15 | theme: PropTypes.object 16 | }; 17 | } 18 | 19 | render() { 20 | const { headerWidgets = [], CustomChildren, theme } = this.props; 21 | let HeaderWidget; 22 | if (!Array.isArray(headerWidgets)) HeaderWidget = headerWidgets; 23 | return ( 24 |