diff --git a/README.md b/README.md index 14526524..885c5573 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ Features The following feature have been implemented so far: -* Mobile WebUI **NEW** +* Mobile WebUI * Reward Systems * Propotional * PPS - * (Planned) PPLNS + * PPLNS **NEW** * Use of memcache for statistics instead of a cronjob * Web User accounts * Re-Captcha protected registration form diff --git a/cronjobs/archive_cleanup.php b/cronjobs/archive_cleanup.php new file mode 100755 index 00000000..6f008d69 --- /dev/null +++ b/cronjobs/archive_cleanup.php @@ -0,0 +1,29 @@ +#!/usr/bin/php +purgeArchive()) { + $log->logError("Failed to delete archived shares, not critical but should be checked!"); +} +?> diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php new file mode 100755 index 00000000..9657b394 --- /dev/null +++ b/cronjobs/pplns_payout.php @@ -0,0 +1,169 @@ +#!/usr/bin/php +logInfo("Please activate this cron in configuration via payout_system = pplns"); + exit(0); +} + +// Fetch all unaccounted blocks +$aAllBlocks = $block->getAllUnaccounted('ASC'); +if (empty($aAllBlocks)) { + $log->logDebug("No new unaccounted blocks found"); + exit(0); +} + +$count = 0; +foreach ($aAllBlocks as $iIndex => $aBlock) { + // We support some dynamic share targets but fall back to our fixed value + // Re-calculate after each run due to re-targets in this loop + if ($config['pplns']['shares']['type'] == 'blockavg' && $block->getBlockCount() > 0) { + $pplns_target = round($block->getAvgBlockShares($config['pplns']['blockavg']['blockcount'])); + } else { + $pplns_target = $config['pplns']['shares']['default'] ; + } + + if (!$aBlock['accounted']) { + $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; + $iCurrentUpstreamId = $aBlock['share_id']; + if (!is_numeric($iCurrentUpstreamId)) { + $log->logFatal("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue"); + exit(1); + } + $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); + $iNewRoundShares = 0; + $config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward']; + $aRoundAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); + + if ($iRoundShares >= $pplns_target) { + $log->logDebug("Matching or exceeding PPLNS target of $pplns_target with $iRoundShares"); + $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 1, $aBlock['share_id']); + if (empty($aAccountShares)) { + $log->logFatal("No shares found for this block, aborted! Block Height : " . $aBlock['height']); + exit(1); + } + } else { + $log->logDebug("Not able to match PPLNS target of $pplns_target with $iRoundShares"); + // We need to fill up with archived shares + // Grab the full current round shares since we didn't match target + $aAccountShares = $aRoundAccountShares; + if (empty($aAccountShares)) { + $log->logFatal("No shares found for this block, aborted! Block height: " . $aBlock['height']); + exit(1); + } + + // Grab only the most recent shares from Archive that fill the missing shares + $log->logInfo('Fetching ' . ($pplns_target - $iRoundShares) . ' additional shares from archive'); + if (!$aArchiveShares = $share->getArchiveShares($pplns_target - $iRoundShares)) { + $log->logError('Failed to fetch shares from archive, setting target to round total'); + $pplns_target = $iRoundShares; + } else { + // Add archived shares to users current shares, if we have any in archive + if (is_array($aArchiveShares)) { + $log->logDebug('Found shares in archive to match PPLNS target, calculating per-user shares'); + foreach($aAccountShares as $key => $aData) { + if (array_key_exists($aData['username'], $aArchiveShares)) { + $log->logDebug('Found user ' . $aData['username'] . ' in archived shares'); + $log->logDebug(' valid : ' . $aAccountShares[$key]['valid'] . ' + ' . $aArchiveShares[$aData['username']]['valid'] . ' = ' . ($aAccountShares[$key]['valid'] + $aArchiveShares[$aData['username']]['valid']) ); + $log->logDebug(' invalid : ' . $aAccountShares[$key]['invalid'] . ' + ' . $aArchiveShares[$aData['username']]['invalid'] . ' = ' . ($aAccountShares[$key]['invalid'] + $aArchiveShares[$aData['username']]['invalid']) ); + $aAccountShares[$key]['valid'] += $aArchiveShares[$aData['username']]['valid']; + $aAccountShares[$key]['invalid'] += $aArchiveShares[$aData['username']]['invalid']; + } + } + } + // We tried to fill up to PPLNS target, now we need to check the actual shares to properly payout users + foreach($aAccountShares as $key => $aData) { + $iNewRoundShares += $aData['valid']; + } + } + } + + // We filled from archive but still are not able to match PPLNS target, re-adjust + if ($iRoundShares < $iNewRoundShares) { + $log->logInfo('Adjusting round target to ' . $iNewRoundShares); + $iRoundShares = $iNewRoundShares; + } + + // Table header for account shares + $log->logInfo("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee"); + + // Loop through all accounts that have found shares for this round + foreach ($aAccountShares as $key => $aData) { + // Payout based on PPLNS target shares, proportional payout for all users + $aData['percentage'] = number_format(round(( 100 / $pplns_target) * $aData['valid'], 8), 8); + $aData['payout'] = number_format(round(( $aData['percentage'] / 100 ) * $dReward, 8), 8); + // Defaults + $aData['fee' ] = 0; + $aData['donation'] = 0; + + if ($config['fees'] > 0) + $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 8); + // Calculate donation amount, fees not included + $aData['donation'] = number_format(round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8), 8); + + // Verbose output of this users calculations + $log->logInfo($aData['id'] . "\t" . + $aData['username'] . "\t" . + $aData['valid'] . "\t" . + $aData['invalid'] . "\t" . + $aData['percentage'] . "\t" . + $aData['payout'] . "\t" . + $aData['donation'] . "\t" . + $aData['fee']); + + // Add full round share statistics, not just PPLNS + foreach ($aRoundAccountShares as $key => $aRoundData) { + if ($aRoundData['username'] == $aData['username']) + if (!$statistics->updateShareStatistics($aRoundData, $aBlock['id'])) + $log->logError('Failed to update share statistics for ' . $aData['username']); + } + // Add new credit transaction + if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit', $aBlock['id'])) + $log->logFatal('Failed to insert new Credit transaction to database for ' . $aData['username']); + // Add new fee debit for this block + if ($aData['fee'] > 0 && $config['fees'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['fee'], 'Fee', $aBlock['id'])) + $log->logFatal('Failed to insert new Fee transaction to database for ' . $aData['username']); + // Add new donation debit + if ($aData['donation'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['donation'], 'Donation', $aBlock['id'])) + $log->logFatal('Failed to insert new Donation transaction to database for ' . $aData['username']); + } + + // Move counted shares to archive before this blockhash upstream share + if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId)) + $log->logError('Failed to copy shares to archive table'); + // Delete all accounted shares + if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { + $log->logFatal("Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!"); + exit(1); + } + // Mark this block as accounted for + if (!$block->setAccounted($aBlock['id'])) { + $log->logFatal("Failed to mark block as accounted! Aborting!"); + exit(1); + } + } +} diff --git a/cronjobs/pps_payout.php b/cronjobs/pps_payout.php index ee35e2ad..58b8dc83 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/pps_payout.php @@ -120,7 +120,7 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $log->logError("Failed to update stats for this block on : " . $aData['username']); } // Move shares to archive - if ($config['archive_shares'] && $aBlock['share_id'] < $iLastShareId) { + if ($aBlock['share_id'] < $iLastShareId) { if (!$share->moveArchive($aBlock['share_id'], $aBlock['id'], @$iLastBlockShare)) $log->logError("Archving failed"); } diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index 8a7dd2cd..faca41ca 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -97,7 +97,8 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { } // Move counted shares to archive before this blockhash upstream share - if ($config['archive_shares']) $share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId); + if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId)) + $log->logError('Failed to copy shares to archive'); // Delete all accounted shares if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { $log->logFatal('Failed to delete accounted shares from ' . $iPreviousShareId . ' to ' . $iCurrentUpstreamId . ', aborted'); diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index c9dfd949..e797cb44 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -16,7 +16,7 @@ PIDFILE='/tmp/mmcfe-ng-cron.pid' CRONHOME='.' # List of cruns to execute -CRONS="findblock.php proportional_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php" +CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php archive_cleanup.php" # Additional arguments to pass to cronjobs CRONARGS="-v" diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 81d1806a..711ed0b5 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -43,6 +43,18 @@ class Block { return false; } + /** + * Get a specific block, by block height + * @param height int Block Height + * @return data array Block information from DB + **/ + public function getBlock($height) { + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE height = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $height) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + return false; + } + /** * Get our last, highest share ID inserted for a block * @param none @@ -79,6 +91,30 @@ class Block { return false; } + /** + * Get total amount of blocks in our table + * @param noone + * @return data int Count of rows + **/ + public function getBlockCount() { + $stmt = $this->mysqli->prepare("SELECT COUNT(id) AS blocks FROM $this->table"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return (int)$result->fetch_object()->blocks; + return false; + } + + /** + * Fetch our average share count for the past N blocks + * @param limit int Maximum blocks to check + * @return data float Float value of average shares + **/ + public function getAvgBlockShares($limit=10) { + $stmt = $this->mysqli->prepare("SELECT AVG(shares) AS average FROM $this->table LIMIT ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $limit) && $stmt->execute() && $result = $stmt->get_result()) + return (float)$result->fetch_object()->average; + return false; + } + /** * Fetch all unconfirmed blocks from table * @param confirmations int Required confirmations to consider block confirmed diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 496652f0..dddabf33 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -13,9 +13,12 @@ class Share { // This defines each share public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time; - public function __construct($debug, $mysqli, $salt) { + public function __construct($debug, $mysqli, $user, $block, $config) { $this->debug = $debug; $this->mysqli = $mysqli; + $this->user = $user; + $this->config = $config; + $this->block = $block; $this->debug->append("Instantiated Share class", 2); } @@ -70,7 +73,7 @@ class Share { count(id) as total FROM $this->table WHERE our_result = 'Y' - AND id BETWEEN ? AND ? + AND id > ? AND id <= ? "); if ($this->checkStmt($stmt)) { $stmt->bind_param('ii', $previous_upstream, $current_upstream); @@ -86,47 +89,94 @@ class Share { * Fetch all shares grouped by accounts to count share per account * @param previous_upstream int Previous found share accepted by upstream to limit results * @param current_upstream int Current upstream accepted share + * @param limit int Limit to this amount of shares for PPLNS * @return data array username, valid and invalid shares from account **/ public function getSharesForAccounts($previous_upstream=0, $current_upstream) { - $stmt = $this->mysqli->prepare("SELECT - a.id, - validT.account AS username, - sum(validT.valid) as valid, - IFNULL(sum(invalidT.invalid),0) as invalid - FROM - ( - SELECT DISTINCT - SUBSTRING_INDEX( `username` , '.', 1 ) as account, - COUNT(id) AS valid - FROM $this->table - WHERE id BETWEEN ? AND ? - AND our_result = 'Y' - GROUP BY account - ) validT - LEFT JOIN - ( - SELECT DISTINCT - SUBSTRING_INDEX( `username` , '.', 1 ) as account, - COUNT(id) AS invalid - FROM $this->table - WHERE id BETWEEN ? AND ? - AND our_result = 'N' - GROUP BY account - ) invalidT - ON validT.account = invalidT.account - INNER JOIN accounts a ON a.username = validT.account - GROUP BY a.username DESC"); - if ($this->checkStmt($stmt)) { - $stmt->bind_param('iiii', $previous_upstream, $current_upstream, $previous_upstream, $current_upstream); - $stmt->execute(); - $result = $stmt->get_result(); - $stmt->close(); + $stmt = $this->mysqli->prepare(" + SELECT + a.id, + SUBSTRING_INDEX( s.username , '.', 1 ) as username, + IFNULL(SUM(IF(our_result='Y', 1, 0)), 0) AS valid, + IFNULL(SUM(IF(our_result='N', 1, 0)), 0) AS invalid + FROM $this->table AS s + LEFT JOIN " . $this->user->getTableName() . " AS a + ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) + WHERE s.id > ? AND s.id <= ? + GROUP BY username DESC + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); + return false; + } + + /** + * Fetch the highest available share ID from archive + **/ + function getMaxArchiveShareId() { + $stmt = $this->mysqli->prepare(" + SELECT MAX(share_id) AS share_id FROM $this->tableArchive + "); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->share_id; + return false; + } + + /** + * We need a certain amount of valid archived shares + * param left int Left/lowest share ID + * param right int Right/highest share ID + * return array data Returns an array with usernames as keys for easy access + **/ + function getArchiveShares($iCount) { + $iMinId = $this->getMaxArchiveShareId() - $iCount; + $iMaxId = $this->getMaxArchiveShareId(); + $stmt = $this->mysqli->prepare(" + SELECT + a.id, + SUBSTRING_INDEX( s.username , '.', 1 ) as account, + IFNULL(SUM(IF(our_result='Y', 1, 0)), 0) AS valid, + IFNULL(SUM(IF(our_result='N', 1, 0)), 0) AS invalid + FROM $this->tableArchive AS s + LEFT JOIN " . $this->user->getTableName() . " AS a + ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) + WHERE s.share_id > ? AND s.share_id <= ? + GROUP BY account DESC"); + if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $iMinId, $iMaxId) && $stmt->execute() && $result = $stmt->get_result()) { + $aData = NULL; + while ($row = $result->fetch_assoc()) { + $aData[$row['account']] = $row; + } + if (is_array($aData)) return $aData; } return false; } + /** + * We keep shares only up to a certain point + * This can be configured by the user. + * @return return bool true or false + **/ + public function purgeArchive() { + if ($this->config['payout_system'] == 'pplns') { + // Fetch our last block so we can go back configured rounds + $aLastBlock = $this->block->getLast(); + // Fetch the block we need to find the share_id + $aBlock = $this->block->getBlock($aLastBlock['height'] - $this->config['archive']['maxrounds']); + // Now that we know our block, remove those shares + $stmt = $this->mysqli->prepare("DELETE FROM $this->tableArchive WHERE block_id < ? AND time < DATE_SUB(now(), INTERVAL ? MINUTE)"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $aBlock['id'], $config['archive']['maxage']) && $stmt->execute()) + return true; + } else { + // We are not running pplns, so we just need to keep shares of the past minutes + $stmt = $this->mysqli->prepare("DELETE FROM $this->tableArchive WHERE time < DATE_SUB(now(), INTERVAL ? MINUTE)"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $config['archive']['maxage']) && $stmt->execute()) + return true; + } + // Catchall + return false; + } + /** * Move accounted shares to archive table, this step is optional * @param previous_upstream int Previous found share accepted by upstream to limit results @@ -135,10 +185,11 @@ class Share { * @return bool **/ public function moveArchive($current_upstream, $block_id, $previous_upstream=0) { - $archive_stmt = $this->mysqli->prepare("INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time) - SELECT id, username, our_result, upstream_result, ?, time - FROM $this->table - WHERE id BETWEEN ? AND ?"); + $archive_stmt = $this->mysqli->prepare(" + INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time) + SELECT id, username, our_result, upstream_result, ?, time + FROM $this->table + WHERE id > ? AND id <= ?"); if ($this->checkStmt($archive_stmt) && $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream) && $archive_stmt->execute()) { $archive_stmt->close(); return true; @@ -148,7 +199,7 @@ class Share { } public function deleteAccountedShares($current_upstream, $previous_upstream=0) { - $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id BETWEEN ? AND ?"); + $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id > ? AND id <= ?"); if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute()) return true; // Catchall @@ -267,4 +318,4 @@ class Share { } } -$share = new Share($debug, $mysqli, SALT); +$share = new Share($debug, $mysqli, $user, $block, $config); diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 9390b534..cee6d5de 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -169,8 +169,35 @@ $config['block_bonus'] = 0; **/ $config['payout_system'] = 'prop'; -// For debugging purposes you can archive shares in the archive_shares table, default: true -$config['archive_shares'] = true; +/** + * Archiving configuration for debugging + * + * Explanation: + * By default, we don't need to archive for a long time. PPLNS and Hashrate + * calculations rely on this archive, but all shares past a certain point can + * safely be deleted. + * + * To ensure we have enough shares on stack for PPLNS, this + * is set to the past 10 rounds. Even with lucky ones in between those should + * fit the PPLNS target. On top of that, even if we have more than 10 rounds, + * we still keep the last maxage shares to ensure we can calculate hashrates. + * Both conditions need to be met in order for shares to be purged from archive. + * + * Proportional mode will only keep the past 24 hours. These are required for + * hashrate calculations to work past a round, hence 24 hours was selected as + * the default. You may want to increase the time for debugging, then add any + * integer reflecting minutes of shares to keep. + * + * Availabe Options: + * maxrounds : PPLNS, keep shares for maxrounds + * maxage : PROP and PPLNS, delete shares older than maxage minutes + * + * Default: + * maxrounds = 10 + * maxage = 60 * 60 * 24 (24h) + **/ +$config['archive']['maxrounds'] = 10; +$config['archive']['maxage'] = 60 * 60 * 24; // URL prefix for block searches, used for block links, default: `http://explorer.litecoin.net/search?q=` // If empty, the block link to the block information page will be removed @@ -183,6 +210,30 @@ $config['chaininfo'] = 'http://allchains.info'; // Pool fees applied to users in percent, default: 0 (disabled) $config['fees'] = 0; +/** + * PPLNS requires some settings to run properly. First we need to define + * a default shares count that is applied if we don't have a proper type set. + * Different dynamic types can be applied, or you can run a fixed scheme. + * + * Explanation + * default : Default target shares for PPLNS + * type : Payout type used in PPLNS + * blockcount : Amount of blocks to check for avg shares + * + * Available Options: + * default : amount of shares, integeger + * type : blockavg or fixed + * blockcount : amount of blocks, any integer + * + * Defaults: + * default = 4000000 + * type = `blockavg` + * blockcount = 10 + **/ +$config['pplns']['shares']['default'] = 4000000; +$config['pplns']['shares']['type'] = 'blockavg'; +$config['pplns']['blockavg']['blockcount'] = 10; + // Pool target difficulty as set in pushpoold configuration file // Please also read this for stratum: https://github.com/TheSerapher/php-mmcfe-ng/wiki/FAQ $config['difficulty'] = 20; diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 147b38e9..cc162501 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -83,9 +83,7 @@ if (@$_SESSION['USERDATA']['id']) { $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); switch ($config['payout_system']) { - case 'pps': - break; - default: + case 'prop' || 'pplns': // Some estimations if (@$aRoundShares['valid'] > 0) { $aGlobal['userdata']['est_block'] = round(( (int)$aGlobal['userdata']['shares']['valid'] / (int)$aRoundShares['valid'] ) * (float)$config['reward'], 8); @@ -98,6 +96,14 @@ if (@$_SESSION['USERDATA']['id']) { $aGlobal['userdata']['est_donation'] = 0; $aGlobal['userdata']['est_payout'] = 0; } + case 'pplns': + if ($iAvgBlockShares = round($block->getAvgBlockShares($config['pplns']['blockavg']['blockcount']))) { + $aGlobal['pplns']['target'] = $iAvgBlockShares; + } else { + $aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; + } + break; + case 'pps': break; } diff --git a/public/templates/mmcFE/global/sidebar_pplns.tpl b/public/templates/mmcFE/global/sidebar_pplns.tpl new file mode 100644 index 00000000..deb0c1cc --- /dev/null +++ b/public/templates/mmcFE/global/sidebar_pplns.tpl @@ -0,0 +1,74 @@ +
+
+
+
+

