foundation-server
Version:
An extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool
1,051 lines (921 loc) • 40.1 kB
JavaScript
/*
*
* Payments (Updated)
*
*/
const fs = require('fs');
const async = require('async');
const utils = require('./utils');
const Stratum = require('foundation-stratum');
////////////////////////////////////////////////////////////////////////////////
// Main Payments Function
const PoolPayments = function (logger, client) {
const _this = this;
process.setMaxListeners(0);
this.pools = [];
this.client = client;
this.poolConfigs = JSON.parse(process.env.poolConfigs);
this.portalConfig = JSON.parse(process.env.portalConfig);
this.forkId = process.env.forkId;
// Check for Deletable Shares
this.checkShares = function(rounds, round) {
let shareFlag = true;
rounds.forEach((cRound) => {
if ((cRound.height === round.height) &&
(cRound.category !== 'kicked') &&
(cRound.category !== 'orphan') &&
(cRound.serialized !== round.serialized)) {
shareFlag = false;
}
});
return shareFlag;
};
// Check Address to Ensure Viability
this.checkAddress = function(daemon, address, command, callback) {
daemon.cmd(command, [address], true, (result) => {
if (result.error) {
callback(true, JSON.stringify(result.error));
} else if (!result.response || !result.response.ismine) {
callback(true, 'The daemon does not own the pool address listed');
} else {
callback(null);
}
});
};
// Ensure Payment Address is Valid for Daemon
this.handleAddress = function(daemon, address, pool, callback) {
_this.checkAddress(daemon, address, 'validateaddress', (error,) => {
if (error) {
_this.checkAddress(daemon, address, 'getaddressinfo', (error, results) => {
if (error) {
logger.error('Payments', pool, `Error with payment processing daemon: ${ results }`);
callback(true, []);
} else {
callback(null, []);
}
});
} else {
callback(null, []);
}
});
};
// Calculate Current Balance in Daemon
this.handleBalance = function(daemon, config, pool, blockType, callback) {
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
daemon.cmd('getbalance', [], true, (result) => {
if (result.error) {
logger.error('Payments', pool, `Error with payment processing daemon: ${ JSON.stringify(result.error) }`);
callback(true, []);
return;
}
try {
const data = result.data.split('result":')[1].split(',')[0].split('.')[1];
const magnitude = parseInt(`10${ new Array(data.length).join('0') }`);
const minSatoshis = parseInt(processingConfig.payments.minPayment * magnitude);
const coinPrecision = magnitude.toString().length - 1;
callback(null, [magnitude, minSatoshis, coinPrecision]);
} catch(e) {
logger.error('Payments', pool, `Error detecting number of satoshis in a coin. Tried parsing: ${ result.data }`);
callback(true, []);
}
});
};
// Calculate Unspent Balance in Daemon
this.handleUnspent = function(daemon, config, category, pool, blockType, callback) {
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
const args = [processingConfig.payments.minConfirmations, 99999999];
daemon.cmd('listunspent', args, true, (result) => {
if (!result || result.error) {
logger.error('Payments', pool, `Error with payment processing daemon: ${ JSON.stringify(result.error) }`);
callback(true, []);
} else {
let balance = parseFloat(0);
if (result.response != null && result.response.length > 0) {
result.response.forEach((instance) => {
if (instance.address && instance.address !== null) {
balance += parseFloat(instance.amount || 0);
}
});
balance = utils.coinsRound(balance, processingConfig.payments.coinPrecision);
}
if (category === 'start') {
logger.special('Payments', pool, `${ processingConfig.coin.name } wallet has a balance of ${ balance } ${ processingConfig.coin.symbol }`);
}
callback(null, [utils.coinsToSatoshis(balance, processingConfig.payments.magnitude)]);
}
});
};
// Handle Shares of Orphan Blocks
this.handleOrphans = function(round, pool, blockType, callback) {
const commands = [];
const dateNow = Date.now();
if (typeof round.orphanShares !== 'undefined') {
logger.warning('Payments', pool, `Moving shares from orphaned block ${ round.height } to current round.`);
// Move Orphaned Shares to Following Round
Object.keys(round.orphanShares).forEach((address) => {
const outputShare = {
time: dateNow,
effort: 0,
identifier: null,
round: 'orphan',
solo: false,
times: round.orphanTimes[address] || 0,
types: { valid: 1, invalid: 0, stale: 0 },
work: round.orphanShares[address],
worker: address,
};
commands.push(['hincrby', `${ pool }:rounds:${ blockType }:current:shared:counts`, 'valid', 1]);
commands.push(['hset', `${ pool }:rounds:${ blockType }:current:shared:shares`, address, JSON.stringify(outputShare)]);
});
}
// Return Commands as Callback
callback(null, commands);
};
// Handle Duplicate Blocks/Rounds
/* istanbul ignore next */
this.handleDuplicates = function(daemon, rounds, pool, blockType, callback) {
const validBlocks = {};
const invalidBlocks = [];
const duplicates = rounds.filter((round) => round.duplicate);
const commands = duplicates.map((round) => ['getblock', [round.hash]]);
rounds = rounds.filter((round) => !round.duplicate);
// Query Daemon Regarding Duplicate Blocks
daemon.batchCmd(commands, (error, blocks) => {
if (error || !blocks) {
logger.error('Payments', pool, `Could not get blocks from daemon: ${ JSON.stringify(error) }`);
callback(true, []);
return;
}
// Build Duplicate Updates
blocks.forEach((block, idx) => {
if (block && block.result) {
if (block.result.confirmations < 0) {
invalidBlocks.push(['smove', `${ pool }:blocks:${ blockType }:pending`, `${ pool }:blocks:${ blockType }:duplicate`, duplicates[idx].serialized]);
} else if (Object.prototype.hasOwnProperty.call(validBlocks, duplicates[idx].hash)) {
invalidBlocks.push(['smove', `${ pool }:blocks:${ blockType }:pending`, `${ pool }:blocks:${ blockType }:duplicate`, duplicates[idx].serialized]);
} else {
validBlocks[duplicates[idx].hash] = duplicates[idx].serialized;
}
}
});
// Update Redis Database w/ Duplicates
if (invalidBlocks.length > 0) {
_this.client.multi(invalidBlocks).exec((error,) => {
if (error) {
logger.error('Payments', pool, `Error could not move invalid duplicate blocks ${ JSON.stringify(error) }`);
callback(true, []);
return;
}
callback(null, [rounds]);
});
} else {
callback(null, [rounds]);
}
});
};
// Handle Workers for Immature Blocks
this.handleImmature = function(config, round, workers, times, maxTime, solo, shared, blockType, callback) {
let totalShares = parseFloat(0);
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
const feeSatoshi = utils.coinsToSatoshis(processingConfig.payments.processingFee, processingConfig.payments.magnitude);
const immature = Math.round(utils.coinsToSatoshis(round.reward, processingConfig.payments.magnitude)) - feeSatoshi;
// Handle Solo Rounds
if (round.solo) {
const worker = workers[round.worker] || {};
worker.shares = worker.shares || {};
const shares = parseFloat(solo[round.worker] || 1);
const total = Math.round(immature);
worker.shares.round = shares;
worker.immature = (worker.immature || 0) + total;
workers[round.worker] = worker;
// Handle Shared Rounds
} else {
// Handle PPLNT Share Reduction
Object.keys(shared).forEach((address) => {
let shares = parseFloat(shared[address]);
const worker = workers[address] || {};
worker.shares = worker.shares || {};
if (times[address] != null && parseFloat(times[address]) > 0) {
const timePeriod = utils.roundTo((parseFloat(times[address]) / maxTime), 2);
if (timePeriod > 0 && timePeriod < 0.51) {
const lost = shares * (1 - timePeriod);
shares = utils.roundTo(Math.max(shares - lost, 0), 2);
}
}
totalShares += shares;
worker.shares.round = shares;
workers[address] = worker;
});
// Calculate Final Block Rewards
Object.keys(shared).forEach((address) => {
const worker = workers[address];
const percent = parseFloat(worker.shares.round) / totalShares;
const total = Math.round(immature * percent);
worker.immature = (worker.immature || 0) + total;
workers[address] = worker;
});
}
// Return Updated Workers as Callback
callback(null, [workers]);
};
// Handle Workers for Generate Blocks
this.handleGenerate = function(config, round, workers, times, maxTime, solo, shared, blockType, callback) {
let totalShares = parseFloat(0);
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
const feeSatoshi = utils.coinsToSatoshis(processingConfig.payments.processingFee, processingConfig.payments.magnitude);
const generate = Math.round(utils.coinsToSatoshis(round.reward, processingConfig.payments.magnitude)) - feeSatoshi;
// Handle Solo Rounds
if (round.solo) {
const worker = workers[round.worker] || {};
worker.shares = worker.shares || {};
const shares = parseFloat(solo[round.worker] || 1);
const total = Math.round(generate);
worker.shares.round = shares;
worker.shares.total = parseFloat(worker.shares.total || 0) + shares;
worker.generate = (worker.generate || 0) + total;
workers[round.worker] = worker;
// Handle Shared Rounds
} else {
// Handle PPLNT Share Reduction
Object.keys(shared).forEach((address) => {
let shares = parseFloat(shared[address]);
const worker = workers[address] || {};
worker.shares = worker.shares || {};
if (times[address] != null && parseFloat(times[address]) > 0) {
const timePeriod = utils.roundTo((parseFloat(times[address]) / maxTime), 2);
if (timePeriod > 0 && timePeriod < 0.51) {
const lost = shares * (1 - timePeriod);
shares = utils.roundTo(Math.max(shares - lost, 0), 2);
}
}
totalShares += shares;
worker.shares.round = shares;
worker.shares.total = parseFloat(worker.shares.total || 0) + shares;
workers[address] = worker;
});
// Calculate Final Block Rewards
Object.keys(shared).forEach((address) => {
const worker = workers[address];
const percent = parseFloat(worker.shares.round) / totalShares;
const total = Math.round(generate * percent);
worker.generate = (worker.generate || 0) + total;
workers[address] = worker;
});
}
// Return Updated Workers as Callback
callback(null, [workers]);
};
// Check Blocks for Duplicates/Issues
/* istanbul ignore next */
this.handleBlocks = function(daemon, config, blockType, callback) {
// Load Blocks from Database
const pool = config.name;
const commands = [
['smembers', `${ pool }:blocks:${ blockType }:pending`],
['smembers', `${ pool }:blocks:${ blockType }:confirmed`]];
_this.client.multi(commands).exec((error, results) => {
if (error) {
logger.error('Payments', pool, `Could not get blocks from database: ${ JSON.stringify(error) }`);
callback(true, []);
return;
}
// Manage Individual Rounds
let rounds = results[0].map((r) => {
const details = JSON.parse(r);
return {
time: details.time,
height: details.height,
hash: details.hash,
reward: details.reward,
transaction: details.transaction,
difficulty: details.difficulty,
worker: details.worker ? details.worker.split('.')[0] : '',
solo: details.solo,
duplicate: false,
serialized: r
};
});
// Check for Block Duplicates
let duplicateFound = false;
rounds = rounds.sort((a, b) => a.height - b.height);
const roundHeights = rounds.flatMap(round => round.height);
rounds.forEach((round) => {
if (utils.countOccurences(roundHeights, round.height) > 1) {
round.duplicate = true;
duplicateFound = true;
}
});
// Handle Duplicate Blocks
if (duplicateFound) {
_this.handleDuplicates(daemon, rounds, pool, blockType, callback);
} else {
callback(null, [rounds]);
}
});
};
// Check Workers for Unpaid Balances
/* istanbul ignore next */
this.handleWorkers = function(config, blockType, data, callback) {
// Load Unpaid Workers from Database
const pool = config.name;
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
const commands = [['hgetall', `${ pool }:payments:${ blockType }:balances`]];
_this.client.multi(commands).exec((error, results) => {
if (error) {
logger.error('Payments', pool, `Could not get workers from database: ${ JSON.stringify(error) }`);
callback(true, []);
}
// Manage Individual Workers
const workers = {};
const magnitude = processingConfig.payments.magnitude;
Object.keys(results[0] || {}).forEach((worker) => {
workers[worker] = {
balance: utils.coinsToSatoshis(parseFloat(results[0][worker]), magnitude)
};
});
// Return Workers as Callback
callback(null, [data[0], workers]);
});
};
// Validate Transaction Hashes
/* istanbul ignore next */
this.handleTransactions = function(daemon, config, blockType, data, callback) {
// Get Hashes for Each Transaction
let rounds = data[0];
const pool = config.name;
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
const commands = rounds.map((round) => ['gettransaction', [round.transaction]]);
// Query Daemon Regarding Transactions
daemon.batchCmd(commands, (error, transactions) => {
if (error || !transactions) {
logger.error('Payments', pool, `Could not get transactions from daemon: ${ JSON.stringify(error) }`);
callback(true, []);
return;
}
// Handle Individual Transactions
transactions.forEach((tx, idx) => {
// Check Daemon Edge Cases
const round = rounds[idx];
if (tx.error && tx.error.code === -5) {
logger.warning('Payments', pool, `Daemon reports invalid transaction: ${ round.transaction }`);
round.category = 'kicked';
return;
} else if (tx.error || !tx.result) {
logger.error('Payments', pool, `Unable to load transaction: ${ round.transaction } ${ JSON.stringify(tx)}`);
return;
} else if (!tx.result.details || (tx.result.details && tx.result.details.length === 0)) {
logger.warning('Payments', pool, `Daemon reports no details for transaction: ${ round.transaction }`);
round.category = 'kicked';
return;
}
// Filter Transactions by Address
const transactions = tx.result.details.filter((tx) => {
let txAddress = tx.address;
if (txAddress.indexOf(':') > -1) {
txAddress = txAddress.split(':')[1];
}
if (blockType === 'primary') {
return txAddress === config.primary.address;
}
});
// Find Generation Transaction
let generationTx = null;
if (transactions.length >= 1) {
generationTx = transactions[0];
} else if (tx.result.details.length > 1){
const sorted = tx.result.details.sort((a, b) => a.vout - b.vout);
generationTx = sorted[0];
} else if (tx.result.details.length === 1) {
generationTx = tx.result.details[0];
}
// Update Round Details
round.category = generationTx.category;
round.confirmations = parseInt(tx.result.confirmations);
if ((round.category === 'generate') || (round.category === 'immature')) {
const reward = parseFloat(generationTx.amount || generationTx.value);
round.reward = utils.coinsRound(reward, processingConfig.payments.coinPrecision);
return;
}
});
// Manage Immature Rounds
rounds = rounds.filter((round) => {
switch (round.category) {
case 'orphan':
case 'kicked':
round.delete = _this.checkShares(rounds, round);
return true;
case 'immature':
case 'generate':
return true;
}
});
// Return Rounds as Callback
callback(null, [rounds, data[1]]);
});
};
// Calculate Shares from Round Data
/* istanbul ignore next */
this.handleShares = function(config, blockType, data, callback) {
const times = [];
const solo = [];
const shared = [];
const pool = config.name;
// Map Commands from Individual Rounds
const commands = data[0].map((round) => {
return ['hgetall', `${ pool }:rounds:${ blockType }:round-${ round.height }:shares`];
});
// Build Commands from Rounds
_this.client.multi(commands).exec((error, results) => {
if (error) {
logger.error('Payments', pool, `Could not load shares data from database: ${ JSON.stringify(error) }`);
callback(true, []);
return;
}
// Build Worker Shares Data w/ Results
results.forEach((round) => {
const timesRound = {};
const soloRound = {};
const sharedRound = {};
// Iterate Through Each Round
Object.keys(round || {}).forEach((entry) => {
// Calculate Round Values
const details = JSON.parse(round[entry]);
const address = entry.split('.')[0];
const timesValue = /^-?\d*(\.\d+)?$/.test(details.times) ? parseFloat(details.times) : 0;
const workValue = /^-?\d*(\.\d+)?$/.test(details.work) ? parseFloat(details.work) : 0;
// Process Round Times Data
if (address in timesRound) {
if (timesValue >= timesRound[address]) {
timesRound[address] = timesValue;
}
} else {
timesRound[address] = timesValue;
}
// Process Round Share Data
if (details.solo) {
if (address in soloRound) {
soloRound[address] += parseFloat(workValue);
} else {
soloRound[address] = parseFloat(workValue);
}
} else {
if (address in sharedRound) {
sharedRound[address] += parseFloat(workValue);
} else {
sharedRound[address] = parseFloat(workValue);
}
}
});
// Push Round Data to Main
times.push(timesRound);
solo.push(soloRound);
shared.push(sharedRound);
});
// Return Share Data as Callback
callback(null, [data[0], data[1], times, solo, shared]);
});
};
// Calculate Amount Owed to Workers
this.handleOwed = function(daemon, config, category, blockType, data, callback) {
let totalOwed = parseInt(0);
const rounds = data[0];
const pool = config.name;
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
const feeSatoshi = utils.coinsToSatoshis(processingConfig.payments.processingFee, processingConfig.payments.magnitude);
// Add to Total Owed from Rounds
rounds.forEach((round) => {
if (round.category === 'generate') {
totalOwed += utils.coinsToSatoshis(round.reward, processingConfig.payments.magnitude) - feeSatoshi;
}
});
// Add to Total Owed from Unpaid
Object.keys(data[1]).forEach((worker) => {
totalOwed += worker.balance || 0;
});
// Check Unspent Balance
_this.handleUnspent(daemon, config, category, pool, blockType, (error, balance) => {
if (error) {
logger.error('Payments', pool, 'Error checking pool balance before processing payments.');
callback(true, []);
return;
}
// Check Balance for Payments
if ((balance[0] < totalOwed) && (category === 'payments')) {
const currentBalance = utils.satoshisToCoins(balance[0], processingConfig.payments.magnitude, processingConfig.payments.coinPrecision);
const owedBalance = utils.satoshisToCoins(totalOwed, processingConfig.payments.magnitude, processingConfig.payments.coinPrecision);
logger.warning('Payments', pool, `Insufficient funds (${ currentBalance }) to process payments (${ owedBalance }), possibly waiting for transactions.`);
}
// Return Payment Data as Callback
callback(null, [rounds, data[1], data[2], data[3], data[4]]);
});
};
// Calculate Scores Given Times/Shares
this.handleRewards = function(config, category, blockType, data, callback) {
let workers = data[1];
const rounds = data[0];
const pool = config.name;
// Manage Shares in each Round
rounds.forEach((round, i) => {
let maxTime = 0;
const times = data[2][i];
const solo = data[3][i];
const shared = data[4][i];
// Check if Shares Exist in Round
if (Object.keys(solo).length <= 0 && Object.keys(shared).length <= 0) {
if (category === 'payments') {
_this.client.smove(`${ pool }:blocks:${ blockType }:pending`, `${ pool }:blocks:${ blockType }:manual`, round.serialized);
logger.error('Payments', pool, `No worker shares for round: ${ round.height }, hash: ${ round.hash }. Manual payout required.`);
return;
}
}
// Find Max Time in ALL Shares
const workerTimes = {};
Object.keys(times).forEach((address) => {
const workerTime = parseFloat(times[address]);
if (maxTime < workerTime) {
maxTime = workerTime;
}
workerTimes[address] = workerTime;
});
// Manage Block Generated
switch (round.category) {
case 'orphan':
case 'kicked':
round.orphanShares = shared;
round.orphanTimes = times;
break;
case 'immature':
_this.handleImmature(config, round, workers, times, maxTime, solo, shared, blockType, (error, results) => {
workers = results[0];
});
break;
case 'generate':
_this.handleGenerate(config, round, workers, times, maxTime, solo, shared, blockType, (error, results) => {
workers = results[0];
});
break;
}
});
// Return Updated Rounds/Workers as Callback
callback(null, [rounds, workers]);
};
// Send Payments if Applicable
this.handleSending = function(daemon, config, blockType, data, callback) {
let totalSent = 0;
const amounts = {};
const commands = [];
const dateNow = Date.now();
const rounds = data[0];
const workers = data[1];
const pool = config.name;
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
// Calculate Amount to Send to Workers
Object.keys(workers).forEach((address) => {
const worker = workers[address];
const amount = Math.round((worker.balance || 0) + (worker.generate || 0));
// Determine Amounts Given Mininum Payment
if (amount >= processingConfig.payments.minPaymentSatoshis) {
worker.sent = utils.satoshisToCoins(amount, processingConfig.payments.magnitude, processingConfig.payments.coinPrecision);
amounts[address] = utils.coinsRound(worker.sent, processingConfig.payments.coinPrecision);
totalSent += worker.sent;
} else {
worker.sent = 0;
worker.change = amount;
}
workers[address] = worker;
});
// Check if No Workers/Rounds
if (Object.keys(amounts).length === 0) {
callback(null, [rounds, workers]);
return;
}
// Send Payments to Workers Through Daemon
const rpcTracking = `sendmany "" ${ JSON.stringify(amounts) }`;
daemon.cmd('sendmany', ['', amounts], true, (result) => {
// Check Error Edge Cases
if (result.error && result.error.code === -5) {
logger.warning('Payments', pool, rpcTracking);
logger.error('Payments', pool, `Error sending payments ${ JSON.stringify(result.error)}`);
callback(true, []);
return;
} else if (result.error && result.error.code === -6) {
logger.warning('Payments', pool, rpcTracking);
logger.error('Payments', pool, `Insufficient funds for payments: ${ JSON.stringify(result.error)}`);
callback(true, []);
return;
} else if (result.error && result.error.message != null) {
logger.warning('Payments', pool, rpcTracking);
logger.error('Payments', pool, `Error sending payments ${ JSON.stringify(result.error)}`);
callback(true, []);
return;
} else if (result.error) {
logger.warning('Payments', pool, rpcTracking);
logger.error('Payments', pool, `Error sending payments ${ JSON.stringify(result.error)}`);
callback(true, []);
return;
}
// Handle Returned Transaction ID
if (result.response) {
const transaction = result.response;
const currentDate = Date.now();
const payments = {
time: currentDate,
paid: totalSent,
miners: Object.keys(amounts).length,
transaction: transaction,
};
// Update Redis Database with Payment Record
logger.special('Payments', pool, `Sent ${ totalSent } ${ processingConfig.coin.symbol } to ${ Object.keys(amounts).length } workers, txid: ${ transaction }`);
commands.push(['zadd', `${ pool }:payments:${ blockType }:records`, dateNow / 1000 | 0, JSON.stringify(payments)]);
callback(null, [rounds, workers, commands]);
return;
// Invalid/No Transaction ID
} else {
logger.error('Payments', pool, 'RPC command did not return txid. Disabling payments to prevent possible double-payouts');
callback(true, []);
return;
}
});
};
// Structure and Apply Redis Updates
/* istanbul ignore next */
this.handleUpdates = function(config, category, blockType, interval, data, callback) {
let totalPaid = 0;
let commands = data[2] || [];
const rounds = data[0];
const workers = data[1];
const pool = config.name;
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
// Update Worker Payouts/Balances
Object.keys(workers).forEach((address) => {
const worker = workers[address];
// Manage Worker Commands [1]
if (category === 'payments') {
if (worker.sent > 0) {
const sent = utils.coinsRound(worker.sent, processingConfig.payments.coinPrecision);
totalPaid = utils.coinsRound(totalPaid + worker.sent, processingConfig.payments.coinPrecision);
commands.push(['hincrbyfloat', `${ pool }:payments:${ blockType }:paid`, address, sent]);
commands.push(['hset', `${ pool }:payments:${ blockType }:balances`, address, 0]);
commands.push(['hset', `${ pool }:payments:${ blockType }:generate`, address, 0]);
} else if (worker.change > 0) {
worker.change = utils.satoshisToCoins(worker.change, processingConfig.payments.magnitude, processingConfig.payments.coinPrecision);
const change = utils.coinsRound(worker.change, processingConfig.payments.coinPrecision);
commands.push(['hset', `${ pool }:payments:${ blockType }:balances`, address, change]);
commands.push(['hset', `${ pool }:payments:${ blockType }:generate`, address, 0]);
}
} else {
if (worker.generate > 0) {
worker.generate = utils.satoshisToCoins(worker.generate, processingConfig.payments.magnitude, processingConfig.payments.coinPrecision);
const generate = utils.coinsRound(worker.generate, processingConfig.payments.coinPrecision);
commands.push(['hset', `${ pool }:payments:${ blockType }:generate`, address, generate]);
} else {
commands.push(['hset', `${ pool }:payments:${ blockType }:generate`, address, 0]);
}
}
// Manage Worker Commands [2]
if (worker.immature > 0) {
worker.immature = utils.satoshisToCoins(worker.immature, processingConfig.payments.magnitude, processingConfig.payments.coinPrecision);
const immature = utils.coinsRound(worker.immature, processingConfig.payments.coinPrecision);
commands.push(['hset', `${ pool }:payments:${ blockType }:immature`, address, immature]);
} else {
commands.push(['hset', `${ pool }:payments:${ blockType }:immature`, address, 0]);
}
});
// Update Worker Shares
const deleteCurrent = function(round, pool, blockType) {
return [
['del', `${ pool }:rounds:${ blockType }:round-${ round.height }:counts`],
['del', `${ pool }:rounds:${ blockType }:round-${ round.height }:shares`]];
};
// Update Round Shares/Times
rounds.forEach((round) => {
switch (round.category) {
case 'kicked':
case 'orphan':
if (category === 'payments') {
commands.push(['smove', `${ pool }:blocks:${ blockType }:pending`, `${ pool }:blocks:${ blockType }:kicked`, round.serialized]);
if (round.delete) {
_this.handleOrphans(round, pool, blockType, (error, results) => {
commands = commands.concat(results);
commands = commands.concat(deleteCurrent(round, pool, blockType));
});
}
}
break;
case 'immature':
break;
case 'generate':
if (category === 'payments') {
commands.push(['smove', `${ pool }:blocks:${ blockType }:pending`, `${ pool }:blocks:${ blockType }:confirmed`, round.serialized]);
commands = commands.concat(deleteCurrent(round, pool, blockType));
}
break;
}
});
// Update Miscellaneous Statistics
if ((category === 'start') || (category === 'payments')) {
const nextInterval = interval + (processingConfig.payments.paymentInterval * 1000);
commands.push(['hincrbyfloat', `${ pool }:payments:${ blockType }:counts`, 'total', totalPaid]);
commands.push(['hset', `${ pool }:payments:${ blockType }:counts`, 'last', interval]);
commands.push(['hset', `${ pool }:payments:${ blockType }:counts`, 'next', nextInterval]);
}
// Manage Redis Commands
_this.client.multi(commands).exec((error,) => {
if (error) {
logger.error('Payments', pool, `Payments sent but could not update redis: ${
JSON.stringify(error) }. Disabling payment processing to prevent double-payouts. The commands in ${
pool.toLowerCase() }_commands.txt must be ran manually`);
fs.writeFile(`${ pool.toLowerCase() }_commands.txt`, JSON.stringify(commands), () => {
logger.error('Could not write output commands.txt, stop the program immediately.');
});
callback(true, []);
return;
}
callback(null, commands);
});
};
// Process Main Payment Checks
/* istanbul ignore next */
this.processChecks = function(daemon, config, category, blockType, interval, callbackMain) {
// Process Checks Incrementally
async.waterfall([
(callback) => _this.handleBlocks(daemon, config, blockType, callback),
(data, callback) => _this.handleWorkers(config, blockType, data, callback),
(data, callback) => _this.handleTransactions(daemon, config, blockType, data, callback),
(data, callback) => _this.handleShares(config, blockType, data, callback),
(data, callback) => _this.handleRewards(config, category, blockType, data, callback),
(data, callback) => _this.handleUpdates(config, category, blockType, interval, data, callback),
], (error) => {
if (error) {
callbackMain(null, false);
return;
}
callbackMain(null, true);
});
};
// Process Main Payment Functionality
/* istanbul ignore next */
this.processPayments = function(daemon, config, category, blockType, interval, callbackMain) {
// Process Payments Incrementally
async.waterfall([
(callback) => _this.handleBlocks(daemon, config, blockType, callback),
(data, callback) => _this.handleWorkers(config, blockType, data, callback),
(data, callback) => _this.handleTransactions(daemon, config, blockType, data, callback),
(data, callback) => _this.handleShares(config, blockType, data, callback),
(data, callback) => _this.handleOwed(daemon, config, category, blockType, data, callback),
(data, callback) => _this.handleRewards(config, category, blockType, data, callback),
(data, callback) => _this.handleSending(daemon, config, blockType, data, callback),
(data, callback) => _this.handleUpdates(config, category, blockType, interval, data, callback),
], (error) => {
if (error) {
callbackMain(null, false);
return;
}
const pool = config.name;
logger.debug('Payments', pool, 'Finished payment processing management and attempted to send out payments.');
callbackMain(null, true);
});
};
// Start Interval Initialization
/* istanbul ignore next */
this.handleIntervals = function(daemon, config, blockType) {
const processingConfig = blockType === 'primary' ? config.primary : config.auxiliary;
// Handle Main Payment Checks
const checkInterval = setInterval(() => {
_this.processChecks(daemon, config, 'checks', blockType, Date.now(), (error) => {
if (error) {
clearInterval(checkInterval);
throw new Error(error);
}
});
}, processingConfig.payments.checkInterval * 1000);
// Handle Main Payment Functionality
if (processingConfig.payments.enabled) {
const paymentInterval = setInterval(() => {
_this.processPayments(daemon, config, 'payments', blockType, Date.now(), (error) => {
if (error) {
clearInterval(paymentInterval);
throw new Error(error);
}
});
}, processingConfig.payments.paymentInterval * 1000);
}
// Start Payment Functionality with Initial Check
setTimeout(() => {
_this.processChecks(daemon, config, 'start', blockType, Date.now(), (error) => {
if (error) {
throw new Error(error);
}
});
}, 100);
};
// Start Payment Interval Management
/* istanbul ignore next */
this.handleManagement = function(data) {
const daemons = data[0];
const config = data[1];
// Setup Intervals for Individual Chains
if (daemons && daemons.length >= 1) {
_this.handleIntervals(daemons[0], config, 'primary');
if (config.auxiliary && config.auxiliary.payments && config.auxiliary.payments.enabled && daemons.length > 1) {
_this.handleIntervals(daemons[1], config, 'auxiliary');
}
}
};
// Handle Primary Payment Processing
/* istanbul ignore next */
this.handlePrimary = function(pool, callbackMain) {
const config = _this.poolConfigs[pool];
config.primary.payments.processingFee = parseFloat(config.primary.payments.transactionFee) || parseFloat(0.0004);
config.primary.payments.minConfirmations = Math.max((config.primary.payments.minConfirmations || 10), 1);
// Build Primary Daemon
const handler = (severity, results) => logger[severity]('Payments', pool, results);
const daemon = new Stratum.daemon([config.primary.payments.daemon], handler);
// Warn if < Recommended Config
if (config.primary.payments.minConfirmations < 3) {
logger.warning('Payments', pool, 'The recommended number of confirmations (primary) is >= 3.');
}
// Handle Initial Validation
async.parallel([
(callback) => _this.handleAddress(daemon, config.primary.address, pool, callback),
(callback) => _this.handleBalance(daemon, config, pool, 'primary', callback),
], (error, results) => {
if (error) {
callbackMain(null, false);
} else {
config.primary.payments.magnitude = results[1][0];
config.primary.payments.minPaymentSatoshis = results[1][1];
config.primary.payments.coinPrecision = results[1][2];
callbackMain(null, [[daemon], config]);
}
});
};
// Handle Auxiliary Payment Processing
/* istanbul ignore next */
this.handleAuxiliary = function(pool, data, callbackMain) {
const config = data[1];
if (config && config.auxiliary && config.auxiliary.enabled) {
config.auxiliary.payments.processingFee = parseFloat(config.auxiliary.payments.transactionFee) || parseFloat(0.0004);
config.auxiliary.payments.minConfirmations = Math.max((config.auxiliary.payments.minConfirmations || 10), 1);
// Build Auxiliary Daemon
const handler = (severity, results) => logger[severity]('Payments', pool, results);
const daemon = new Stratum.daemon([config.auxiliary.payments.daemon], handler);
// Warn if < Recommended Config
if (config.auxiliary.payments.minConfirmations < 3) {
logger.warning('Payments', pool, 'The recommended number of confirmations (auxiliary) is >= 3.');
}
// Handle Initial Validation
async.parallel([
(callback) => _this.handleBalance(daemon, config, pool, 'auxiliary', callback),
], (error, results) => {
if (error) {
callbackMain(null, [data[0], config]);
} else {
data[0].push(daemon);
config.auxiliary.payments.magnitude = results[0][0];
config.auxiliary.payments.minPaymentSatoshis = results[0][1];
config.auxiliary.payments.coinPrecision = results[0][2];
callbackMain(null, [data[0], config]);
}
});
} else {
callbackMain(null, [data[0], config]);
}
};
// Handle Payment Processing for Enabled Pools
/* istanbul ignore next */
this.handlePayments = function(pool, callbackMain) {
async.waterfall([
(callback) => _this.handlePrimary(pool, callback),
(data, callback) => _this.handleAuxiliary(pool, data, callback)
], (error, results) => {
if (error) {
callbackMain(null, false);
} else {
_this.handleManagement(results);
}
});
};
// Output Derived Payment Information
this.outputPaymentInfo = function(pools) {
pools.forEach((pool) => {
const poolOptions = _this.poolConfigs[pool];
const processingConfig = poolOptions.primary.payments;
logger.debug('Payments', pool, `Payment processing setup to run every ${
processingConfig.paymentInterval } second(s) with daemon (${
processingConfig.daemon.username }@${ processingConfig.daemon.host }:${
processingConfig.daemon.port }) and redis (${ _this.portalConfig.redis.host }:${
_this.portalConfig.redis.port })`
);
});
};
// Start Worker Capabilities
/* istanbul ignore next */
this.setupPayments = function(callback) {
async.filter(Object.keys(_this.poolConfigs), _this.handlePayments, (error, results) => {
_this.outputPaymentInfo(results);
callback();
});
};
};
module.exports = PoolPayments;