Allow tweaking bitcoin.conf directly from the UI

Co-authored-by: Luke Childs <lukechilds123@gmail.com>
Co-authored-by: Mayank Chhabra <mayankchhabra9@gmail.com>
Co-authored-by: Steven Briscoe <me@stevenbriscoe.com>
This commit is contained in:
Nathan Fretz 2022-12-21 08:59:31 -08:00 committed by Luke Childs
parent 66c88969de
commit cc5e1fd98d
25 changed files with 8638 additions and 7185 deletions

3
.gitignore vendored
View File

@ -10,4 +10,5 @@ lb_settings.json
.nyc_output
coverage
.todo
bitcoin
data/
.env.local

40
bin/www
View File

@ -8,6 +8,11 @@ var app = require('../app');
var debug = require('debug')('nodejs-regular-webapp2:server');
var http = require('http');
const diskLogic = require('../logic/disk');
const diskService = require('../services/disk');
const bitcoindLogic = require('../logic/bitcoind');
const constants = require('../utils/const');
/**
* Get port from environment and store in Express.
*/
@ -29,6 +34,34 @@ server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Function to create default bitcoin core config files if they do not already exist.
*/
async function createConfFilesAndRestartBitcoind() {
console.log('umbrel-bitcoin.conf does not exist, creating config files with Umbrel default values');
const config = await diskLogic.getJsonStore();
// set torProxyForClearnet to false for existing installs
if (constants.BITCOIN_INITIALIZE_WITH_CLEARNET_OVER_TOR) config.torProxyForClearnet = true;
await diskLogic.applyCustomBitcoinConfig(config);
const MAX_TRIES = 60;
let tries = 0;
while (tries < MAX_TRIES) {
try {
await bitcoindLogic.stop();
break;
} catch (error) {
console.error(error);
tries++;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
/**
* Normalize a port into a number, string, or false.
*/
@ -81,11 +114,16 @@ function onError(error) {
* Event listener for HTTP server "listening" event.
*/
function onListening() {
async function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
console.log('Listening on ' + bind);
// if umbrel-bitcoin.conf does not exist, create default bitcoin core config files and restart bitcoind.
if (! await diskService.fileExists(constants.UMBREL_BITCOIN_CONF_FILEPATH)) {
createConfFilesAndRestartBitcoind();
}
}

View File

@ -1,15 +1,48 @@
version: "3.7"
services:
i2pd_daemon:
container_name: i2pd_daemon
image: purplei2p/i2pd:release-2.44.0@sha256:d154a599793c393cf9c91f8549ba7ece0bb40e5728e1813aa6dd4c210aa606f6
user: "1000:1000"
command: --sam.enabled=true --sam.address=0.0.0.0 --sam.port=7656 --loglevel=error
restart: on-failure
volumes:
- ${PWD}/data/i2pd:/home/i2pd/data
networks:
default:
ipv4_address: "10.21.0.2"
tor_server:
container_name: tor_server
image: getumbrel/tor:0.4.7.8@sha256:2ace83f22501f58857fa9b403009f595137fa2e7986c4fda79d82a8119072b6a
user: "1000:1000"
restart: on-failure
entrypoint: /tor-entrypoint/tor-entrypoint.sh
volumes:
- ${PWD}/data/tor:/etc/tor
- ${PWD}/tor-entrypoint:/tor-entrypoint
environment:
HOME: "/tmp"
networks:
default:
ipv4_address: "10.21.0.3"
bitcoind:
image: lncm/bitcoind:v22.0@sha256:37a1adb29b3abc9f972f0d981f45e41e5fca2e22816a023faa9fdc0084aa4507
command: -regtest -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcauth=umbrel:5071d8b3ba93e53e414446ff9f1b7d7b$$375e9731abd2cd2c2c44d2327ec19f4f2644256fdeaf4fc5229bf98b778aafec
image: lncm/bitcoind:v24.0@sha256:db19fe46f30acd3854f4f0d239278137d828ce3728f925c8d92faaab1ba8556a
user: "1000:1000"
command: -port=8333 -rpcport=8332 -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcauth=umbrel:5071d8b3ba93e53e414446ff9f1b7d7b$$375e9731abd2cd2c2c44d2327ec19f4f2644256fdeaf4fc5229bf98b778aafec
volumes:
- ${PWD}/data/bitcoin:/data/.bitcoin
restart: on-failure
restart: unless-stopped
stop_grace_period: 15m30s
ports:
- "18443:18443" # regtest
- "8332:8332" # rpc
- "8333:8333" # p2p
networks:
default:
ipv4_address: "10.21.0.4"
server:
build: .
depends_on: [bitcoind]
@ -17,12 +50,35 @@ services:
restart: on-failure
ports:
- "3005:3005"
volumes:
- ${PWD}/data/app:/data # volume to persist advanced settings json
- ${PWD}/data/bitcoin:/bitcoin/.bitcoin # volume to persist umbrel-bitcoin.conf and bitcoin.conf
environment:
PORT: "3005"
BITCOIN_HOST: "bitcoind"
RPC_PORT: "18443" # - regtest
BITCOIN_P2P_PORT: 8333
BITCOIN_RPC_PORT: 8332
BITCOIN_DEFAULT_NETWORK: "regtest"
RPC_USER: "umbrel"
RPC_PASSWORD: "moneyprintergobrrr"
BITCOIN_RPC_HIDDEN_SERVICE: "somehiddenservice.onion"
BITCOIN_P2P_HIDDEN_SERVICE: "anotherhiddenservice.onion"
DEVICE_DOMAIN_NAME: "test.local"
DEVICE_DOMAIN_NAME: "test.local"
BITCOIND_IP: "10.21.0.4"
TOR_PROXY_IP: "10.21.0.3"
TOR_PROXY_PORT: "9050"
TOR_PROXY_CONTROL_PORT: "9051"
TOR_PROXY_CONTROL_PASSWORD: "umbrelisneat"
I2P_DAEMON_IP: "10.21.0.2"
I2P_DAEMON_PORT: "7656"
networks:
default:
ipv4_address: "10.21.0.5"
networks:
default:
name: advanced_settings_test_network
ipam:
driver: default
config:
- subnet: "10.21.0.0/16"

View File

@ -12,20 +12,34 @@ async function getConnectionsCount() {
var outBoundConnections = 0;
var inBoundConnections = 0;
var clearnetConnections = 0;
var torConnections = 0;
var i2pConnections = 0;
peerInfo.result.forEach(function(peer) {
if (peer.inbound === false) {
outBoundConnections++;
return;
} else {
inBoundConnections++;
}
inBoundConnections++;
if (peer.network === "onion") {
torConnections++;
} else if (peer.network === "i2p") {
i2pConnections++;
} else {
// ipv4 and ipv6 are clearnet
clearnetConnections++;
}
});
const connections = {
total: inBoundConnections + outBoundConnections,
inbound: inBoundConnections,
outbound: outBoundConnections
outbound: outBoundConnections,
clearnet: clearnetConnections,
tor: torConnections,
i2p: i2pConnections
};
return connections;
@ -72,12 +86,16 @@ async function getLocalSyncInfo() {
var blockCount = blockChainInfo.blocks;
var headerCount = blockChainInfo.headers;
var percent = blockChainInfo.verificationprogress;
var pruned = blockChainInfo.pruned;
var pruneTargetSize = blockChainInfo.pruneTargetSize;
return {
chain,
percent,
currentBlock: blockCount,
headerCount: headerCount // eslint-disable-line object-shorthand,
headerCount: headerCount, // eslint-disable-line object-shorthand,
pruned,
pruneTargetSize
};
}
@ -92,6 +110,7 @@ async function getSyncStatus() {
return localSyncInfo;
}
// TODO - consider using getNetworkInfo for info on proxy for ipv4 and ipv6
async function getVersion() {
const networkInfo = await bitcoindService.getNetworkInfo();
const unformattedVersion = networkInfo.result.subversion;
@ -259,6 +278,11 @@ async function nodeStatusSummary() {
};
}
async function stop() {
const stopResponse = await bitcoindService.stop();
return {stopResponse};
}
module.exports = {
getBlockHash,
getTransaction,
@ -273,5 +297,6 @@ module.exports = {
getSyncStatus,
getVersion,
nodeStatusDump,
nodeStatusSummary
nodeStatusSummary,
stop
};

176
logic/disk.js Normal file
View File

@ -0,0 +1,176 @@
const fs = require("fs");
const path = require("path");
const constants = require("utils/const.js");
const diskService = require("services/disk");
// TODO - consider moving these unit conversions to utils/const.js
const GB_TO_MiB = 953.674;
const MB_TO_MiB = 0.953674;
const DEFAULT_ADVANCED_SETTINGS = {
clearnet: true,
torProxyForClearnet: false,
tor: true,
i2p: true,
incomingConnections: false,
cacheSizeMB: 450,
mempoolFullRbf: false,
prune: {
enabled: false,
pruneSizeGB: 300,
},
reindex: false,
network: constants.BITCOIN_DEFAULT_NETWORK
}
async function getJsonStore() {
try {
const jsonStore = await diskService.readJsonFile(constants.JSON_STORE_FILE);
return { ...DEFAULT_ADVANCED_SETTINGS, ...jsonStore };
} catch (error) {
return DEFAULT_ADVANCED_SETTINGS;
}
}
async function applyCustomBitcoinConfig(bitcoinConfig) {
await applyBitcoinConfig(bitcoinConfig, false);
}
async function applyDefaultBitcoinConfig() {
await applyBitcoinConfig(DEFAULT_ADVANCED_SETTINGS, true);
}
async function applyBitcoinConfig(bitcoinConfig, shouldOverwriteExistingFile) {
await Promise.all([
updateJsonStore(bitcoinConfig),
generateUmbrelBitcoinConfig(bitcoinConfig),
generateBitcoinConfig(shouldOverwriteExistingFile),
]);
}
// There's a race condition here if you do two updates in parallel but it's fine for our current use case
async function updateJsonStore(newProps) {
const jsonStore = await getJsonStore();
return diskService.writeJsonFile(constants.JSON_STORE_FILE, {
...jsonStore,
...newProps
});
}
// creates umbrel-bitcoin.conf
function generateUmbrelBitcoinConfig(settings) {
const confString = settingsToMultilineConfString(settings);
return diskService.writePlainTextFile(constants.UMBREL_BITCOIN_CONF_FILEPATH, confString);
}
// creates bitcoin.conf with includeconf=umbrel-bitcoin.conf
async function generateBitcoinConfig(shouldOverwriteExistingFile = false) {
const baseName = path.basename(constants.UMBREL_BITCOIN_CONF_FILEPATH);
const includeConfString = `# Load additional configuration file, relative to the data directory.\nincludeconf=${baseName}`;
const fileExists = await diskService.fileExists(constants.BITCOIN_CONF_FILEPATH);
// if bitcoin.conf does not exist or should be overwritten, create it with includeconf=umbrel-bitcoin.conf
if (!fileExists || shouldOverwriteExistingFile) {
return await diskService.writePlainTextFile(constants.BITCOIN_CONF_FILEPATH, includeConfString);
}
const existingConfContents = await diskService.readUtf8File(constants.BITCOIN_CONF_FILEPATH);
// if bitcoin.conf exists but does not include includeconf=umbrel-bitcoin.conf, add includeconf=umbrel-bitcoin.conf to the top of the file
if (!existingConfContents.includes(includeConfString)) {
return await diskService.writePlainTextFile(constants.BITCOIN_CONF_FILEPATH, `${includeConfString}\n${existingConfContents}`);
}
// do nothing if bitcoin.conf exists and contains includeconf=umbrel-bitcoin.conf
}
function settingsToMultilineConfString(settings) {
const umbrelBitcoinConfig = [];
// [CHAIN]
umbrelBitcoinConfig.push("# [chain]");
if (settings.network !== 'main') {
umbrelBitcoinConfig.push(`chain=${settings.network}`)
}
// [CORE]
umbrelBitcoinConfig.push("");
umbrelBitcoinConfig.push("# [core]");
// dbcache
umbrelBitcoinConfig.push("# Maximum database cache size in MiB");
umbrelBitcoinConfig.push(`dbcache=${Math.round(settings.cacheSizeMB * MB_TO_MiB)}`);
// mempoolfullrbf
if (settings.mempoolFullRbf) {
umbrelBitcoinConfig.push("# Allow any transaction in the mempool of Bitcoin Node to be replaced with newer versions of the same transaction that include a higher fee.");
umbrelBitcoinConfig.push('mempoolfullrbf=1');
}
// prune
if (settings.prune.enabled) {
umbrelBitcoinConfig.push("# Reduce disk space requirements to this many MiB by enabling pruning (deleting) of old blocks. This mode is incompatible with -txindex and -coinstatsindex. WARNING: Reverting this setting requires re-downloading the entire blockchain. (default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, greater than or equal to 550 = automatically prune blocks to stay under target size in MiB).");
umbrelBitcoinConfig.push(`prune=${Math.round(settings.prune.pruneSizeGB * GB_TO_MiB)}`);
}
// reindex
if (settings.reindex) {
umbrelBitcoinConfig.push('# Rebuild chain state and block index from the blk*.dat files on disk.');
umbrelBitcoinConfig.push('reindex=1');
}
// [NETWORK]
umbrelBitcoinConfig.push("");
umbrelBitcoinConfig.push("# [network]");
// clearnet
if (settings.clearnet) {
umbrelBitcoinConfig.push('# Connect to peers over the clearnet.')
umbrelBitcoinConfig.push('onlynet=ipv4');
umbrelBitcoinConfig.push('onlynet=ipv6');
}
if (settings.torProxyForClearnet) {
umbrelBitcoinConfig.push('# Connect through <ip:port> SOCKS5 proxy.');
umbrelBitcoinConfig.push(`proxy=${constants.TOR_PROXY_IP}:${constants.TOR_PROXY_PORT}`);
}
// tor
if (settings.tor) {
umbrelBitcoinConfig.push('# Use separate SOCKS5 proxy <ip:port> to reach peers via Tor hidden services.');
umbrelBitcoinConfig.push('onlynet=onion');
umbrelBitcoinConfig.push(`onion=${constants.TOR_PROXY_IP}:${constants.TOR_PROXY_PORT}`);
umbrelBitcoinConfig.push('# Tor control <ip:port> and password to use when onion listening enabled.');
umbrelBitcoinConfig.push(`torcontrol=${constants.TOR_PROXY_IP}:${constants.TOR_PROXY_CONTROL_PORT}`);
umbrelBitcoinConfig.push(`torpassword=${constants.TOR_PROXY_CONTROL_PASSWORD}`);
}
// i2p
if (settings.i2p) {
umbrelBitcoinConfig.push('# I2P SAM proxy <ip:port> to reach I2P peers.');
umbrelBitcoinConfig.push(`i2psam=${constants.I2P_DAEMON_IP}:${constants.I2P_DAEMON_PORT}`);
umbrelBitcoinConfig.push('onlynet=i2p');
}
// incoming connections
umbrelBitcoinConfig.push('# Enable/disable incoming connections from peers.');
const listen = settings.incomingConnections ? 1 : 0;
umbrelBitcoinConfig.push(`listen=1`);
umbrelBitcoinConfig.push(`listenonion=${listen}`);
umbrelBitcoinConfig.push(`i2pacceptincoming=${listen}`);
umbrelBitcoinConfig.push(`# Required to configure Tor control port properly`);
umbrelBitcoinConfig.push(`[${settings.network}]`);
umbrelBitcoinConfig.push(`bind=0.0.0.0:8333`);
umbrelBitcoinConfig.push(`bind=${constants.BITCOIND_IP}:8334=onion`);
return umbrelBitcoinConfig.join('\n');
}
module.exports = {
getJsonStore,
applyCustomBitcoinConfig,
applyDefaultBitcoinConfig,
};

View File

@ -2,19 +2,19 @@ const constants = require('utils/const.js');
const NodeError = require('models/errors.js').NodeError;
function getBitcoinP2PConnectionDetails() {
const torAddress = constants.BITCOIN_P2P_HIDDEN_SERVICE;
const port = constants.BITCOIN_P2P_PORT;
const torConnectionString = `${torAddress}:${port}`;
const localAddress = constants.DEVICE_DOMAIN_NAME;
const localConnectionString = `${localAddress}:${port}`;
const torAddress = constants.BITCOIN_P2P_HIDDEN_SERVICE;
const port = constants.BITCOIN_P2P_PORT;
const torConnectionString = `${torAddress}:${port}`;
const localAddress = constants.DEVICE_DOMAIN_NAME;
const localConnectionString = `${localAddress}:${port}`;
return {
torAddress,
port,
torConnectionString,
localAddress,
localConnectionString
};
return {
torAddress,
port,
torConnectionString,
localAddress,
localConnectionString
};
}
function getBitcoinRPCConnectionDetails() {

View File

@ -2,6 +2,8 @@ const express = require('express');
const router = express.Router();
const systemLogic = require('logic/system.js');
const diskLogic = require('logic/disk');
const bitcoindLogic = require('logic/bitcoind.js');
const safeHandler = require('utils/safeHandler');
router.get('/bitcoin-p2p-connection-details', safeHandler(async(req, res) => {
@ -16,4 +18,48 @@ router.get('/bitcoin-rpc-connection-details', safeHandler(async(req, res) => {
return res.json(connectionDetails);
}));
router.get('/bitcoin-config', safeHandler(async(req, res) => {
const bitcoinConfig = await diskLogic.getJsonStore();
return res.json(bitcoinConfig);
}));
// updateJsonStore / generateUmbrelBitcoinConfig / generateBitcoinConfig are all called through these routes below so that even if user closes the browser prematurely, the backend will complete the update.
router.post('/update-bitcoin-config', safeHandler(async(req, res) => {
// store old bitcoinConfig in memory to revert to in case of errors setting new config and restarting bitcoind
const oldBitcoinConfig = await diskLogic.getJsonStore();
const newBitcoinConfig = req.body.bitcoinConfig;
try {
await diskLogic.applyCustomBitcoinConfig(newBitcoinConfig);
await bitcoindLogic.stop();
res.json({success: true});
} catch (error) {
// revert everything to old config values
await diskLogic.applyCustomBitcoinConfig(oldBitcoinConfig);
res.json({success: false}); // show error to user in UI
}
}));
router.post('/restore-default-bitcoin-config', safeHandler(async(req, res) => {
// store old bitcoinConfig in memory to revert to in case of errors setting new config and restarting bitcoind
const oldBitcoinConfig = await diskLogic.getJsonStore();
try {
await diskLogic.applyDefaultBitcoinConfig();
await bitcoindLogic.stop();
res.json({success: true});
} catch (error) {
// revert everything to old config values
await diskLogic.applyCustomBitcoinConfig(oldBitcoinConfig);
res.json({success: false}); // show error to user in UI
}
}));
module.exports = router;

View File

@ -1,6 +1,5 @@
const RpcClient = require('bitcoind-rpc');
const camelizeKeys = require('camelize-keys');
const BitcoindError = require('models/errors.js').BitcoindError;
const BITCOIND_RPC_PORT = process.env.RPC_PORT || 8332; // eslint-disable-line no-magic-numbers, max-len
@ -13,7 +12,7 @@ const rpcClient = new RpcClient({
user: BITCOIND_RPC_USER, // eslint-disable-line object-shorthand
pass: BITCOIND_RPC_PASSWORD, // eslint-disable-line object-shorthand
host: BITCOIND_HOST,
port: BITCOIND_RPC_PORT,
port: BITCOIND_RPC_PORT
});
function promiseify(rpcObj, rpcFn, what) {
@ -115,6 +114,10 @@ function help() {
return promiseify(rpcClient, rpcClient.help, 'help data');
}
function stop() {
return promiseify(rpcClient, rpcClient.stop, 'stop');
}
module.exports = {
getMiningInfo,
getBestBlockHash,
@ -127,4 +130,5 @@ module.exports = {
getMempoolInfo,
getNetworkInfo,
help,
stop,
};

126
services/disk.js Normal file
View File

@ -0,0 +1,126 @@
/**
* Generic disk functions.
*/
const logger = require("utils/logger");
const fs = require("fs");
const crypto = require("crypto");
const UINT32_BYTES = 4;
// Asynchronously checks if a file exists
async function fileExists(filePath) {
try {
await fs.promises.access(filePath);
return true;
} catch (err) {
return false;
}
}
// Reads a file. Wraps fs.readFile into a native promise
function readFile(filePath, encoding) {
return new Promise((resolve, reject) =>
fs.readFile(filePath, encoding, (err, str) => {
if (err) {
reject(err);
} else {
resolve(str);
}
})
);
}
// Reads a file as a utf8 string. Wraps fs.readFile into a native promise
async function readUtf8File(filePath) {
return (await readFile(filePath, "utf8")).trim();
}
async function readJsonFile(filePath) {
return readUtf8File(filePath).then(JSON.parse);
}
// Writes a string to a file. Wraps fs.writeFile into a native promise
// This is _not_ concurrency safe, so don't export it without making it like writeJsonFile
function writeFile(filePath, data, encoding) {
return new Promise((resolve, reject) =>
fs.writeFile(filePath, data, encoding, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
);
}
function writeJsonFile(filePath, obj) {
const tempFileName = `${filePath}.${crypto
.randomBytes(UINT32_BYTES)
.readUInt32LE(0)}`;
return writeFile(tempFileName, JSON.stringify(obj, null, 2), "utf8")
.then(
() =>
new Promise((resolve, reject) =>
fs.rename(tempFileName, filePath, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
)
)
.catch((err) => {
if (err) {
fs.unlink(tempFileName, (err) => {
logger.warn("Error removing temporary file after error", "disk", {
err,
tempFileName,
});
});
}
throw err;
});
}
function writePlainTextFile(filePath, string) {
const tempFileName = `${filePath}.${crypto
.randomBytes(UINT32_BYTES)
.readUInt32LE(0)}`;
return writeFile(tempFileName, string, "utf8")
.then(
() =>
new Promise((resolve, reject) =>
fs.rename(tempFileName, filePath, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
)
).catch((err) => {
if (err) {
fs.unlink(tempFileName, (err) => {
logger.warn("Error removing temporary file after error", "disk", {
err,
tempFileName,
});
});
}
})
}
module.exports = {
fileExists,
readFile,
readUtf8File,
readJsonFile,
writeJsonFile,
writeFile,
writePlainTextFile
};

View File

@ -0,0 +1,11 @@
#!/bin/bash
TORRC_PATH="/etc/tor/torrc"
PLAIN_TEXT_PASSWORD="umbrelisneat"
HASH_PASSWORD=`tor --hash-password "$PLAIN_TEXT_PASSWORD"`
# clobber old file
echo "SocksPort 0.0.0.0:9050" > "${TORRC_PATH}"
echo "ControlPort 0.0.0.0:9051" >> "${TORRC_PATH}"
echo "CookieAuthentication 1" >> "${TORRC_PATH}"
echo "CookieAuthFileGroupReadable 1" >> "${TORRC_PATH}"
echo "HashedControlPassword $HASH_PASSWORD" >> "${TORRC_PATH}"
tor -f "${TORRC_PATH}"

24
ui/package-lock.json generated
View File

@ -16,6 +16,7 @@
"countup.js": "^2.0.4",
"highcharts": "^9.3.2",
"highcharts-vue": "^1.4.0",
"lodash.clonedeep": "^4.5.0",
"moment": "^2.24.0",
"qrcode.vue": "^1.7.0",
"vue": "^2.6.10",
@ -9609,6 +9610,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -15216,10 +15222,9 @@
"license": "MIT"
},
"node_modules/vue-slider-component": {
"version": "3.2.15",
"resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-3.2.15.tgz",
"integrity": "sha512-FpmMsQ6MQFn22B6boDcEjRmuawdaHwjHRVZiuv5w37jijHra6+HogjSrh3mb42jE+PUIFFagXi36oFEzpDbadg==",
"license": "MIT",
"version": "3.2.20",
"resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-3.2.20.tgz",
"integrity": "sha512-S5+4d6zdL+/ClpDQoIgImIdXRv2b+75PIy3cDGsZsakhroJD6cSFA0juY/AblGqhvIkNcBIU354eOw6T26DWbA==",
"dependencies": {
"core-js": "^3.6.5",
"vue-property-decorator": "^8.0.0"
@ -22998,6 +23003,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -27180,9 +27190,9 @@
"integrity": "sha512-FUlILrW3DGitS2h+Xaw8aRNvGTwtuaxrRkNSHWTizOfLUie7wuYwezeZ50iflRn8YPV5kxmU2LQuu3nM/b3Zsg=="
},
"vue-slider-component": {
"version": "3.2.15",
"resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-3.2.15.tgz",
"integrity": "sha512-FpmMsQ6MQFn22B6boDcEjRmuawdaHwjHRVZiuv5w37jijHra6+HogjSrh3mb42jE+PUIFFagXi36oFEzpDbadg==",
"version": "3.2.20",
"resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-3.2.20.tgz",
"integrity": "sha512-S5+4d6zdL+/ClpDQoIgImIdXRv2b+75PIy3cDGsZsakhroJD6cSFA0juY/AblGqhvIkNcBIU354eOw6T26DWbA==",
"requires": {
"core-js": "^3.6.5",
"vue-property-decorator": "^8.0.0"

View File

@ -16,6 +16,7 @@
"countup.js": "^2.0.4",
"highcharts": "^9.3.2",
"highcharts-vue": "^1.4.0",
"lodash.clonedeep": "^4.5.0",
"moment": "^2.24.0",
"qrcode.vue": "^1.7.0",
"vue": "^2.6.10",

View File

@ -62,7 +62,7 @@ export default {
this.loadingPollInProgress = true;
// Then check if middleware api is up
// Then check if middleware api and bitcoin core are both up
if (this.loadingProgress <= 40) {
this.loadingProgress = 40;
await this.$store.dispatch("system/getApi");

View File

@ -0,0 +1,437 @@
<template v-slot:modal-header="{ close }" title="Advanced Settings">
<b-form @submit.prevent="submit">
<div
class="px-0 px-sm-3 pb-3 d-flex flex-column justify-content-between w-100"
>
<h3 class="mt-1">Advanced Settings</h3>
<b-alert variant="warning" show class="mb-3">
<small>
Be careful when changing the settings below as they may cause issues
with other apps on your Umbrel that connect to your Bitcoin node. Only make
changes if you understand the potential effects on connected apps or
wallets.
</small>
</b-alert>
<b-overlay :show="isSettingsDisabled" rounded="sm">
<div
class="advanced-settings-container d-flex flex-column p-3 pb-sm-3 bg-light mb-2"
>
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="clearnet">
<p class="font-weight-bold mb-0">Outgoing Connections to Clearnet Peers</p>
</label>
</div>
<div>
<toggle-switch
id="clearnet"
class="align-self-center"
:on="settings.clearnet"
@toggle="status => (settings.clearnet = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Connect to peers available on the clearnet (publicly accessible internet).
</small>
</div>
<hr class="advanced-settings-divider" />
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="tor">
<p class="font-weight-bold mb-0">Outgoing Connections to Tor Peers</p>
</label>
</div>
<div>
<toggle-switch
id="tor"
class="align-self-center"
:on="settings.tor"
@toggle="status => (settings.tor = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Connect to peers available on the Tor network.
</small>
</div>
<hr class="advanced-settings-divider" />
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="proxy">
<p class="font-weight-bold mb-0">Connect to all Clearnet Peers over Tor</p>
</label>
</div>
<div>
<toggle-switch
id="proxy"
class="align-self-center"
:on="settings.torProxyForClearnet"
:disabled="isTorProxyDisabled"
:tooltip="torProxyTooltip"
@toggle="status => (settings.torProxyForClearnet = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Connect to peers available on the clearnet via Tor to preserve your anonymity at the cost of slightly less security.
</small>
</div>
<hr class="advanced-settings-divider" />
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="I2P">
<p class="font-weight-bold mb-0">Outgoing Connections to I2P Peers</p>
</label>
</div>
<div>
<toggle-switch
id="I2P"
class="align-self-center"
:on="settings.i2p"
@toggle="status => (settings.i2p = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Connect to peers available on the I2P network.
</small>
</div>
<hr class="advanced-settings-divider" />
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="allow-incoming-connections">
<p class="font-weight-bold mb-0">Incoming Connections</p>
</label>
</div>
<div class="">
<toggle-switch
id="allow-incoming-connections"
class="align-self-center"
:on="settings.incomingConnections"
@toggle="status => (settings.incomingConnections = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Broadcast your node to the Bitcoin network to help other nodes
access the blockchain. You may need to set up port forwarding on
your router to allow incoming connections from clearnet-only peers.
</small>
</div>
<hr class="advanced-settings-divider" />
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="cache-size">
<p class="font-weight-bold mb-0">Cache Size (MB)</p>
</label>
</div>
<div class="">
<b-form-input
class="advanced-settings-input"
id="cache-size"
type="number"
v-model="settings.cacheSizeMB"
></b-form-input>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Choose the size of the UTXO set to store in RAM. A larger cache can
speed up the initial synchronization of your Bitcoin node, but after
the initial sync is complete, a larger cache value does not significantly
improve performance and may use more RAM than needed.
</small>
</div>
<hr class="advanced-settings-divider" />
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="mempool">
<p class="font-weight-bold mb-0">Replace-By-Fee (RBF) for All Transactions</p>
</label>
</div>
<div>
<toggle-switch
id="mempool"
class="align-self-center"
:on="settings.mempoolFullRbf"
@toggle="status => (settings.mempoolFullRbf = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Allow any transaction in the mempool of your Bitcoin node to be replaced with
a newer version of the same transaction that includes a higher fee.
</small>
</div>
<hr class="advanced-settings-divider" />
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="prune-old-blocks">
<p class="font-weight-bold mb-0">Prune Old Blocks</p>
</label>
</div>
<div>
<toggle-switch
id="prune-old-blocks"
class="align-self-center"
:on="settings.prune.enabled"
@toggle="status => (settings.prune.enabled = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Save storage space by pruning (deleting) old blocks and keeping only
a limited copy of the blockchain. Use the slider to choose the size
of the blockchain you want to store. It may take some time for your
node to be online after you turn on pruning. If you turn off pruning
after turning it on, you'll need to download the entire blockchain
again.
</small>
<prune-slider
id="prune-old-blocks"
class="mt-3 mb-3"
:minValue="1"
:maxValue="maxPruneSizeGB"
:startingValue="settings.prune.pruneSizeGB"
:disabled="!settings.prune.enabled"
@change="value => (settings.prune.pruneSizeGB = value)"
></prune-slider>
</div>
<hr class="advanced-settings-divider" />
<!-- <div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="reindex-blockchain">
<p class="font-weight-bold mb-0">Reindex Blockchain</p>
</label>
</div>
<div>
<toggle-switch
id="reindex-blockchain"
class="align-self-center"
:on="settings.reindex"
@toggle="status => (settings.reindex = status)"
></toggle-switch>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Rebuild the database index used by your Bitcoin node. This can
be useful if the index becomes corrupted.
</small>
</div>
<hr class="advanced-settings-divider" /> -->
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="w-75">
<label class="mb-0" for="network">
<p class="font-weight-bold mb-0">Network</p>
</label>
</div>
<div>
<b-form-select
id="network"
v-model="settings.network"
:options="networks"
></b-form-select>
</div>
</div>
</div>
<small class="w-sm-75 d-block text-muted mt-1">
Choose which network you want your Bitcoin node to connect to.
If you change the network, restart your Umbrel to make sure any
apps connected to your Bitcoin node continue to work properly.
</small>
</div>
<!-- template overlay with empty div to show an overlay with no spinner -->
<template #overlay>
<div></div>
</template>
</b-overlay>
<b-alert variant="warning" :show="showOutgoingConnectionsError" class="mt-2" @dismissed="showOutgoingConnectionsError=false">
<small>
Please choose at least one source for outgoing connections (Clearnet, Tor, or I2P).
</small>
</b-alert>
<div class="mt-2 mb-2">
<b-row>
<b-col cols="12" lg="6">
<b-button @click="clickRestoreDefaults" class="btn-border" variant="outline-secondary" block :disabled="isSettingsDisabled">
Restore Default Settings</b-button
>
</b-col>
<b-col cols="12" lg="6">
<b-button class="mt-2 mt-lg-0" variant="success" type="submit" block :disabled="isSettingsDisabled">
Save and Restart Bitcoin Node</b-button
>
</b-col>
</b-row>
</div>
</div>
</b-form>
</template>
<script>
import cloneDeep from "lodash.clonedeep";
import { mapState } from "vuex";
import ToggleSwitch from "./Utility/ToggleSwitch.vue";
import PruneSlider from "./PruneSlider.vue";
export default {
data() {
return {
settings: {},
networks: [
{ value: "main", text: "mainnet" },
{ value: "test", text: "testnet" },
{ value: "signet", text: "signet" },
{ value: "regtest", text: "regtest" }
],
maxPruneSizeGB: 300,
showOutgoingConnectionsError: false
};
},
computed: {
...mapState({
bitcoinConfig: state => state.user.bitcoinConfig,
rpc: state => state.bitcoin.rpc,
p2p: state => state.bitcoin.p2p
}),
isTorProxyDisabled() {
return !this.settings.clearnet || !this.settings.tor;
},
torProxyTooltip() {
if (!this.settings.clearnet || !this.settings.tor) {
return "Outgoing connections to both clearnet and Tor peers must be enabled to turn this on.";
} else {
return "";
}
}
},
watch: {
isTorProxyDisabled(value) {
if (!value) return;
this.settings.torProxyForClearnet = false;
}
},
props: {
isSettingsDisabled: {
type: Boolean,
default: false
}
},
created() {
this.setSettings();
},
components: {
ToggleSwitch,
PruneSlider
},
methods: {
submit() {
this.showOutgoingConnectionsError = false;
if (!this.isOutgoingConnectionsValid()) return this.showOutgoingConnectionsError = true;
this.$emit("submit", this.settings);
},
clickRestoreDefaults() {
if (window.confirm("Are you sure you want to restore the default settings?")) {
this.$emit("clickRestoreDefaults");
}
},
setSettings() {
// deep clone bitcoinConfig in order to properly reset state if user hides modal instead of clicking the save and restart button
this.settings = cloneDeep(this.bitcoinConfig);
},
isOutgoingConnectionsValid() {
return this.settings.clearnet || this.settings.tor || this.settings.i2p;
}
}
};
</script>
<!-- removed scoped in order to place scrollbar on bootstrap-vue .modal-body. Increased verbosity on other classes-->
<style lang="scss">
.advanced-settings-container {
border-radius: 1rem;
.advanced-settings-divider {
// same styles as bootstrap <b-dropdown-divider/>
height: 0;
margin: 1.25rem 0;
overflow: hidden;
border-top: 1px solid #e9ecef;
}
.advanced-settings-input {
max-width: 75px;
}
// to remove arrows on number input field
.advanced-settings-input::-webkit-outer-spin-button, .advanced-settings-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.advanced-settings-input[type="number"] {
-moz-appearance: textfield;
}
}
.btn-border {
border: solid 1px !important;
}
.modal-body::-webkit-scrollbar {
width: 5px;
}
.modal-body::-webkit-scrollbar-track {
background: transparent;
margin-block-end: 1rem;
}
.modal-body::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.4);
border-radius: 10px;
}
.modal-body::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.5);
}
/* sm breakpoint */
@media (min-width: 576px) {
.w-sm-75 {
width: 75% !important;
}
}
</style>

View File

@ -138,12 +138,17 @@ export default {
},
computed: {
...mapState({
isBitcoinCoreOperational: state => state.bitcoin.operational,
syncPercent: state => state.bitcoin.percent,
blocks: state => state.bitcoin.blocks
})
},
methods: {
async fetchBlocks() {
// don't poll if bitcoin core isn't yet running
if (!this.isBitcoinCoreOperational) {
return;
}
//prevent multiple polls if previous poll already in progress
if (this.pollInProgress) {
return;

View File

@ -5,6 +5,7 @@
<img alt="Umbrel" src="@/assets/icon.svg" class="mb-5 logo" />
<!-- <b-spinner class="my-4" variant="primary"></b-spinner> -->
<small v-if="isRestarting" class="text-muted mb-3">Restarting...</small>
<b-progress
:value="progress"
class="mb-2 w-75"
@ -25,10 +26,17 @@
<script>
export default {
data() {
return {};
return {
isRestarting: false
};
},
props: { progress: Number },
created() {},
created() {
if (this.$route.query.hasOwnProperty("restart")) {
this.isRestarting = true;
this.$router.replace({ query: {} });
}
},
methods: {},
components: {}
};

View File

@ -0,0 +1,85 @@
<template>
<div class="prune-slider">
<vue-slider
v-model="value"
:tooltip="'always'"
:min="minValue"
:max="maxValue"
:interval="1"
:contrained="true"
:disabled="disabled"
@change="change"
>
<template v-slot:tooltip="{ value, focus }">
<div :class="['custom-tooltip', { focus }]">
<small class="text-muted">{{ value }}GB</small>
</div>
</template>
</vue-slider>
</div>
</template>
<script>
import VueSlider from "vue-slider-component";
import "vue-slider-component/theme/default.css";
export default {
components: {
VueSlider
},
data() {
return {
value: this.startingValue
};
},
props: {
disabled: {
type: Boolean,
default: false
},
minValue: {
type: Number,
required: true
},
maxValue: {
type: Number,
required: true
},
startingValue: {
type: Number,
required: true
}
},
computed: {},
methods: {
change() {
return this.$emit("change", this.value);
}
}
};
</script>
<style lang="scss">
$dotShadow: 0px 4px 10px rgba(0, 0, 0, 0.25);
$dotShadowFocus: 0px 4px 10px rgba(0, 0, 0, 0.4);
.custom-tooltip {
transform: translateY(50px);
}
.prune-slider .vue-slider-rail {
cursor: pointer;
background: linear-gradient(to right, #f6b900, #00cd98);
}
.prune-slider .vue-slider-process {
background-color: transparent;
}
.prune-slider .vue-slider-disabled {
.vue-slider-rail {
cursor: not-allowed;
background: #ccc;
}
}
</style>

View File

@ -24,6 +24,27 @@
<span class="text-muted" style="margin-left: 0.5rem;">{{
suffix
}}</span>
<div class="ml-1">
<b-popover
v-if="showPopover"
:target="popoverId"
placement="bottom"
triggers="hover focus"
>
<p v-for="content in popoverContent" :key="content" class="m-0">{{ content }}</p>
</b-popover>
<b-icon
v-if="showPopover"
:id="popoverId"
icon="info-circle"
size="lg"
style="cursor: pointer"
class="text-muted"
></b-icon>
</div>
</div>
</div>
<div
@ -100,6 +121,18 @@ export default {
showPercentChange: {
type: Boolean,
default: false
},
showPopover: {
type: Boolean,
default: false
},
popoverId: {
type: String,
default: ""
},
popoverContent: {
type: Array,
default: () => []
}
},
computed: {

View File

@ -0,0 +1,101 @@
<template>
<!-- div wrapper with v-b-tooltip to allow tooltip to show when toggle is disabled -->
<div v-b-tooltip.hover.left :title="tooltip">
<div
class="toggle"
:class="{
'toggle-off': !on,
'toggle-on': on,
'toggle-disabled': disabled,
'toggle-loading': loading
}"
:disabled="disabled"
@click="toggle"
>
<div
class="toggle-switch justify-items-center"
:class="{
'toggle-switch-off': !on,
'toggle-switch-on': on
}"
></div>
</div>
</div>
</template>
<script>
export default {
methods: {
toggle() {
if (this.disabled) {
return;
}
return this.$emit("toggle", !this.on);
}
},
props: {
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
tooltip: {
type: String,
default: ""
},
on: {
type: Boolean,
default: false
}
},
emits: ["toggle"]
};
</script>
<style scoped lang="scss">
$toggle-width: 50px;
.toggle {
border-radius: calc($toggle-width * 0.5);
width: $toggle-width;
height: calc($toggle-width * 0.6);
box-sizing: border-box;
display: flex;
align-items: center;
cursor: pointer;
transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
background: linear-gradient(346.78deg, #f7fcfc 0%, #fafcfa 100%);
border: 1px solid rgba(0, 0, 0, 0.04);
// TODO - may want to calc box-shadow px values to scale correctly with $toggle-width
box-shadow: inset 0px 5px 10px rgba(0, 0, 0, 0.1);
&.toggle-on {
background: var(--success);
box-shadow: none;
}
&.toggle-disabled {
cursor: not-allowed;
}
&.toggle-loading {
cursor: wait;
}
}
.toggle-switch {
margin: 0;
height: calc($toggle-width * 0.5);
width: calc($toggle-width * 0.5);
border-radius: 50%;
background: #ffffff;
transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toggle-switch-off {
transform: translateX(10%);
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
}
.toggle-switch-on {
transform: translateX(90%);
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -4,6 +4,7 @@ import Vuex from "vuex";
//Modules
import system from "./modules/system";
import bitcoin from "./modules/bitcoin";
import user from "./modules/user";
Vue.use(Vuex);
@ -47,6 +48,7 @@ export default new Vuex.Store({
getters,
modules: {
system,
bitcoin
bitcoin,
user
}
});

View File

@ -1,6 +1,8 @@
import API from "@/helpers/api";
import { toPrecision } from "@/helpers/units";
const BYTES_PER_GB = 1000000000;
// Initial state
const state = () => ({
operational: false,
@ -24,6 +26,8 @@ const state = () => ({
},
currentBlock: 0,
chain: "",
pruned: false,
pruneTargetSizeGB: 0,
blockHeight: 0,
blocks: [],
percent: -1, //for loading state
@ -37,7 +41,10 @@ const state = () => ({
peers: {
total: 0,
inbound: 0,
outbound: 0
outbound: 0,
clearnet: 0,
tor: 0,
i2p: 0
},
chartData: []
});
@ -53,6 +60,8 @@ const mutations = {
state.currentBlock = sync.currentBlock;
state.blockHeight = sync.headerCount;
state.chain = sync.chain;
state.pruned = sync.pruned;
state.pruneTargetSizeGB = Math.round(sync.pruneTargetSize / BYTES_PER_GB);
if (sync.status === "calibrating") {
state.calibrating = true;
@ -104,6 +113,9 @@ const mutations = {
state.peers.total = peers.total || 0;
state.peers.inbound = peers.inbound || 0;
state.peers.outbound = peers.outbound || 0;
state.peers.clearnet = peers.clearnet || 0;
state.peers.tor = peers.tor || 0;
state.peers.i2p = peers.i2p || 0;
},
setChartData(state, chartData) {

View File

@ -0,0 +1,38 @@
import API from "@/helpers/api";
// Initial state
const state = () => ({
bitcoinConfig: {}
});
// Functions to update the state directly
const mutations = {
setBitcoinConfig(state, bitcoinConfig) {
state.bitcoinConfig = bitcoinConfig;
}
};
const actions = {
async getBitcoinConfig({ commit }) {
const existingConfig = await API.get(
`${process.env.VUE_APP_API_BASE_URL}/v1/bitcoind/system/bitcoin-config`
);
if (existingConfig) {
commit("setBitcoinConfig", existingConfig);
}
},
updateBitcoinConfig({ commit }, bitcoinConfig) {
commit("setBitcoinConfig", bitcoinConfig);
}
};
const getters = {};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};

View File

@ -15,9 +15,10 @@
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="4" cy="4" r="4" fill="#00CD98" />
<circle cx="4" cy="4" r="4" :fill="`${isBitcoinCoreOperational ? '#00CD98' : '#F6B900'}`" />
</svg>
<small class="ml-1 text-success">Running</small>
<small v-if="isBitcoinCoreOperational" class="ml-1 text-success">Running</small>
<small v-else class="ml-1 text-warning">Starting</small>
<h3 class="d-block font-weight-bold mb-1">Bitcoin Node</h3>
<span class="d-block text-muted">{{
version ? `Bitcoin Core ${version}` : "..."
@ -25,6 +26,7 @@
</div>
</div>
<div class="d-flex col-12 col-md-auto justify-content-start align-items-center p-0">
<!-- TODO - work on responsiveness of connect + settings button -->
<b-button
type="button"
variant="primary"
@ -34,8 +36,53 @@
<b-icon icon="plus" aria-hidden="true"></b-icon>
Connect
</b-button>
<b-dropdown
class="ml-3"
variant="link"
toggle-class="text-decoration-none p-0"
no-caret
right
>
<template v-slot:button-content>
<svg
width="18"
height="4"
viewBox="0 0 18 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2 4C3.10457 4 4 3.10457 4 2C4 0.89543 3.10457 0 2 0C0.89543 0 0 0.89543 0 2C0 3.10457 0.89543 4 2 4Z"
fill="#6c757d"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9 4C10.1046 4 11 3.10457 11 2C11 0.89543 10.1046 0 9 0C7.89543 0 7 0.89543 7 2C7 3.10457 7.89543 4 9 4Z"
fill="#6c757d"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16 4C17.1046 4 18 3.10457 18 2C18 0.89543 17.1046 0 16 0C14.8954 0 14 0.89543 14 2C14 3.10457 14.8954 4 16 4Z"
fill="#6c757d"
/>
</svg>
</template>
<b-dropdown-item href="#" v-b-modal.advanced-settings-modal><b-badge pill variant="primary" class="mr-1">New</b-badge> Advanced Settings</b-dropdown-item>
</b-dropdown>
</div>
</div>
<b-alert :show="showReindexCompleteAlert" variant="warning">Reindexing is now complete. Turn off "Reindex blockchain" in <span class="open-settings" @click="() => $bvModal.show('advanced-settings-modal')">advanced settings</span> to prevent reindexing every time Bitcoin Node restarts.</b-alert>
<b-alert :show="showReindexInProgressAlert" variant="info">Reindexing in progress...</b-alert>
<b-alert :show="showRestartError" variant="danger" dismissible @dismissed="showRestartError=false">
Something went wrong while attempting to change the configuration of Bitcoin Node.
</b-alert>
</div>
<b-row class="row-eq-height">
@ -84,7 +131,7 @@
</card-widget>
</b-col>
<b-col col cols="12" md="7" lg="8">
<card-widget class="overflow-x" header="Network">
<card-widget class="overflow-x" :header="networkWidgetHeader">
<div class>
<div class="px-3 px-lg-4">
<b-row>
@ -92,8 +139,11 @@
<stat
title="Connections"
:value="stats.peers"
suffix="Peers"
showNumericChange
:suffix="`${stats.peers === 1 ? 'Peer' : 'Peers'}`"
showPercentChange
:showPopover="true"
popoverId="connections-popover"
:popoverContent="[`Clearnet${torProxy ? ' (over Tor)': ''}: ${peers.clearnet}`, `Tor: ${peers.tor}`, `I2P: ${peers.i2p}`]"
></stat>
</b-col>
<b-col col cols="6" md="3">
@ -118,6 +168,9 @@
:value="abbreviateSize(stats.blockchainSize)[0]"
:suffix="abbreviateSize(stats.blockchainSize)[1]"
showPercentChange
:showPopover="pruned"
popoverId="blockchain-size-popover"
:popoverContent='[`Your "Prune Old Blocks" setting has set the max blockchain size to ${pruneTargetSizeGB}GB.`]'
></stat>
</b-col>
</b-row>
@ -127,10 +180,14 @@
</card-widget>
</b-col>
</b-row>
<b-modal id="connect-modal" size="lg" centered hide-footer>
<connection-modal></connection-modal>
</b-modal>
<b-modal id="advanced-settings-modal" size="lg" centered hide-footer scrollable>
<advanced-settings-modal :isSettingsDisabled="isRestartPending" @submit="saveSettingsAndRestartBitcoin" @clickRestoreDefaults="restoreDefaultSettingsAndRestartBitcoin"></advanced-settings-modal>
</b-modal>
</div>
</template>
@ -138,27 +195,52 @@
// import Vue from "vue";
import { mapState } from "vuex";
import API from "@/helpers/api";
import delay from "@/helpers/delay";
import CardWidget from "@/components/CardWidget";
import Blockchain from "@/components/Blockchain";
import Stat from "@/components/Utility/Stat";
import ConnectionModal from "@/components/ConnectionModal";
import AdvancedSettingsModal from "@/components/AdvancedSettingsModal";
import ChartWrapper from "@/components/ChartWrapper.vue";
export default {
data() {
return {};
return {
isRestartPending: false,
showRestartError: false
};
},
computed: {
...mapState({
isBitcoinCoreOperational: state => state.bitcoin.operational,
syncPercent: state => state.bitcoin.percent,
blocks: state => state.bitcoin.blocks,
version: state => state.bitcoin.version,
currentBlock: state => state.bitcoin.currentBlock,
blockHeight: state => state.bitcoin.blockHeight,
stats: state => state.bitcoin.stats,
peers: state => state.bitcoin.peers,
rpc: state => state.bitcoin.rpc,
p2p: state => state.bitcoin.p2p
})
p2p: state => state.bitcoin.p2p,
reindex: state => state.user.bitcoinConfig.reindex,
network: state => state.user.bitcoinConfig.network,
pruned: state => state.bitcoin.pruned,
pruneTargetSizeGB: state => state.bitcoin.pruneTargetSizeGB,
torProxy: state => state.user.bitcoinConfig.torProxyForClearnet
}),
showReindexInProgressAlert() {
return this.reindex && this.syncPercent !== 100 && !this.isRestartPending;
},
showReindexCompleteAlert() {
return this.reindex && this.syncPercent === 100 && !this.isRestartPending;
},
networkWidgetHeader() {
if (!this.network || this.network === "main") return "Network";
if (this.network === "test") return "Network (testnet)";
return `Network (${this.network})`;
}
},
methods: {
random(min, max) {
@ -185,33 +267,113 @@ export default {
fetchStats() {
this.$store.dispatch("bitcoin/getStats");
},
fetchPeers() {
this.$store.dispatch("bitcoin/getPeers");
},
fetchConnectionDetails() {
return Promise.all([
this.$store.dispatch("bitcoin/getP2PInfo"),
this.$store.dispatch("bitcoin/getRpcInfo")
]);
},
fetchBitcoinConfigSettings() {
this.$store.dispatch("user/getBitcoinConfig");
},
async saveSettingsAndRestartBitcoin(bitcoinConfig) {
try {
this.isRestartPending = true;
this.$store.dispatch("user/updateBitcoinConfig", bitcoinConfig);
const response = await API.post(
`${process.env.VUE_APP_API_BASE_URL}/v1/bitcoind/system/update-bitcoin-config`,
{ bitcoinConfig }
);
if (response.data.success) {
// reload the page to reset all state and show loading view while bitcoin core restarts.
this.$router.push({ query: { restart: "1" } });
window.location.reload();
} else {
this.fetchBitcoinConfigSettings();
this.showRestartError = true;
this.isRestartPending = false;
}
} catch (error) {
console.error(error);
this.fetchBitcoinConfigSettings();
this.showRestartError = true;
this.$bvModal.hide("advanced-settings-modal");
this.isRestartPending = false;
}
},
async restoreDefaultSettingsAndRestartBitcoin() {
try {
this.isRestartPending = true;
const response = await API.post(
`${process.env.VUE_APP_API_BASE_URL}/v1/bitcoind/system/restore-default-bitcoin-config`
);
// dispatch getBitcoinConfig after post request to avoid referencing default values in the store.
this.$store.dispatch("user/getBitcoinConfig");
if (response.data.success) {
// reload the page to reset all state and show loading view while bitcoin core restarts.
this.$router.push({ query: { restart: "1" } });
window.location.reload();
} else {
this.fetchBitcoinConfigSettings();
this.showRestartError = true;
this.isRestartPending = false;
}
} catch (error) {
console.error(error);
this.fetchBitcoinConfigSettings();
this.showRestartError = true;
this.$bvModal.hide("advanced-settings-modal");
this.isRestartPending = false;
}
}
},
created() {
async created() {
// fetch settings first because bitcoin core
// is not operational if pruning is in progress
this.fetchBitcoinConfigSettings();
// wait until bitcoin core is operational
while (true) { /* eslint-disable-line */
await this.$store.dispatch("bitcoin/getStatus");
if (this.isBitcoinCoreOperational) {
break;
}
await delay(1000);
}
this.$store.dispatch("bitcoin/getVersion");
this.fetchStats();
this.fetchPeers();
this.fetchConnectionDetails();
this.interval = window.setInterval(this.fetchStats, 5000);
this.interval = window.setInterval(() => {
this.fetchStats();
this.fetchPeers();
}, 5000);
},
beforeDestroy() {
window.clearInterval(this.interval);
if (this.interval) {
window.clearInterval(this.interval);
}
},
components: {
CardWidget,
Blockchain,
Stat,
ConnectionModal,
ChartWrapper,
AdvancedSettingsModal,
ChartWrapper
}
};
</script>
<style lang="scss" scoped>
<style lang="scss">
.app-icon {
height: 120px;
width: 120px;
@ -220,4 +382,23 @@ export default {
.overflow-x {
overflow-x: visible;
}
</style>
.dropdown-menu {
margin-top: 0.5rem;
padding: 4px 0;
border-radius: 4px;
}
.dropdown-item {
padding-top: 8px;
padding-bottom: 8px;
}
.open-settings {
text-decoration: underline;
}
.open-settings:hover {
cursor: pointer;
}
</style>

14297
ui/yarn.lock

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,22 @@ module.exports = {
REQUEST_CORRELATION_ID_KEY: 'reqId',
DEVICE_HOSTNAME: process.env.DEVICE_HOSTNAME || 'umbrel.local',
BITCOIN_P2P_HIDDEN_SERVICE: process.env.BITCOIN_P2P_HIDDEN_SERVICE,
BITCOIN_P2P_PORT: process.env.BITCOIN_P2P_PORT || 8333,
BITCOIN_RPC_HIDDEN_SERVICE: process.env.BITCOIN_RPC_HIDDEN_SERVICE,
BITCOIN_RPC_PORT: process.env.BITCOIN_RPC_PORT || 8332,
BITCOIN_RPC_USER: process.env.BITCOIN_RPC_USER || 'umbrel',
BITCOIN_RPC_PASSWORD: process.env.BITCOIN_RPC_PASSWORD || 'moneyprintergobrrr',
DEVICE_DOMAIN_NAME: process.env.DEVICE_DOMAIN_NAME
};
DEVICE_DOMAIN_NAME: process.env.DEVICE_DOMAIN_NAME,
JSON_STORE_FILE: process.env.JSON_STORE_FILE || "/data/bitcoin-config.json",
UMBREL_BITCOIN_CONF_FILEPATH: process.env.UMBREL_BITCOIN_CONF_FILE || "/bitcoin/.bitcoin/umbrel-bitcoin.conf",
BITCOIN_CONF_FILEPATH: process.env.BITCOIN_CONF_FILE || "/bitcoin/.bitcoin/bitcoin.conf",
BITCOIN_INITIALIZE_WITH_CLEARNET_OVER_TOR: process.env.BITCOIN_INITIALIZE_WITH_CLEARNET_OVER_TOR === 'true',
BITCOIN_P2P_PORT: process.env.BITCOIN_P2P_PORT || 8333,
BITCOIN_RPC_PORT: process.env.BITCOIN_RPC_PORT || 8332,
BITCOIN_DEFAULT_NETWORK: process.env.BITCOIN_DEFAULT_NETWORK || "mainnet",
BITCOIND_IP: process.env.BITCOIND_IP,
TOR_PROXY_IP: process.env.TOR_PROXY_IP,
TOR_PROXY_PORT: process.env.TOR_PROXY_PORT,
TOR_PROXY_CONTROL_PORT: process.env.TOR_PROXY_CONTROL_PORT,
TOR_PROXY_CONTROL_PASSWORD: process.env.TOR_PROXY_CONTROL_PASSWORD,
I2P_DAEMON_IP: process.env.I2P_DAEMON_IP,
I2P_DAEMON_PORT: process.env.I2P_DAEMON_PORT
};