Dashboard

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/public/templates/mobile/global/sidebar_pplns.tpl b/public/templates/mobile/global/sidebar_pplns.tpl new file mode 100644 index 00000000..95dcf93c --- /dev/null +++ b/public/templates/mobile/global/sidebar_pplns.tpl @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PPLNS Target{$GLOBAL.pplns.target|number_format}
 
Your Stats
Hashrate{$GLOBAL.userdata.hashrate|number_format} KH/s
Unpaid Shares
Your Valid{$GLOBAL.userdata.shares.valid|number_format}
Pool Valid{$GLOBAL.roundshares.valid|number_format}
Round Shares
Pool Valid{$GLOBAL.roundshares.valid|number_format}
Pool Invalid{$GLOBAL.roundshares.invalid|number_format}{if $GLOBAL.roundshares.valid > 0} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%){/if}
Your Invalid{$GLOBAL.userdata.shares.invalid|number_format}{if $GLOBAL.roundshares.valid > 0} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%){/if}
{$GLOBAL.config.currency} Round Estimate
Block{$GLOBAL.userdata.est_block|number_format:"3"}
Fees{$GLOBAL.userdata.est_fee|number_format:"3"}
Donation{$GLOBAL.userdata.est_donation|number_format:"3"}
Payout{$GLOBAL.userdata.est_payout|number_format:"3"}
 
{$GLOBAL.config.currency} Account Balance
Confirmed{$GLOBAL.userdata.balance.confirmed|default:"0"}
Unconfirmed{$GLOBAL.userdata.balance.unconfirmed|default:"0"}