Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashmate): import existing Core data #1915

Merged
merged 11 commits into from
Jul 9, 2024
2 changes: 2 additions & 0 deletions packages/dashmate/src/createDIContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import createIpAndPortsFormFactory from './listr/prompts/createIpAndPortsForm.js
import registerMasternodeWithCoreWalletFactory from './listr/tasks/setup/regular/registerMasternode/registerMasternodeWithCoreWallet.js';
import registerMasternodeWithDMTFactory from './listr/tasks/setup/regular/registerMasternode/registerMasternodeWithDMT.js';
import writeConfigTemplatesFactory from './templates/writeConfigTemplatesFactory.js';
import importCoreDataTaskFactory from './listr/tasks/setup/regular/importCoreDataTaskFactory.js';

/**
* @param {Object} [options]
Expand Down Expand Up @@ -298,6 +299,7 @@ export default async function createDIContainer(options = {}) {
.singleton(),
registerMasternodeWithDMT: asFunction(registerMasternodeWithDMTFactory)
.singleton(),
importCoreDataTask: asFunction(importCoreDataTaskFactory).singleton(),
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function configureNodeTaskFactory(createIpAndPortsForm) {
task.title = `Configure ${ctx.nodeType}`;

// Masternode Operator key
if (ctx.nodeType === NODE_TYPE_MASTERNODE) {
if (ctx.nodeType === NODE_TYPE_MASTERNODE && !ctx.config.get('core.masternode.operator.privateKey')) {
const masternodeOperatorPrivateKey = await task.prompt({
type: 'input',
header: ` Please enter your Masternode Operator BLS Private key.
Expand Down Expand Up @@ -71,12 +71,23 @@ export default function configureNodeTaskFactory(createIpAndPortsForm) {

let form;
if (ctx.initialIpForm) {
// Using for testing
form = ctx.initialIpForm;
} else {
let initialIp = ctx.nodeType === NODE_TYPE_MASTERNODE ? '' : undefined;
if (ctx.importedExternalIp) {
initialIp = ctx.importedExternalIp;
}

let initialCoreP2PPort = showEmptyPort ? '' : undefined;
if (ctx.importedP2pPort) {
initialCoreP2PPort = ctx.importedP2pPort;
}

form = await task.prompt(await createIpAndPortsForm(ctx.preset, {
isHPMN: ctx.isHP,
initialIp: ctx.nodeType === NODE_TYPE_MASTERNODE ? '' : undefined,
initialCoreP2PPort: showEmptyPort ? '' : undefined,
initialIp,
initialCoreP2PPort,
initialPlatformHTTPPort: showEmptyPort ? '' : undefined,
initialPlatformP2PPort: showEmptyPort ? '' : undefined,
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { Listr } from 'listr2';
import fs from 'fs';
import path from 'path';
import {
NETWORK_TESTNET,
} from '../../../../constants.js';

/**
* @param {Config} config
* @return {validateCoreDataDirectoryPath}
*/
function validateCoreDataDirectoryPathFactory(config) {
/**
* @typedef {function} validateCoreDataDirectoryPath
* @param {string} value
* @return {string|boolean}
*/
function validateCoreDataDirectoryPath(value) {
if (value.length === 0) {
return 'should not be empty';
}

// Path must be absolute
if (!path.isAbsolute(value)) {
return 'path must be absolute';
}

// Should contain data and dashd.conf
const configFilePath = path.join(value, 'dash.conf');

let dataDirName = 'blocks';
if (config.get('network') === NETWORK_TESTNET) {
dataDirName = 'testnet3';
}

try {
fs.accessSync(configFilePath, fs.constants.R_OK);
fs.accessSync(path.join(value, dataDirName), fs.constants.R_OK);
} catch (e) {
return 'directory must be readable and contain blockchain data with dash.conf';
}

const configFileContent = fs.readFileSync(configFilePath, 'utf8');

// Config file should contain testnet=1 in case of testnet
// and shouldn't contain testnet=1, regtest=1 or devnet= in case of mainnet
if (config.get('network') === NETWORK_TESTNET) {
if (!configFileContent.includes('testnet=1')) {
return 'dash.conf should be configured for testnet';
}
} else if (configFileContent.includes('testnet=1')
|| configFileContent.includes('regtest=1')
|| configFileContent.includes('devnet=')) {
return 'dash.conf should be configured for mainnet';
}

// Config file should contain masternodeblsprivkey in case of masternode
if (config.get('core.masternode.enable')) {
if (!configFileContent.includes('masternodeblsprivkey=')) {
return 'dash.conf should contain masternodeblsprivkey';
}
}

return true;
}

return validateCoreDataDirectoryPath;
}

