#!/usr/bin/env node
const bodyParser = require('koa-bodyparser');
const chalk = require('chalk');
const http = require('http');
const Integrations = require('@sentry/integrations');
const Koa = require('koa');
const Router = require('koa-router');
const Sentry = require('@sentry/node');
const { program } = require('commander');
const { flatten } = require('lodash');
const { arch, platform, release } = require('os');
const eventBus = require('./lib/services/event-bus');
const logger = require('./lib/services/logger');
const config = require('./lib/services/config');
const Proxy = require('./lib/proxy');
const store = require('./lib/services/store');
const version = require('./lib/version');
const { getMiner } = require('./lib/miner');
const IdleProgram = require('./lib/idle-program');
const startupMessage = require('./lib/startup-message');
const profitabilityService = require('./lib/services/profitability-service');
const dashboard = require('./lib/services/cli-dashboard');
const foxyPoolGateway = require('./lib/services/foxy-pool-gateway');
const binaryManager = require('./lib/miner/binary-manager/binary-manager');
const configManager = require('./lib/miner/config-manager/config-manager');
const FirstRunWizard = require('./lib/first-run-wizard/first-run-wizard');
program
.version(version)
.option('--config ', 'The custom foxy miner config.yaml file path')
.option('--live', 'Show a live dashboard with stats')
.parse(process.argv);
if (program.opts().config) {
store.configFilePath = program.opts().config;
}
if (program.opts().live) {
store.useDashboard = true;
dashboard.init();
}
(async () => {
const result = await config.init();
if (result === null) {
const firstRunWizard = new FirstRunWizard(store.configFilePath);
await firstRunWizard.run();
process.exit(0);
}
if (config.logToFile) {
logger.enableFileLogging();
}
startupMessage();
eventBus.publish('log/info', `Config loaded from ${store.configFilePath} successfully`);
Sentry.init({
dsn: 'https://[email protected]/1462805',
release: `Foxy-Miner@${version}`,
integrations: [
new Integrations.Dedupe(),
new Integrations.ExtraErrorData(),
new Integrations.Transaction(),
],
ignoreErrors: [
/ENOSYS/
],
});
Sentry.configureScope((scope) => {
scope.setTag('os.arch', arch());
scope.setTag('os.platform', platform());
scope.setTag('os.release', release());
scope.setContext('Config', {
'Foxy-Miner': JSON.stringify(config.config, null, 2),
});
});
process.on('unhandledRejection', (err) => {
eventBus.publish('log/error', `Error: ${err.message}`);
});
process.on('uncaughtException', (err) => {
eventBus.publish('log/error', `Error: ${err.message}`);
});
const app = new Koa();
app.on('error', err => {
eventBus.publish('log/error', `Error: ${err.message}`);
});
const router = new Router();
app.use(bodyParser());
if (config.useProfitability) {
await profitabilityService.init(config.useEcoBlockRewardsForProfitability);
}
let minerConfigs;
if (config.miner) {
minerConfigs = config.miner
.map((minerConfig, index) => ({
...minerConfig,
index,
}))
.filter(minerConfig => !minerConfig.disabled);
} else {
minerConfigs = [{
upstreams: config.upstreams,
minerBinPath: config.minerBinPath,
minerConfigPath: config.minerConfigPath,
minerType: config.minerType,
minerOutputToConsole: config.minerOutputToConsole,
assumeScannedAfter: config.config.assumeScannedAfter,
isCpuOnly: config.config.isCpuOnly,
isManaged: config.config.isManaged,
}];
}
const singleProxy = minerConfigs.length === 1;
const managedMiners = minerConfigs
.map(minerConfig => ({
miner: getMiner({ minerType: minerConfig.minerType }),
minerConfig,
}))
.filter(({ miner, minerConfig }) => miner.supportsManagement && minerConfig.isManaged);
const groupedManagedMiners = managedMiners.reduce((acc, curr) => {
const identifier = `${curr.minerConfig.minerType}/${curr.minerConfig.isCpuOnly}`;
if (!acc[identifier]) {
acc[identifier] = curr;
}
return acc;
}, {});
await Promise.all(Object.values(groupedManagedMiners).map(async ({ minerConfig }) => {
try {
await binaryManager.ensureMinerDownloaded({ minerType: minerConfig.minerType, isCpuOnly: minerConfig.isCpuOnly });
} catch (err) {
eventBus.publish('log/error', `Failed to download the miner binary for type ${minerConfig.minerType} with error: ${err}`);
process.exit(0);
}
}));
const proxies = await Promise.all(minerConfigs.map(async (minerConfig) => {
const proxyIndex = (minerConfig.index || 0) + 1;
const miner = getMiner({ minerType: minerConfig.minerType });
if (miner.supportsManagement && minerConfig.isManaged) {
configManager.ensureMinerConfigExists({
minerType: minerConfig.minerType,
config: config.config,
minerIndex: !!config.miner ? minerConfig.index : null,
});
configManager.updateMinerConfig({
minerType: minerConfig.minerType,
config: config.config,
minerIndex: !!config.miner ? minerConfig.index : null,
});
const minerBinPath = binaryManager.getMinerBinaryPath({ minerType: minerConfig.minerType, isCpuOnly: minerConfig.isCpuOnly });
const minerConfigPath = configManager.getMinerConfigPath({
minerType: minerConfig.minerType,
minerIndex: !!config.miner ? minerConfig.index : null,
});
minerConfig.minerBinPath = minerBinPath;
minerConfig.minerConfigPath = minerConfigPath;
}
const minerInstance = new miner.Miner(
minerConfig.minerBinPath,
minerConfig.minerConfigPath,
minerConfig.minerOutputToConsole
);
const enabledUpstreams = minerConfig.upstreams.filter(upstreamConfig => !upstreamConfig.disabled);
const proxy = new Proxy({
upstreamConfigs: enabledUpstreams,
proxyIndex,
showProxyIndex: !singleProxy,
miner: minerInstance,
minerConfig: minerConfig,
});
minerInstance.proxy = proxy;
const endpoints = [`/${proxyIndex}/burst`];
if (singleProxy) {
endpoints.unshift('/burst');
}
for (let endpoint of endpoints) {
router.get(endpoint, (ctx) => {
const requestType = ctx.query.requestType;
switch (requestType) {
case 'getMiningInfo':
ctx.body = proxy.getMiningInfo();
break;
default:
eventBus.publish('log/error', `Unknown requestType ${requestType} with data: ${JSON.stringify(ctx.params)}. Please message this info to the creator of this software.`);
ctx.status = 400;
ctx.body = {
error: {
message: 'unknown request type',
code: 4,
},
};
}
});
router.post(endpoint, async (ctx) => {
const requestType = ctx.query.requestType;
switch (requestType) {
case 'getMiningInfo':
ctx.body = proxy.getMiningInfo();
break;
case 'submitNonce':
const options = {
ip: ctx.request.ip,
maxScanTime: ctx.params.maxScanTime,
minerName: ctx.req.headers['x-minername'] || ctx.req.headers['x-miner'],
userAgent: ctx.req.headers['user-agent'],
miner: ctx.req.headers['x-miner'],
capacity: parseInt(ctx.req.headers['x-capacity']),
accountKey: ctx.req.headers['x-account'],
accountName: ctx.req.headers['x-accountname'] || ctx.req.headers['x-mineralias'] || null,
color: ctx.req.headers['x-color'] || null,
};
const submissionObj = {
accountId: ctx.query.accountId,
blockheight: ctx.query.blockheight,
nonce: ctx.query.nonce,
deadline: ctx.query.deadline,
secretPhrase: ctx.query.secretPhrase !== '' ? ctx.query.secretPhrase : null,
};
ctx.body = await proxy.submitNonce(submissionObj, options);
if (ctx.body.error) {
ctx.status = 400;
}
break;
default:
eventBus.publish('log/error', `Unknown requestType ${requestType} with data: ${JSON.stringify(ctx.params)}. Please message this info to the creator of this software.`);
ctx.status = 400;
ctx.body = {
error: {
message: 'unknown request type',
code: 4,
},
};
}
});
}
return {
miner: minerInstance,
proxy,
};
}));
if (store.useDashboard) {
dashboard.proxies = proxies.map(({proxy}) => proxy);
dashboard.start();
}
const coins = [...new Set(flatten(proxies.map(({proxy}) =>
proxy.upstreamConfigs
.filter(upstreamConfig => upstreamConfig.type === 'foxypool' && upstreamConfig.coin && !upstreamConfig.url)
.map(upstreamConfig => upstreamConfig.coin.toUpperCase())
)))];
if (coins.length > 0) {
foxyPoolGateway.coins = coins;
await foxyPoolGateway.init();
}
await Promise.all(proxies.map(({proxy}) => proxy.init()));
app.use(router.routes());
app.use(router.allowedMethods());
const server = http.createServer(app.callback());
server.on('error', (err) => {
eventBus.publish('log/error', `Error: ${err.message}`);
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
process.exit(1);
}
});
server.listen(config.listenPort, config.listenHost);
const startupLine = `Foxy-Miner ${version} initialized`;
eventBus.publish('log/info', store.getUseColors() ? chalk.green(startupLine) : startupLine);
proxies.map(({ proxy }) => {
const listenLine = `Accepting connections on http://${config.listenAddr}${singleProxy ? '' : '/' + (proxy.proxyIndex)}`;
eventBus.publish('log/info', store.getUseColors() ? chalk.blueBright(listenLine) : listenLine);
});
await Promise.all(proxies.map(({miner}) => miner.start()));
if (config.config.runIdleBinPath && singleProxy) {
const idleProgram = new IdleProgram(config.config.runIdleBinPath, config.config.runIdleKillBinPath);
proxies[0].miner.subscribe('new-round', () => idleProgram.stop());
proxies[0].miner.subscribe('all-rounds-finished', () => idleProgram.start());
}
})();