/**
*
* @param {Docker} docker
* @param {dockerPull} dockerPull
* @param {generateEnvs} generateEnvs
* @return {importCoreDataTask}
*/
export default function importCoreDataTaskFactory(docker, dockerPull, generateEnvs) {
/**
* @typedef {function} importCoreDataTask
* @returns {Listr}
*/
async function importCoreDataTask() {
return new Listr([
{
title: 'Import existing Core data',
task: async (ctx, task) => {
const doImport = await task.prompt({
type: 'toggle',
header: ` If you run a masternode on the same machine, you can
import your existing data so you don't need to sync node from scratch.
You current user must have read access to this directory.\n`,
shumkov marked this conversation as resolved.
Show resolved Hide resolved
message: 'Import existing data?',
enabled: 'Yes',
disabled: 'No',
initial: true,
});
// TODO: Wording needs to be updated

if (!doImport) {
task.skip();
return;
}

// Masternode Operator key
const coreDataPath = await task.prompt({
type: 'input',
header: ` Please enter path to your existing Dash Core data directory.

- You current user must have read access to this directory.
- Data directory usually ends with .dashcore and contains dash.conf and data files (what files we expect)
- If dash.conf stored separately you should copy or link this file to data dire\n`,
shumkov marked this conversation as resolved.
Show resolved Hide resolved
message: 'Core data directory path',
validate: validateCoreDataDirectoryPathFactory(ctx.config),
});

// Read configuration from dashd.conf
const configPath = path.join(coreDataPath, 'dash.conf');
const configFileContent = fs.readFileSync(configPath, 'utf8');
const masternodeOperatorPrivateKey = configFileContent.match(/^masternodeblsprivkey=([^ \n]+)/m)[1];

if (masternodeOperatorPrivateKey) {
ctx.config.set('core.masternode.operator.privateKey', masternodeOperatorPrivateKey);
}

const host = configFileContent.match(/^bind=([^ \n]+)/m)[1];

if (host) {
ctx.config.set('core.p2p.host', host);
}

// Store values to fill in the configure node form

// eslint-disable-next-line prefer-destructuring
ctx.importedP2pPort = configFileContent.match(/^port=([^ \n]+)/m)[1];

// eslint-disable-next-line prefer-destructuring
ctx.importedExternalIp = configFileContent.match(/^externalip=([^ \n]+)/m)[1];

// Copy data directory to docker a volume

// Create a volume
const { COMPOSE_PROJECT_NAME: composeProjectName } = generateEnvs(ctx.config);

const volumeName = `${composeProjectName}_core_data`;

// Check if volume already exists
const volumes = await docker.listVolumes();
const exists = volumes.Volumes.some((volume) => volume.Name === volumeName);

if (exists) {
throw new Error(`Volume ${volumeName} already exists. Please remove it first.`);
}

await docker.createVolume(volumeName);

// Pull image
await dockerPull('alpine');

const commands = [
`mkdir /${volumeName}/.dashcore/`,
'cd /source',
`cp -avL * /${volumeName}/.dashcore/`,
`chown -R 1000:1000 /${volumeName}/`,
`rm /${volumeName}/.dashcore/dash.conf`,
];

// Copy data and set user dash
const [result] = await docker.run(
'alpine',
[
'/bin/sh',
'-c',
commands.join(' && '),
],
task.stdout(),
{
HostConfig: {
AutoRemove: true,
Binds: [
`${coreDataPath}:/source:ro`,
],
Mounts: [
{
Type: 'volume',
Source: volumeName,
Target: `/${volumeName}`,
},
],
},
},
);

if (result.StatusCode !== 0) {
throw new Error('Cannot copy data dir to volume');
}

// TODO: Wording needs to be updated
await task.prompt({
type: 'confirm',
header: ` You need to stop your existing node before your start a dashmate
node\n`,
shumkov marked this conversation as resolved.
Show resolved Hide resolved
message: 'Press any key to continue...',
default: ' ',
separator: () => '',
format: () => '',
initial: true,
isTrue: () => true,
});

// eslint-disable-next-line no-param-reassign
task.output = `${coreDataPath} imported`;
},
options: {
persistentOutput: true,
},
},
]);
}

return importCoreDataTask;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import generateRandomString from '../../../util/generateRandomString.js';
* @param {configureNodeTask} configureNodeTask
* @param {configureSSLCertificateTask} configureSSLCertificateTask
* @param {DefaultConfigs} defaultConfigs
* @param {importCoreDataTask} importCoreDataTask
*/
export default function setupRegularPresetTaskFactory(
configFile,
Expand All @@ -35,6 +36,7 @@ export default function setupRegularPresetTaskFactory(
configureNodeTask,
configureSSLCertificateTask,
defaultConfigs,
importCoreDataTask,
) {
/**
* @typedef {setupRegularPresetTask}
Expand Down Expand Up @@ -131,6 +133,10 @@ export default function setupRegularPresetTaskFactory(
enabled: (ctx) => !ctx.isMasternodeRegistered && ctx.nodeType === NODE_TYPE_MASTERNODE,
task: () => registerMasternodeGuideTask(),
},
{
enabled: (ctx) => ctx.isMasternodeRegistered,
task: () => importCoreDataTask(),
},
{
enabled: (ctx) => ctx.isMasternodeRegistered || ctx.nodeType === NODE_TYPE_FULLNODE,
task: () => configureNodeTask(),
Expand Down
Loading