diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d0187d0a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# How to contribute + +Third-party patches are much appreciated. This is a rather large project +and a single person can not work on it 24/7 to address all issues and +feature requests. If you feel comfortable with PHP and Smarty you should +consider following this contribution guide! + +## Getting Started + +* Make sure you have a [GitHub account][4]. +* Submit an [Issue][1] for your issue, assuming one does not already exist. + * Clearly describe the issue including steps to reproduce when it is a bug. + * Make sure you fill in the earliest version that you know has the issue. +* Fork the repository into your GitHub account + +## Making Changes + +* Create a topic branch from where you want to base your work. + * This is usually the `next` branch. + * Only target release branches if you are certain your fix must be on that + branch. + * To quickly create a topic branch based on `next`; `git branch + fix/next/my_contribution next` then checkout the new branch with `git + checkout fix/next/my_contribution`. Please avoid working directly on the + `next` branch. +* Make commits of logical units. +* Check for unnecessary whitespace with `git diff --check` before committing. +* Make sure your commit messages are in the proper format. + +```` + (#99999) Make the example in CONTRIBUTING imperative and concrete + + Without this patch applied the example commit message in the CONTRIBUTING + document is not a concrete example. This is a problem because the + contributor is left to imagine what the commit message should look like + based on a description rather than an example. This patch fixes the + problem by making the example concrete and imperative. + + The first line is a real life imperative statement with an issue number + from our issue tracker. The body describes the behavior without the patch, + why this is a problem, and how the patch fixes the problem when applied. +```` + +## Submitting Changes + +* Push your changes to a topic branch in your fork of the repository. +* Submit a pull request to the origin repository. +* Update your issue that you have submitted code and are ready for it to be reviewed. + * Include a link to the pull request in the ticket + +# Additional Resources + +* [Issue Tracker][1] +* [General GitHub documentation][2] +* [GitHub pull request documentation][3] + +[1]: https://github.com/TheSerapher/php-mmcfe-ng/issues "Issue" +[2]: http://help.github.com/ "GitHub documentation" +[3]: http://help.github.com/send-pull-requests/ "GitHub pull request documentation" +[4]: https://github.com/signup/free "GitHub account" diff --git a/README.md b/README.md index 5ad2e71a..66e3dcc2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Description =========== -mmcFE-ng is a web frontend for Pooled LTC Mining. +mmcFE-ng is a web frontend for Pooled LTC Mining. A pool using this interface is running at http://pool.grewe.ca The web frontend layout is based on mmcFE, the original work by Greedi: https://github.com/Greedi/mmcFE @@ -23,6 +23,13 @@ I was hoping to keep this out of the README but apparently people remove or chan at the bottom of the page. For those of you finding my project and are willing to appreciate the work with some hard earned LTC feel free to donate to my LTC address: `Lge95QR2frp9y1wJufjUPCycVsg5gLJPW8` +Donors +====== + +These people have supported this project with a donation: + +* [obigal](https://github.com/obigal) + Requirements ============ @@ -36,6 +43,7 @@ in the appropriate forums. * PHP 5.4+ * php5-mysqlnd * php5-memcached + * php5-curl * MySQL Server * mysql-server * memcached @@ -47,6 +55,10 @@ Features The following feature have been implemented so far: +* Reward Systems + * Propotional + * (Planned) PPS + * (Planned) PPLNS * Use of memcache for statistics instead of a cronjob * Web User accounts * Worker accounts @@ -63,107 +75,27 @@ The following feature have been implemented so far: Installation ============ -Please ensure you fullfill the minimal installation requirements listed above -and install any missing packages or software. +Please take a look at the [Quick Start Guide](https://github.com/TheSerapher/php-mmcfe-ng/wiki/Quick-Start-Guide). This will give you +an idea how to setup `mmcfe-ng`. -Download Source ---------------- +Contributing +============ -Download the (stable) master branch from Github: +You can contribute to this project in different ways: -``` -git clone -b master git://github.com/TheSerapher/php-mmcfe-ng.git mmcfe-ng -``` +* Report outstanding issues and bugs by creating an [Issue][1] +* Suggest feature enhancements also via [Issues][1] +* Fork the project, create a branch and file a pull request to improve the code itself -Or, if you are not using git, use the ZIP file provided: +Contact +======= -``` -wget https://github.com/TheSerapher/php-mmcfe-ng/archive/master.zip -unzip master.zip -mv php-mmcfe-ng-master mmcfe-ng -``` - -Permissions ------------ - -Please ensure your webuser (e.g. `www-data`, `apache`) has write access to -the `mmcfe-ng/public/templates/compile` folder! Otherwise compiled -templates can not be stored: - -``` -sudo chown www-data mmcfe-ng/public/templates/compile -``` - -Apache2 Configuration ---------------------- - -Please point your website document root to the `mmcfe-ng/public` folder -and enable auto-index for `index.php`. - -Memcache --------- - -Please install and start a default memcache instance. Not only would you -need one for `pushpoold` but the statistics page is storing data in -`memcache` as well to improve performance. Your memcache can be -configured in the global configuration file (see below). - -Configuration -------------- - -Please create the `mmcfe-ng/public/include/config/global.inc.php` -configuration from the supplied template -`mmcfe-ng/public/include/config/global.inc.dist.php`. - -Pushpoold ---------- - -Please ensure the passwords are read from the proper table by adding this to your configuration: - -``` - # database settings - "database" : { - "engine" : "mysql", - "port" : "3306", - "name" : "mmcfeng_database_name", - "username" : "someuser", - "password" : "somepass", - "sharelog" : true, - "stmt.pwdb":"SELECT `password` FROM `workers` WHERE `username` = ?", - "stmt.sharelog":"INSERT INTO shares (rem_host, username, our_result, upstream_result, reason, solution) VALUES (?, ?, ?, ?, ?, ?)" - }, - -``` - -Database -======== - -Now that the software is ready we need to import the database. -You will find the SQL file in the `mmcfe-ng/sql` folder. -Import this file into an existing database and you should -have the proper structure ready. - -TODO -==== - -I tried to cover most features available in mmcFE. There might be some missing still -(like graphs, some stats) but if you figure there is a core function missing please let -me know by creating an [Issue][1] marked as `Feature Request`. - -Disclaimer -========== - -This is a **WIP Project**. Most functionality is now added, the core -features are available and the backend cronjobs are working. I would not recommend -running this on a live pool yet. You can play around and test basic functionality but -wait for any live deployment for at least a stable Release Candidate. - - [1]: https://github.com/TheSerapher/php-mmcfe-ng/issues "Issue" +You can find me on Freenode.net, #mmcfe-ng. License and Author ================== -Copyright 2012, Sebastian Grewe +Copyright 2012, Sebastian Grewe Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -177,3 +109,5 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + [1]: https://github.com/TheSerapher/php-mmcfe-ng/issues "Issue" diff --git a/cronjobs/blockupdate.php b/cronjobs/blockupdate.php index 877471a8..65779a3a 100755 --- a/cronjobs/blockupdate.php +++ b/cronjobs/blockupdate.php @@ -33,7 +33,18 @@ $aAllBlocks = $block->getAllUnconfirmed($config['confirmations']); verbose("ID\tBlockhash\tConfirmations\t\n"); foreach ($aAllBlocks as $iIndex => $aBlock) { $aBlockInfo = $bitcoin->query('getblock', $aBlock['blockhash']); + // Fetch this blocks transaction details to find orphan blocks + $aTxDetails = $bitcoin->query('gettransaction', $aBlockInfo['tx'][0]); verbose($aBlock['id'] . "\t" . $aBlock['blockhash'] . "\t" . $aBlock['confirmations'] . " -> " . $aBlockInfo['confirmations'] . "\t"); + if ($aTxDetails['details'][0]['category'] == 'orphan') { + // We have an orphaned block, we need to invalidate all transactions for this one + if ($transaction->setOrphan($aBlock['id']) && $block->setConfirmations($aBlock['id'], -1)) { + verbose("ORPHAN\n"); + } else { + verbose("ORPHAN_ERR"); + } + continue; + } if ($aBlock['confirmations'] == $aBlockInfo['confirmations']) { verbose("SKIPPED\n"); } else if ($block->setConfirmations($aBlock['id'], $aBlockInfo['confirmations'])) { diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index 985ad84d..025c1d3d 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -23,7 +23,8 @@ limitations under the License. require_once('shared.inc.php'); // Fetch our last block found from the DB as a starting point -$strLastBlockHash = @$block->getLast()->blockhash; +$aLastBlock = @$block->getLast(); +$strLastBlockHash = $aLastBlock['blockhash']; if (!$strLastBlockHash) { $strLastBlockHash = ''; } @@ -39,28 +40,63 @@ if ( $bitcoin->can_connect() === true ){ // Nothing to do so bail out if (empty($aTransactions['transactions'])) { verbose("No new transactions since last block\n"); - exit(0); -} +} else { -// Table header -verbose("Blockhash\t\tHeight\tAmount\tConfirmations\tDiff\t\tTime\t\t\tStatus\n"); + // Table header + verbose("Blockhash\t\tHeight\tAmount\tConfirmations\tDiff\t\tTime\t\t\tStatus\n"); -foreach ($aTransactions['transactions'] as $iIndex => $aData) { - if ( $aData['category'] == 'generate' || $aData['category'] == 'immature' ) { - $aBlockInfo = $bitcoin->query('getblock', $aData['blockhash']); - $aData['height'] = $aBlockInfo['height']; - $aData['difficulty'] = $aBlockInfo['difficulty']; - verbose(substr($aData['blockhash'], 0, 15) . "...\t" . - $aData['height'] . "\t" . - $aData['amount'] . "\t" . - $aData['confirmations'] . "\t\t" . - $aData['difficulty'] . "\t" . - strftime("%Y-%m-%d %H:%M:%S", $aData['time']) . "\t"); - if ( $block->addBlock($aData) ) { - verbose("Added\n"); - } else { - verbose("Failed" . "\n"); + // Let us add those blocks as unaccounted + foreach ($aTransactions['transactions'] as $iIndex => $aData) { + if ( $aData['category'] == 'generate' || $aData['category'] == 'immature' ) { + $aBlockInfo = $bitcoin->query('getblock', $aData['blockhash']); + $aData['height'] = $aBlockInfo['height']; + $aData['difficulty'] = $aBlockInfo['difficulty']; + verbose(substr($aData['blockhash'], 0, 15) . "...\t" . + $aData['height'] . "\t" . + $aData['amount'] . "\t" . + $aData['confirmations'] . "\t\t" . + $aData['difficulty'] . "\t" . + strftime("%Y-%m-%d %H:%M:%S", $aData['time']) . "\t"); + if ( $block->addBlock($aData) ) { + verbose("Added\n"); + } else { + verbose("Failed" . "\n"); + } } } } + +// Now with our blocks added we can scan for their upstream shares +$aAllBlocks = $block->getAllUnaccounted('ASC'); + +// Loop through our unaccounted blocks +verbose("Block ID\tBlock Height\tShare ID\tFinder\t\t\tStatus\n"); +foreach ($aAllBlocks as $iIndex => $aBlock) { + if (empty($aBlock['share_id'])) { + // Fetch this blocks upstream ID + if ($share->setUpstream($block->getLastUpstreamId())) { + $iCurrentUpstreamId = $share->getUpstreamId(); + $iAccountId = $user->getUserId($share->getUpstreamFinder()); + } else { + verbose("Unable to fetch blocks upstream share\n"); + verbose($share->getError() . "\n"); + continue; + } + // Store new information + $strStatus = "OK"; + if (!$block->setShareId($aBlock['id'], $iCurrentUpstreamId)) + $strStatus = "Share ID Failed"; + if (!$block->setFinder($aBlock['id'], $iAccountId)) + $strStatus = "Finder Failed"; + verbose( + $aBlock['id'] . "\t\t" + . $aBlock['height'] . "\t\t" + . $iCurrentUpstreamId . "\t\t" + . "[$iAccountId] " . $user->getUserName($iAccountId) . "\t\t" + . $strStatus + . "\n" + ); + } +} ?> + diff --git a/cronjobs/pps_payout.php b/cronjobs/proportional_payout.php similarity index 74% rename from cronjobs/pps_payout.php rename to cronjobs/proportional_payout.php index 8040c0f5..30d0b018 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/proportional_payout.php @@ -32,33 +32,30 @@ if (empty($aAllBlocks)) { $count = 0; foreach ($aAllBlocks as $iIndex => $aBlock) { if (!$aBlock['accounted']) { - if ($share->setUpstream(@$aAllBlocks[$iIndex - 1]['time'])) { - $share->setLastUpstreamId(); - } + $iPreviousShareId = $aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; + $iCurrentUpstreamId = $aBlock['share_id']; + $aAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); + $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); - if ($share->setUpstream($aBlock['time'])) { - $iCurrentUpstreamId = $share->getUpstreamId(); - } else { - verbose("Unable to fetch blocks upstream share\n"); - verbose($share->getError() . "\n"); - continue; - } - $aAccountShares = $share->getSharesForAccounts($share->getLastUpstreamId(), $iCurrentUpstreamId); - $iRoundShares = $share->getRoundShares($share->getLastUpstreamId(), $iCurrentUpstreamId); + // Table header for block details verbose("ID\tHeight\tTime\t\tShares\tFinder\t\tShare ID\tPrev Share\t\tStatus\n"); - verbose($aBlock['id'] . "\t" . $aBlock['height'] . "\t" . $aBlock['time'] . "\t" . $iRoundShares . "\t" . $share->getUpstreamFinder() . "\t" . $share->getUpstreamId() . "\t\t" . $share->getLastUpstreamId()); + verbose($aBlock['id'] . "\t" . $aBlock['height'] . "\t" . $aBlock['time'] . "\t" . $iRoundShares . "\t" . $user->getUserName($aBlock['account_id']) . "\t" . $iCurrentUpstreamId . "\t\t" . $iPreviousShareId); + if (empty($aAccountShares)) { verbose("\nNo shares found for this block\n\n"); sleep(2); continue; } $strStatus = "OK"; - if (!$block->setFinder($aBlock['id'], $user->getUserId($share->getUpstreamFinder()))) - $strStatus = "Finder Failed"; + // Store share information for this block if (!$block->setShares($aBlock['id'], $iRoundShares)) $strStatus = "Shares Failed"; verbose("\t\t$strStatus\n\n"); + + // Table header for account shares verbose("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee\t\tStatus\n"); + + // Loop through all accounts that have found shares for this round foreach ($aAccountShares as $key => $aData) { // Payout based on shares, PPS system $aData['percentage'] = number_format(round(( 100 / $iRoundShares ) * $aData['valid'], 8), 8); @@ -99,10 +96,19 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $strStatus = "Donation Failed"; verbose("\t$strStatus\n"); } - verbose("------------------------------------------------------------------------\n\n"); // Move counted shares to archive before this blockhash upstream share - $share->moveArchive($share->getLastUpstreamId(), $iCurrentUpstreamId, $aBlock['id']); - $block->setAccounted($aBlock['id']); + if ($config['archive_shares']) $share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId); + // Delete all accounted shares + if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { + verbose("\nERROR : Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!\n"); + exit(1); + } + // Mark this block as accounted for + if (!$block->setAccounted($aBlock['id'])) { + verbose("\nERROR : Failed to mark block as accounted! Aborting!\n"); + } + + verbose("------------------------------------------------------------------------\n\n"); } } diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh new file mode 100755 index 00000000..730a9063 --- /dev/null +++ b/cronjobs/run-crons.sh @@ -0,0 +1,74 @@ +#!/bin/bash + + +######################### +# # +# Configuration Options # +# # +######################### +# PHP Detections, if this fails hard code it +PHP_BIN=$( which php ) + +# Path to PID file, needs to be writable by user running this +PIDFILE='/tmp/mmcfe-ng-cron.pid' + +# Location of our cronjobs, assume current directory +CRONHOME='.' + +# List of cruns to execute +CRONS="findblock.php proportional_payout.php blockupdate.php auto_payout.php tickerupdate.php" + +# Additional arguments to pass to cronjobs +CRONARGS="-v" + +# Output additional runtime information +VERBOSE="0" + +################################################################ +# # +# You probably don't need to change anything beyond this point # +# # +################################################################ + +# Change working director to CRONHOME +if ! cd $CRONHOME 2>/dev/null; then + echo "Unable to change to working directory \$CRONHOME: $CRONHOME" + exit 1 +fi + +# Confiuration checks +if [[ -z $PHP_BIN || ! -x $PHP_BIN ]]; then + echo "Unable to locate you php binary." + exit 1 +fi + +if [[ ! -e 'shared.inc.php' ]]; then + echo "Not in cronjobs folder, please ensure \$CRONHOME is set!" + exit 1 +fi + +# Our PID of this shell +PID=$$ + +if [[ -e $PIDFILE ]]; then + echo "Cron seems to be running already" + RUNPID=$( cat $PIDFILE ) + if ps fax | grep -q "^\<$RUNPID\>"; then + echo "Process found in process table, aborting" + exit 1 + else + echo "Process $RUNPID not found. Plese remove $PIDFILE if process is indeed dead." + exit 1 + fi +fi + +# Write our PID file +echo $PID > $PIDFILE + +for cron in $CRONS; do + [[ $VERBOSE == 1 ]] && echo "Running $cron, see output below for details" + $PHP_BIN $cron $CRONARGS +done + +# Remove pidfile +rm -f $PIDFILE diff --git a/cronjobs/tickerupdate.php b/cronjobs/tickerupdate.php new file mode 100755 index 00000000..7850ade9 --- /dev/null +++ b/cronjobs/tickerupdate.php @@ -0,0 +1,36 @@ +#!/usr/bin/php +getApi($config['price']['url'], $config['price']['target'])) { + if (!$setting->setValue('price', $aData['ticker']['last'])) + verbose("ERR Table update failed"); +} else { + verbose("ERR Failed download JSON data from " . $config['price']['url'].$config['price']['target'] . "\n"); +} + +?> diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 0da83b59..ea16564e 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -3,6 +3,7 @@ require_once(CLASS_DIR . '/debug.class.php'); require_once(CLASS_DIR . '/bitcoin.class.php'); require_once(CLASS_DIR . '/statscache.class.php'); +require_once(CLASS_DIR . '/bitcoinwrapper.class.php'); require_once(INCLUDE_DIR . '/database.inc.php'); require_once(INCLUDE_DIR . '/smarty.inc.php'); // Load classes that need the above as dependencies @@ -12,4 +13,4 @@ require_once(CLASS_DIR . '/share.class.php'); require_once(CLASS_DIR . '/worker.class.php'); require_once(CLASS_DIR . '/statistics.class.php'); require_once(CLASS_DIR . '/transaction.class.php'); -require_once(CLASS_DIR . '/settings.class.php'); +require_once(CLASS_DIR . '/setting.class.php'); diff --git a/public/include/classes/bitcoin.class.php b/public/include/classes/bitcoin.class.php index 9a901be6..fa88d415 100644 --- a/public/include/classes/bitcoin.class.php +++ b/public/include/classes/bitcoin.class.php @@ -898,6 +898,3 @@ class BitcoinClient extends jsonrpc_client { return $this->query("getaddressesbyaccount", $account); } } - -// Auto-load this class -$bitcoin = new BitcoinClient($config['wallet']['type'], $config['wallet']['username'], $config['wallet']['password'], $config['wallet']['host']); diff --git a/public/include/classes/bitcoinwrapper.class.php b/public/include/classes/bitcoinwrapper.class.php new file mode 100644 index 00000000..eb6d55e0 --- /dev/null +++ b/public/include/classes/bitcoinwrapper.class.php @@ -0,0 +1,45 @@ +type = $type; + $this->username = $username; + $this->password = $password; + $this->host = $host; + // $this->debug is already used + $this->oDebug = $debug; + $this->memcache = $memcache; + return parent::__construct($this->type, $this->username, $this->password, $this->host); + } + /** + * Wrap variouns methods to add caching + **/ + public function getblockcount() { + $this->oDebug->append("STA " . __METHOD__, 4); + if ($data = $this->memcache->get(__FUNCTION__)) return $data; + return $this->memcache->setCache(__FUNCTION__, parent::getblockcount()); + } + public function getdifficulty() { + $this->oDebug->append("STA " . __METHOD__, 4); + if ($data = $this->memcache->get(__FUNCTION__)) return $data; + return $this->memcache->setCache(__FUNCTION__, parent::getdifficulty()); + } + public function getestimatedtime($iCurrentPoolHashrate) { + $this->oDebug->append("STA " . __METHOD__, 4); + if ($iCurrentPoolHashrate == 0) return 0; + if ($data = $this->memcache->get(__FUNCTION__)) return $data; + $dDifficulty = parent::getdifficulty(); + return $this->memcache->setCache(__FUNCTION__, $dDifficulty * pow(2,32) / $iCurrentPoolHashrate); + } +} + +// Load this wrapper +$bitcoin = new BitcoinWrapper($config['wallet']['type'], $config['wallet']['username'], $config['wallet']['password'], $config['wallet']['host'], $debug, $memcache); diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index e4c3f7e2..aad32c08 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -27,17 +27,27 @@ class Block { return $this->table; } + /** + * Specific method to fetch the latest block found + * @param none + * @return data array Array with database fields as keys + **/ public function getLast() { $stmt = $this->mysqli->prepare("SELECT * FROM $this->table ORDER BY height DESC LIMIT 1"); if ($this->checkStmt($stmt)) { $stmt->execute(); $result = $stmt->get_result(); $stmt->close(); - return $result->fetch_object(); + return $result->fetch_assoc(); } return false; } + /** + * Fetch all unaccounted blocks + * @param order string Sort order, default ASC + * @return data array Array with database fields as keys + **/ public function getAllUnaccounted($order='ASC') { $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE accounted = 0 ORDER BY height $order"); if ($this->checkStmt($stmt)) { @@ -49,8 +59,13 @@ class Block { return false; } + /** + * Fetch all unconfirmed blocks from table + * @param confirmations int Required confirmations to consider block confirmed + * @return data array Array with database fields as keys + **/ public function getAllUnconfirmed($confirmations='120') { - $stmt = $this->mysqli->prepare("SELECT id, blockhash, confirmations FROM $this->table WHERE confirmations < ?"); + $stmt = $this->mysqli->prepare("SELECT id, blockhash, confirmations FROM $this->table WHERE confirmations < ? AND confirmations > -1"); if ($this->checkStmt($stmt)) { $stmt->bind_param("i", $confirmations); $stmt->execute(); @@ -61,6 +76,12 @@ class Block { return false; } + /** + * Update confirmations for an existing block + * @param block_id int Block ID to update + * @param confirmations int New confirmations value + * @return bool + **/ public function setConfirmations($block_id, $confirmations) { $stmt = $this->mysqli->prepare("UPDATE $this->table SET confirmations = ? WHERE id = ?"); if ($this->checkStmt($stmt)) { @@ -72,6 +93,11 @@ class Block { return false; } + /** + * Fetch all blocks ordered by DESC height + * @param order string ASC or DESC ordering + * @return data array Array with database fields as keys + **/ public function getAll($order='DESC') { $stmt = $this->mysqli->prepare("SELECT * FROM $this->table ORDER BY height $order"); if ($this->checkStmt($stmt)) { @@ -83,6 +109,11 @@ class Block { return false; } + /** + * Add new new block to the database + * @param block array Block data as an array, see bind_param + * @return bool + **/ public function addBlock($block) { $stmt = $this->mysqli->prepare("INSERT INTO $this->table (height, blockhash, confirmations, amount, difficulty, time) VALUES (?, ?, ?, ?, ?, ?)"); if ($this->checkStmt($stmt)) { @@ -99,6 +130,23 @@ class Block { return false; } + public function getLastUpstreamId() { + $stmt = $this->mysqli->prepare(" + SELECT MAX(share_id) AS share_id FROM $this->table + "); + if ($this->checkStmt($stmt) && $stmt->execute() && $stmt->bind_result($share_id) && $stmt->fetch()) + return $share_id ? $share_id : 0; + // Catchall + return false; + } + + /** + * Update a single column within a single row + * @param block_id int Block ID to update + * @param field string Column name to update + * @param value string Value to insert + * @return bool + **/ private function updateSingle($block_id, $field, $value) { $stmt = $this->mysqli->prepare("UPDATE $this->table SET $field = ? WHERE id = ?"); if ($this->checkStmt($stmt)) { @@ -114,19 +162,49 @@ class Block { return false; } + /** + * Set finder of a block + * @param block_id int Block ID + * @param account_id int Account ID of finder + * @return bool + **/ public function setFinder($block_id, $account_id=NULL) { return $this->updateSingle($block_id, 'account_id', $account_id); } + /** + * Set finding share for a block + * @param block_id int Block ID + * @param share_id int Upstream valid share ID + * @return bool + **/ + public function setShareId($block_id, $share_id) { + return $this->updateSingle($block_id, 'share_id', $share_id); + } + + /** + * Set counted shares for a block + * @param block_id int Block ID + * @param shares int Share count + * @return bool + **/ public function setShares($block_id, $shares=NULL) { return $this->updateSingle($block_id, 'shares', $shares); } + /** + * Set block to be accounted for + * @param block_id int Block ID + * @return bool + **/ public function setAccounted($block_id=NULL) { if (empty($block_id)) return false; return $this->updateSingle($block_id, 'accounted', 1); } + /** + * Helper function + **/ private function checkStmt($bState) { if ($bState ===! true) { $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); @@ -137,4 +215,5 @@ class Block { } } +// Automatically load our class for furhter usage $block = new Block($debug, $mysqli, SALT); diff --git a/public/include/classes/setting.class.php b/public/include/classes/setting.class.php new file mode 100644 index 00000000..f2c1cbea --- /dev/null +++ b/public/include/classes/setting.class.php @@ -0,0 +1,55 @@ +debug = $debug; + $this->mysqli = $mysqli; + $this->salt = $salt; + $this->table = 'settings'; + } + + /** + * Fetch a value from our table + * @param name string Setting name + * @return value string Value + **/ + public function getValue($name) { + $query = $this->mysqli->prepare("SELECT value FROM $this->table WHERE name=? LIMIT 1"); + if ($query) { + $query->bind_param('s', $name); + $query->execute(); + $query->bind_result($value); + $query->fetch(); + $query->close(); + } else { + $this->debug->append("Failed to fetch variable $name from $this->table"); + return false; + } + return $value; + } + + /** + * Insert or update a setting + * @param name string Name of the variable + * @param value string Variable value + * @return bool + **/ + public function setValue($name, $value) { + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (name, value) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE value = ? + "); + if ($stmt && $stmt->bind_param('sss', $name, $value, $value) && $stmt->execute()) + return true; + $this->debug->append("Failed to set $name to $value"); + echo $this->mysqli->error; + return false; + } +} + +$setting = new Setting($debug, $mysqli, SALT); diff --git a/public/include/classes/settings.class.php b/public/include/classes/settings.class.php deleted file mode 100644 index 3c8f9dc0..00000000 --- a/public/include/classes/settings.class.php +++ /dev/null @@ -1,31 +0,0 @@ -debug = $debug; - $this->mysqli = $mysqli; - $this->salt = $salt; - $this->table = 'settings'; - } - - public function getValue($name) { - $query = $this->mysqli->prepare("SELECT value FROM $this->table WHERE setting=? LIMIT 1"); - if ($query) { - $query->bind_param('s', $name); - $query->execute(); - $query->bind_result($value); - $query->fetch(); - $query->close(); - } else { - $this->debug->append("Failed to fetch variable $name from $this->table"); - return false; - } - return $value; - } -} - -$settings = new Settings($debug, $mysqli, SALT); diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index f9e7c869..b0a3b249 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -27,13 +27,29 @@ class Share { return $this->sError; } + /** + * Fetch archive tables name for this class + * @param none + * @return data string Table name + **/ public function getArchiveTableName() { return $this->tableArchive; } + /** + * Fetch normal table name for this class + * @param none + * @return data string Table name + **/ public function getTableName() { return $this->table; } + /** + * Get all valid shares for this round + * @param previous_upstream int Previous found share accepted by upstream to limit results + * @param current_upstream int Current upstream accepted share + * @return data int Total amount of counted shares + **/ public function getRoundShares($previous_upstream=0, $current_upstream) { $stmt = $this->mysqli->prepare("SELECT count(id) as total @@ -51,6 +67,12 @@ class Share { return false; } + /** + * 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 + * @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, @@ -90,29 +112,41 @@ class Share { return false; } - public function moveArchive($previous_upstream=0, $current_upstream,$block_id) { + /** + * Move accounted shares to archive table, this step is optional + * @param previous_upstream int Previous found share accepted by upstream to limit results + * @param current_upstream int Current upstream accepted share + * @param block_id int Block ID to assign shares to a specific block + * @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 ?"); - $delete_stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id BETWEEN ? AND ?"); - if ($this->checkStmt($archive_stmt) && $this->checkStmt($delete_stmt)) { - $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream); - $archive_stmt->execute(); - $delete_stmt->bind_param('ii', $previous_upstream, $current_upstream); - $delete_stmt->execute(); - $delete_stmt->close(); + if ($this->checkStmt($archive_stmt) && $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream) && $archive_stmt->execute()) { $archive_stmt->close(); return true; } + // Catchall return false; } + public function deleteAccountedShares($current_upstream, $previous_upstream=0) { + $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id BETWEEN ? AND ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute()) + return true; + // Catchall + return false; + } + /** + * Set/get last found share accepted by upstream: id and accounts + **/ public function setLastUpstreamId() { $this->iLastUpstreamId = @$this->oUpstream->id ? $this->oUpstream->id : 0; } public function getLastUpstreamId() { - return @$this->iLastUpstreamId; + return @$this->iLastUpstreamId ? @$this->iLastUpstreamId : 0; } public function getUpstreamFinder() { return @$this->oUpstream->account; @@ -120,27 +154,33 @@ class Share { public function getUpstreamId() { return @$this->oUpstream->id; } - public function setUpstream($time='') { - $stmt = $this->mysqli->prepare("SELECT + /** + * Find upstream accepted share that should be valid for a specific block + * Assumptions: + * * Shares are matching blocks in ASC order + * * We can skip all upstream shares of previously found ones used in a block + * @param last int Skips all shares up to last to find new share + * @return bool + **/ + public function setUpstream($last=0) { + $stmt = $this->mysqli->prepare(" + SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, id FROM $this->table WHERE upstream_result = 'Y' - AND UNIX_TIMESTAMP(time) BETWEEN ? AND (? + 1) + AND id > ? ORDER BY id ASC LIMIT 1"); - if ($this->checkStmt($stmt)) { - $stmt->bind_param('ii', $time, $time); - $stmt->execute(); - if (! $result = $stmt->get_result()) { - $this->setErrorMessage("No result returned from query"); - $stmt->close(); - } - $stmt->close(); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $last) && $stmt->execute() && $result = $stmt->get_result()) { $this->oUpstream = $result->fetch_object(); return true; } + // Catchall return false; } + /** + * Helper function + **/ private function checkStmt($bState) { if ($bState ===! true) { $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index 853e854a..8b12391e 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -43,25 +43,13 @@ class Statistics { return true; } - /** - * Another wrapper, we want to store data in memcache and return the actual data - * for further processing - * @param key string Our memcache key - * @param data mixed Our data to store in Memcache - * @param expiration time Our expiration time, see Memcached documentation - * @return data mixed Return our stored data unchanged - **/ - public function setCache($key, $data, $expiration=NULL) { - if ($this->config['memcache']['enabled']) $this->memcache->set($key, $data, $expiration); - return $data; - } - /** * Get our last $limit blocks found * @param limit int Last limit blocks * @return array **/ public function getBlocksFound($limit=10) { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $limit)) return $data; $stmt = $this->mysqli->prepare(" SELECT b.*, a.username as finder @@ -70,7 +58,7 @@ class Statistics { ON b.account_id = a.id ORDER BY height DESC LIMIT ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $limit) && $stmt->execute() && $result = $stmt->get_result()) - return $this->setCache(__FUNCTION__ . $limit, $result->fetch_all(MYSQLI_ASSOC), 5); + return $this->memcache->setCache(__FUNCTION__ . $limit, $result->fetch_all(MYSQLI_ASSOC), 5); // Catchall $this->debug->append("Failed to find blocks:" . $this->mysqli->error); return false; @@ -84,6 +72,7 @@ class Statistics { * @return bool **/ public function updateShareStatistics($aStats, $iBlockId) { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id, valid, invalid, block_id) VALUES (?, ?, ?, ?)"); if ($this->checkStmt($stmt) && $stmt->bind_param('iiii', $aStats['id'], $aStats['valid'], $aStats['invalid'], $iBlockId) && $stmt->execute()) return true; // Catchall @@ -98,6 +87,7 @@ class Statistics { * @return data object Return our hashrateas an object **/ public function getCurrentHashrate() { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" SELECT SUM(hashrate) AS hashrate FROM @@ -107,7 +97,7 @@ class Statistics { SELECT ROUND(COUNT(id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) AS hashrate FROM " . $this->share->getArchiveTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) ) AS sum"); // Catchall - if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) return $this->setCache(__FUNCTION__, $result->fetch_object()->hashrate); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) return $this->memcache->setCache(__FUNCTION__, $result->fetch_object()->hashrate); $this->debug->append("Failed to get hashrate: " . $this->mysqli->error); return false; } @@ -118,6 +108,7 @@ class Statistics { * @return data object Our share rate in shares per second **/ public function getCurrentShareRate() { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" SELECT ROUND(SUM(sharerate) / 600, 2) AS sharerate FROM @@ -126,7 +117,7 @@ class Statistics { UNION ALL SELECT COUNT(id) AS sharerate FROM " . $this->share->getArchiveTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) ) AS sum"); - if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) return $this->setCache(__FUNCTION__, $result->fetch_object()->sharerate); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) return $this->memcache->setCache(__FUNCTION__, $result->fetch_object()->sharerate); // Catchall $this->debug->append("Failed to fetch share rate: " . $this->mysqli->error); return false; @@ -138,6 +129,7 @@ class Statistics { * @return data array invalid and valid shares **/ public function getRoundShares() { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" SELECT @@ -150,7 +142,7 @@ class Statistics { WHERE UNIX_TIMESTAMP(time) >IFNULL((SELECT MAX(time) FROM blocks),0) AND our_result = 'N' ) as invalid"); if ( $this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) - return $this->setCache(__FUNCTION__, $result->fetch_assoc()); + return $this->memcache->setCache(__FUNCTION__, $result->fetch_assoc()); // Catchall $this->debug->append("Failed to fetch round shares: " . $this->mysqli->error); return false; @@ -162,6 +154,7 @@ class Statistics { * @return data array invalid and valid share counts **/ public function getUserShares($account_id) { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT @@ -184,7 +177,7 @@ class Statistics { AND u.id = ? ) AS invalid"); if ($stmt && $stmt->bind_param("ii", $account_id, $account_id) && $stmt->execute() && $result = $stmt->get_result()) - return $this->setCache(__FUNCTION__ . $account_id, $result->fetch_assoc()); + return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_assoc()); // Catchall $this->debug->append("Unable to fetch user round shares: " . $this->mysqli->error); return false; @@ -205,7 +198,7 @@ class Statistics { AND s.time > DATE_SUB(now(), INTERVAL 10 MINUTE) AND u.id = ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $account_id) && $stmt->execute() && $result = $stmt->get_result() ) - return $this->setCache(__FUNCTION__ . $account_id, $result->fetch_object()->hashrate); + return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_object()->hashrate); // Catchall $this->debug->append("Failed to fetch hashrate: " . $this->mysqli->error); return false; @@ -217,6 +210,7 @@ class Statistics { * @return data int Current hashrate in khash/s **/ public function getWorkerHashrate($worker_id) { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $worker_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT ROUND(COUNT(s.id) * POW(2,21)/600/1000) AS hashrate @@ -226,7 +220,7 @@ class Statistics { AND s.time > DATE_SUB(now(), INTERVAL 10 MINUTE) AND u.id = ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $account_id) && $stmt->execute() && $result = $stmt->get_result() ) - return $this->setCache(__FUNCTION__ . $worker_id, $result->fetch_object()->hashrate); + return $this->memcache->setCache(__FUNCTION__ . $worker_id, $result->fetch_object()->hashrate); // Catchall $this->debug->append("Failed to fetch hashrate: " . $this->mysqli->error); return false; @@ -239,6 +233,7 @@ class Statistics { * @return data array Users with shares, account or hashrate, account **/ public function getTopContributors($type='shares', $limit=15) { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $type . $limit)) return $data; switch ($type) { case 'shares': @@ -251,7 +246,7 @@ class Statistics { ORDER BY shares DESC LIMIT ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $limit) && $stmt->execute() && $result = $stmt->get_result()) - return $this->setCache(__FUNCTION__ . $type . $limit, $result->fetch_all(MYSQLI_ASSOC)); + return $this->memcache->setCache(__FUNCTION__ . $type . $limit, $result->fetch_all(MYSQLI_ASSOC)); $this->debug->append("Fetching shares failed: "); return false; break; @@ -266,7 +261,7 @@ class Statistics { GROUP BY account ORDER BY hashrate DESC LIMIT ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $limit) && $stmt->execute() && $result = $stmt->get_result()) - return $this->setCache(__FUNCTION__ . $type . $limit, $result->fetch_all(MYSQLI_ASSOC)); + return $this->memcache->setCache(__FUNCTION__ . $type . $limit, $result->fetch_all(MYSQLI_ASSOC)); $this->debug->append("Fetching shares failed: "); return false; break; @@ -280,6 +275,7 @@ class Statistics { * @return data array NOT FINISHED YET **/ public function getHourlyHashrateByAccount($account_id) { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT @@ -300,7 +296,7 @@ class Statistics { AND a.id = ? GROUP BY HOUR(time)"); if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $account_id, $account_id) && $stmt->execute() && $result = $stmt->get_result()) - return $this->setCache(__FUNCTION__ . $account_id, $result->fetch_all(MYSQLI_ASSOC), 3600); + return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_all(MYSQLI_ASSOC), 3600); // Catchall $this->debug->append("Failed to fetch hourly hashrate: " . $this->mysqli->error); return false; diff --git a/public/include/classes/statscache.class.php b/public/include/classes/statscache.class.php index b6884d61..e84386da 100644 --- a/public/include/classes/statscache.class.php +++ b/public/include/classes/statscache.class.php @@ -43,6 +43,19 @@ class StatsCache extends Memcached { $this->debug->append("Key not found", 3); } } + /** + * Another wrapper, we want to store data in memcache and return the actual data + * for further processing + * @param key string Our memcache key + * @param data mixed Our data to store in Memcache + * @param expiration time Our expiration time, see Memcached documentation + * @return data mixed Return our stored data unchanged + **/ + public function setCache($key, $data, $expiration=NULL) { + if ($this->config['memcache']['enabled']) $this->set($key, $data, $expiration); + return $data; + } + } $memcache = new StatsCache($config, $debug); diff --git a/public/include/classes/tools.class.php b/public/include/classes/tools.class.php new file mode 100644 index 00000000..e97b3746 --- /dev/null +++ b/public/include/classes/tools.class.php @@ -0,0 +1,46 @@ +debug = $debug; + } + + /** + * Fetch JSON data from an API + * @param url string API URL + * @param target string API method + * @param auth array Optional authentication data to be sent with + * @return dec array JSON decoded PHP array + **/ + public function getApi($url, $target, $auth=NULL) { + static $ch = null; + static $ch = null; + if (is_null($ch)) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; BTCE PHP client; '.php_uname('s').'; PHP/'.phpversion().')'); + } + curl_setopt($ch, CURLOPT_URL, $url . $target); + // curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); + + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + + // run the query + $res = curl_exec($ch); + if ($res === false) throw new Exception('Could not get reply: '.curl_error($ch)); + $dec = json_decode($res, true); + if (!$dec) throw new Exception('Invalid data received, please make sure connection is working and requested API exists'); + return $dec; + } +} + +$tools = new Tools($debug); diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 7f8cd76d..15f29c77 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -25,6 +25,15 @@ class Transaction { return $this->sError; } + /** + * Add a new transaction to our class table + * @param account_id int Account ID to book transaction for + * @param amount float Coin amount + * @param type string Transaction type [Credit, Debit_AP, Debit_MP, Fee, Donation, Orphan_Credit, Orphan_Fee, Orphan_Donation] + * @param block_id int Block ID to link transaction to [optional] + * @param coin_address string Coin address for this transaction [optional] + * @return bool + **/ public function addTransaction($account_id, $amount, $type='Credit', $block_id=NULL, $coin_address=NULL) { $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id, amount, block_id, type, coin_address) VALUES (?, ?, ?, ?, ?)"); if ($this->checkStmt($stmt)) { @@ -38,10 +47,55 @@ class Transaction { return false; } - public function addDebit($account_id, $amount, $type='AP') { + /** + * Sometimes transactions become orphans when a block associated to them is orphaned + * Updates the transaction types to Orphan_ + * @param block_id int Orphaned block ID + * @return bool + **/ + public function setOrphan($block_id) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + UPDATE $this->table + SET type = 'Orphan_Credit' + WHERE type = 'Credit' + AND block_id = ? + "); + if (!($this->checkStmt($stmt) && $stmt->bind_param('i', $block_id) && $stmt->execute())) { + $this->debug->append("Failed to set orphan credit transactions for $block_id"); + return false; + } + $stmt = $this->mysqli->prepare(" + UPDATE $this->table + SET type = 'Orphan_Fee' + WHERE type = 'Fee' + AND block_id = ? + "); + if (!($this->checkStmt($stmt) && $stmt->bind_param('i', $block_id) && $stmt->execute())) { + $this->debug->append("Failed to set orphan fee transactions for $block_id"); + return false; + } + $stmt = $this->mysqli->prepare(" + UPDATE $this->table + SET type = 'Orphan_Donation' + WHERE type = 'Donation' + AND block_id = ? + "); + if (!($this->checkStmt($stmt) && $stmt->bind_param('i', $block_id) && $stmt->execute())) { + $this->debug->append("Failed to set orphan donation transactions for $block_id"); + return false; + } + return true; } + /** + * Get all transactions from start for account_id + * @param account_id int Account ID + * @param start int Starting point, id of transaction + * @return data array Database fields as defined in SELECT + **/ public function getTransactions($account_id, $start=0) { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare(" SELECT t.id AS id, @@ -74,7 +128,50 @@ class Transaction { return true; } + /** + * Get total balance for all users locked in wallet + * @param none + * @return data double Amount locked for users + **/ + public function getLockedBalance() { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + SELECT ROUND(IFNULL(t1.credit, 0) - IFNULL(t2.debit, 0) - IFNULL(t3.other, 0), 8) AS balance + FROM + ( + SELECT sum(t.amount) AS credit + FROM $this->table AS t + LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id + WHERE t.type = 'Credit' + AND b.confirmations >= ? + ) AS t1, + ( + SELECT sum(t.amount) AS debit + FROM $this->table AS t + WHERE t.type IN ('Debit_MP', 'Debit_AP') + ) AS t2, + ( + SELECT sum(t.amount) AS other + FROM transactions AS t + LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id + WHERE t.type IN ('Donation','Fee') + AND b.confirmations >= ? + ) AS t3"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $this->config['confirmations'], $this->config['confirmations']) && $stmt->execute() && $stmt->bind_result($dBalance) && $stmt->fetch()) + return $dBalance; + // Catchall + $this->setErrorMessage('Unable to find locked credits for all users'); + $this->debug->append('MySQL query failed : ' . $this->mysqli->error); + return false; + } + + /** + * Get an accounts total balance + * @param account_id int Account ID + * @return data float Credit - Debit - Fees - Donation + **/ public function getBalance($account_id) { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare(" SELECT ROUND(IFNULL(t1.credit, 0) - IFNULL(t2.debit, 0) - IFNULL(t3.other, 0), 8) AS balance FROM diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index a2b7c07e..d6582676 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -11,10 +11,11 @@ class User { private $user = array(); private $tableAccountBalance = 'accountBalance'; - public function __construct($debug, $mysqli, $salt) { + public function __construct($debug, $mysqli, $salt, $config) { $this->debug = $debug; $this->mysqli = $mysqli; $this->salt = $salt; + $this->config = $config; $this->debug->append("Instantiated User class", 2); } @@ -34,6 +35,27 @@ class User { return $this->getSingle($username, 'id', 'username', 's'); } + public function getUserEmail($username) { + return $this->getSingle($username, 'email', 'username', 's'); + } + + public function getUserToken($id) { + return $this->getSingle($id, 'token', 'id'); + } + + public function getIdFromToken($token) { + return $this->getSingle($token, 'id', 'token', 's'); + } + + public function setUserToken($id) { + $field = array( + 'name' => 'token', + 'type' => 's', + 'value' => hash('sha256', $id.time().$this->salt) + ); + return $this->updateSingle($id, $field); + } + /** * Check user login * @param username string Username @@ -41,6 +63,7 @@ class User { * @return bool **/ public function checkLogin($username, $password) { + $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Checking login for $username with password $password", 2); if ( $this->checkUserPassword($username, $password) ) { $this->createSession($username); @@ -56,6 +79,7 @@ class User { * @return bool **/ public function checkPin($userId, $pin=false) { + $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Confirming PIN for $userId and pin $pin", 2); $stmt = $this->mysqli->prepare("SELECT pin FROM $this->table WHERE id=? AND pin=? LIMIT 1"); $pin_hash = hash('sha256', $pin.$this->salt); @@ -76,6 +100,7 @@ class User { * @return array Return result **/ private function getSingle($value, $search='id', $field='id', $type="i") { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare("SELECT $search FROM $this->table WHERE $field = ? LIMIT 1"); if ($this->checkStmt($stmt)) { $stmt->bind_param($type, $value); @@ -94,6 +119,7 @@ class User { * @return data array All users with payout setup **/ public function getAllAutoPayout() { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare(" SELECT id, username, coin_address, ap_threshold @@ -115,6 +141,7 @@ class User { * @return data string Coin Address **/ public function getCoinAddress($userID) { + $this->debug->append("STA " . __METHOD__, 4); return $this->getSingle($userID, 'coin_address', 'id'); } @@ -124,6 +151,7 @@ class User { * @return data string Coin Address **/ public function getDonatePercent($userID) { + $this->debug->append("STA " . __METHOD__, 4); $dPercent = $this->getSingle($userID, 'donate_percent', 'id'); if ($dPercent > 100) $dPercent = 100; if ($dPercent < 0) $dPercent = 0; @@ -136,18 +164,17 @@ class User { * @param field string Field to update * @return bool **/ - private function updateSingle($userID, $field) { - $stmt = $this->mysqli->prepare("UPDATE $this->table SET " . $field['name'] . " = ? WHERE userId = ? LIMIT 1"); - if ($this->checkStmt($stmt)) { - $stmt->bind_param($field['type'].'i', $field['value'], $userID); - $stmt->execute(); - $stmt->close(); + private function updateSingle($id, $field) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("UPDATE $this->table SET " . $field['name'] . " = ? WHERE id = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param($field['type'].'i', $field['value'], $id) && $stmt->execute()) return true; - } + $this->debug->append("Unable to update " . $field['name'] . " with " . $field['value'] . " for ID $id"); return false; } private function checkStmt($bState) { + $this->debug->append("STA " . __METHOD__, 4); if ($bState ===! true) { $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); $this->setErrorMessage('Internal application Error'); @@ -156,7 +183,16 @@ class User { return true; } + /** + * Update the accounts password + * @param userID int User ID + * @param current string Current password + * @param new1 string New password + * @param new2 string New password confirmation + * @return bool + **/ public function updatePassword($userID, $current, $new1, $new2) { + $this->debug->append("STA " . __METHOD__, 4); if ($new1 !== $new2) { $this->setErrorMessage( 'New passwords do not match' ); return false; @@ -180,7 +216,16 @@ class User { return false; } + /** + * Update account information from the edit account page + * @param userID int User ID + * @param address string new coin address + * @param threshold float auto payout threshold + * @param donat float donation % of income + * @return bool + **/ public function updateAccount($userID, $address, $threshold, $donate) { + $this->debug->append("STA " . __METHOD__, 4); $bUser = false; $threshold = min(250, max(0, floatval($threshold))); if ($threshold < 1) $threshold = 0.0; @@ -196,7 +241,30 @@ class User { return false; } + /** + * Check API key for authentication + * @param key string API key hash + * @return bool + **/ + public function checkApiKey($key) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT api_key, id FROM $this->table WHERE api_key = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param("s", $key) && $stmt->execute() && $stmt->bind_result($api_key, $id) && $stmt->fetch()) { + if ($api_key === $key) + return $id; + } + header("HTTP/1.1 401 Unauthorized"); + die('Access denied'); + } + + /** + * Check a password for a user + * @param username string Username + * @param password string Password + * @return bool + **/ private function checkUserPassword($username, $password) { + $this->debug->append("STA " . __METHOD__, 4); $user = array(); $stmt = $this->mysqli->prepare("SELECT username, id FROM $this->table WHERE username=? AND pass=? LIMIT 1"); if ($this->checkStmt($stmt)) { @@ -212,7 +280,13 @@ class User { return false; } + /** + * Create a PHP session for a user + * @param username string Username to create session for + * @return none + **/ private function createSession($username) { + $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Log in user to _SESSION", 2); session_regenerate_id(true); $_SESSION['AUTHENTICATED'] = '1'; @@ -220,25 +294,41 @@ class User { $_SESSION['USERDATA'] = $this->user; } + /** + * Log out current user, destroy the session + * @param none + * @return true + **/ public function logoutUser() { + $this->debug->append("STA " . __METHOD__, 4); session_destroy(); session_regenerate_id(true); return true; } + /** + * Fetch this classes table name + * @return table string This classes table name + **/ public function getTableName() { + $this->debug->append("STA " . __METHOD__, 4); return $this->table; } + /** + * Fetch some basic user information to store for later user + * @param userID int User ID + * return data array Database fields as used in SELECT + **/ public function getUserData($userID) { + $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Fetching user information for user id: $userID"); $stmt = $this->mysqli->prepare(" SELECT - id, username, pin, pass, admin, + id, username, pin, api_key, admin, IFNULL(donate_percent, '0') as donate_percent, coin_address, ap_threshold FROM $this->table WHERE id = ? LIMIT 0,1"); - echo $this->mysqli->error; if ($this->checkStmt($stmt)) { $stmt->bind_param('i', $userID); if (!$stmt->execute()) { @@ -253,7 +343,18 @@ class User { return false; } + /** + * Register a new user in the system + * @param username string Username + * @param password1 string Password + * @param password2 string Password verification + * @param pin int 4 digit PIN code + * @param email1 string Email address + * @param email2 string Email confirmation + * @return bool + **/ public function register($username, $password1, $password2, $pin, $email1='', $email2='') { + $this->debug->append("STA " . __METHOD__, 4); if (strlen($password1) < 8) { $this->setErrorMessage( 'Password is too short, minimum of 8 characters required' ); return false; @@ -275,15 +376,23 @@ class User { return false; } $apikey = hash("sha256",$username.$salt); - $stmt = $this->mysqli->prepare(" - INSERT INTO $this->table (username, pass, email, pin, api_key) - VALUES (?, ?, ?, ?, ?) - "); + if ($this->mysqli->query("SELECT id FROM $this->table LIMIT 1")->num_rows > 0) { + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (username, pass, email, pin, api_key) + VALUES (?, ?, ?, ?, ?) + "); + } else { + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (username, pass, email, pin, api_key, admin) + VALUES (?, ?, ?, ?, ?, 1) + "); + } if ($this->checkStmt($stmt)) { $stmt->bind_param('sssss', $username, hash("sha256", $password1.$this->salt), $email1, hash("sha256", $pin.$this->salt), $apikey); if (!$stmt->execute()) { $this->setErrorMessage( 'Unable to register' ); if ($stmt->sqlstate == '23000') $this->setErrorMessage( 'Username already exists' ); + echo $this->mysqli->error; return false; } $stmt->close(); @@ -291,6 +400,77 @@ class User { } return false; } + + /** + * User a one time token to reset a password + * @param token string one time token + * @param new1 string New password + * @param new2 string New password verification + * @return bool + **/ + public function useToken($token, $new1, $new2) { + $this->debug->append("STA " . __METHOD__, 4); + if ($id = $this->getIdFromToken($token)) { + if ($new1 !== $new2) { + $this->setErrorMessage( 'New passwords do not match' ); + return false; + } + if ( strlen($new1) < 8 ) { + $this->setErrorMessage( 'New password is too short, please use more than 8 chars' ); + return false; + } + $new = hash('sha256', $new1.$this->salt); + $stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ?, token = NULL WHERE id = ? AND token = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('sis', $new, $id, $token) && $stmt->execute() && $stmt->affected_rows === 1) { + return true; + } + } else { + $this->setErrorMessage("Unable find user for your token"); + return false; + } + return false; + } + + /** + * Reset a password by sending a password reset mail + * @param username string Username to reset password for + * @param smarty object Smarty object for mail templating + * @return bool + **/ + public function resetPassword($username, $smarty) { + $this->debug->append("STA " . __METHOD__, 4); + // Fetch the users mail address + if (!$email = $this->getUserEmail($username)) { + $this->setErrorMessage("Unable to find a mail address for user $username"); + return false; + } + if (!$this->setUserToken($this->getUserId($username))) { + $this->setErrorMessage("Unable to setup token for password reset"); + return false; + } + // Send password reset link + if (!$token = $this->getUserToken($this->getUserId($username))) { + $this->setErrorMessage("Unable fetch token for password reset"); + return false; + } + $smarty->assign('TOKEN', $token); + $smarty->assign('USERNAME', $username); + $smarty->assign('WEBSITENAME', $this->config['website']['name']); + $headers = 'From: Website Administration <' . $this->config['website']['email'] . ">\n"; + $headers .= "MIME-Version: 1.0\n"; + $headers .= "Content-Type: text/html; charset=ISO-8859-1\r\n"; + if (mail($email, + $smarty->fetch('templates/mail/subject.tpl'), + $smarty->fetch('templates/mail/body.tpl'), + $headers)) { + return true; + } else { + $this->setErrorMessage("Unable to send mail to your address"); + return false; + } + return false; + } } -$user = new User($debug, $mysqli, SALT); +// Make our class available automatically +$user = new User($debug, $mysqli, SALT, $config); diff --git a/public/include/classes/worker.class.php b/public/include/classes/worker.class.php index 85254900..5a5c19c4 100644 --- a/public/include/classes/worker.class.php +++ b/public/include/classes/worker.class.php @@ -6,7 +6,7 @@ if (!defined('SECURITY')) class Worker { private $sError = ''; - private $table = 'workers'; + private $table = 'pool_worker'; public function __construct($debug, $mysqli, $user, $share, $config) { $this->debug = $debug; @@ -41,6 +41,7 @@ class Worker { * @return bool **/ public function updateWorkers($account_id, $data) { + $this->debug->append("STA " . __METHOD__, 4); $username = $this->user->getUserName($account_id); foreach ($data as $key => $value) { // Prefix the WebUser to Worker name @@ -61,6 +62,7 @@ class Worker { * @return mixed array Workers and their settings or false **/ public function getWorkers($account_id) { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare(" SELECT id, username, password, ( SELECT SIGN(COUNT(id)) FROM " . $this->share->getTableName() . " WHERE username = $this->table.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS active, @@ -81,6 +83,7 @@ class Worker { * @return data mixed int count if any workers are active, false otherwise **/ public function getCountAllActiveWorkers() { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare("SELECT COUNT(DISTINCT username) AS total FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE)"); if ($this->checkStmt($stmt)) { if (!$stmt->execute()) { @@ -103,6 +106,7 @@ class Worker { * @return bool **/ public function addWorker($account_id, $workerName, $workerPassword) { + $this->debug->append("STA " . __METHOD__, 4); $username = $this->user->getUserName($account_id); $workerName = "$username.$workerName"; $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id, username, password) VALUES(?, ?, ?)"); @@ -125,6 +129,7 @@ class Worker { * @return bool **/ public function deleteWorker($account_id, $id) { + $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE account_id = ? AND id = ?"); if ($this->checkStmt($stmt)) { $stmt->bind_param('ii', $account_id, $id); diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 37d39c3f..2bb6069f 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -21,12 +21,20 @@ define('DEBUG', 0); define('SALT', 'PLEASEMAKEMESOMETHINGRANDOM'); $config = array( + 'price' => array( + 'url' => 'https://btc-e.com/api/2', + 'target' => '/ltc_usd/ticker' + ), 'website' => array( 'name' => 'The Pool', 'slogan' => 'Resistance is futile', + 'email' => 'test@example.com', // Mail address used for notifications ), + 'archive_shares' => true, // Store accounted shares in archive table? + 'blockexplorer' => 'http://explorer.litecoin.net/search?q=', // URL for block searches, prefixed to each block number + 'chaininfo' => 'http://allchains.info', // Link to Allchains for Difficulty information 'fees' => 0, - 'difficulty' => '31', // Target difficulty for this pool as set in pushpoold json + 'difficulty' => '20', // Target difficulty for this pool as set in pushpoold json 'reward' => '50', // Reward for finding blocks, fixed value but changes someday 'confirmations' => '120', // Confirmations per block needed to credit transactions 'memcache' => array( diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index c8c9f6fc..bb29cace 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -37,7 +37,7 @@ if ( ! $user->checkPin($_SESSION['USERDATA']['id'], $_POST['authPin']) && $_POST if ($continue == true && $transaction->addTransaction($_SESSION['USERDATA']['id'], $dBalance, 'Debit_MP', NULL, $sCoinAddress)) $_SESSION['POPUP'][] = array('CONTENT' => 'Transaction completed', 'TYPE' => 'success'); } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to pushpool service', 'TYPE' => 'errormsg'); + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service', 'TYPE' => 'errormsg'); } } else { $_SESSION['POPUP'][] = array('CONTENT' => 'Insufficient funds, you need more than 0.1 LTC to cover transaction fees', 'TYPE' => 'errormsg'); @@ -61,6 +61,7 @@ if ( ! $user->checkPin($_SESSION['USERDATA']['id'], $_POST['authPin']) && $_POST break; } } + // Tempalte specifics $smarty->assign("CONTENT", "default.tpl"); ?> diff --git a/public/include/pages/api.inc.php b/public/include/pages/api.inc.php new file mode 100644 index 00000000..c2e64a60 --- /dev/null +++ b/public/include/pages/api.inc.php @@ -0,0 +1,12 @@ +checkApiKey($_REQUEST['api_key']); + +header('HTTP/1.1 400 Bad Request'); +die('400 Bad Request'); +?> diff --git a/public/include/pages/api/getblockcount.inc.php b/public/include/pages/api/getblockcount.inc.php new file mode 100644 index 00000000..2cbd06a5 --- /dev/null +++ b/public/include/pages/api/getblockcount.inc.php @@ -0,0 +1,24 @@ +checkApiKey($_REQUEST['api_key']); + +if ($bitcoin->can_connect() === true){ + if (!$iBlock = $memcache->get('iBlock')) { + $iBlock = $bitcoin->query('getblockcount'); + $memcache->set('iBlock', $iBlock); + } +} else { + $iBlock = 0; +} + +// Output JSON format +echo json_encode(array('getblockcount' => $iBlock)); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getblocksfound.inc.php b/public/include/pages/api/getblocksfound.inc.php new file mode 100644 index 00000000..00883dad --- /dev/null +++ b/public/include/pages/api/getblocksfound.inc.php @@ -0,0 +1,20 @@ +checkApiKey($_REQUEST['api_key']); + +// Set a sane limit, overwrite with URL parameter +$iLimit = 10; +if (@$_REQUEST['limit']) + $iLimit = $_REQUEST['limit']; + +// Output JSON format +echo json_encode(array('getblocksfound' => $statistics->getBlocksFound($iLimit))); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getcurrentworkers.inc.php b/public/include/pages/api/getcurrentworkers.inc.php new file mode 100644 index 00000000..4e26cc1c --- /dev/null +++ b/public/include/pages/api/getcurrentworkers.inc.php @@ -0,0 +1,15 @@ +checkApiKey($_REQUEST['api_key']); + +// Output JSON format +echo json_encode(array('getcurrentworkers' => $worker->getCountAllActiveWorkers())); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getdifficulty.inc.php b/public/include/pages/api/getdifficulty.inc.php new file mode 100644 index 00000000..9d2aa7a2 --- /dev/null +++ b/public/include/pages/api/getdifficulty.inc.php @@ -0,0 +1,25 @@ +checkApiKey($_REQUEST['api_key']); + +// Fetch data from litecoind +if ($bitcoin->can_connect() === true){ + if (!$dDifficulty = $memcache->get('dDifficulty')) { + $memcache->set('dDifficulty', $dDifficulty); + $dDifficulty = $bitcoin->query('getdifficulty'); + } +} else { + $iDifficulty = 1; +} + +// Output JSON format +echo json_encode(array('getdifficulty' => $dDifficulty)); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getestimatedtime.inc.php b/public/include/pages/api/getestimatedtime.inc.php new file mode 100644 index 00000000..a48393fa --- /dev/null +++ b/public/include/pages/api/getestimatedtime.inc.php @@ -0,0 +1,18 @@ +checkApiKey($_REQUEST['api_key']); + +// Estimated time to find the next block +$iCurrentPoolHashrate = $statistics->getCurrentHashrate() * 1000; + +// Output JSON format +echo json_encode(array('getestimatedtime' => $bitcoin->getestimatedtime($iCurrentPoolHashrate))); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getpoolhashrate.inc.php b/public/include/pages/api/getpoolhashrate.inc.php new file mode 100644 index 00000000..6f6763ec --- /dev/null +++ b/public/include/pages/api/getpoolhashrate.inc.php @@ -0,0 +1,15 @@ +checkApiKey($_REQUEST['api_key']); + +// Output JSON format +echo json_encode(array('getpoolhashrate' => $statistics->getCurrentHashrate())); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getpoolsharerate.inc.php b/public/include/pages/api/getpoolsharerate.inc.php new file mode 100644 index 00000000..8e9117f1 --- /dev/null +++ b/public/include/pages/api/getpoolsharerate.inc.php @@ -0,0 +1,15 @@ +checkApiKey($_REQUEST['api_key']); + +// Output JSON format +echo json_encode(array('getpoolsharerate' => $statistics->getCurrentShareRate())); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/gettimesincelastblock.inc.php b/public/include/pages/api/gettimesincelastblock.inc.php new file mode 100644 index 00000000..532da6bd --- /dev/null +++ b/public/include/pages/api/gettimesincelastblock.inc.php @@ -0,0 +1,26 @@ +checkApiKey($_REQUEST['api_key']); + +// Fetch our last block found +$aBlocksFoundData = $statistics->getBlocksFound(1); + +// Time since last block +$now = new DateTime( "now" ); +if (!empty($aBlocksFoundData)) { + $dTimeSinceLast = ($now->getTimestamp() - $aBlocksFoundData[0]['time']); +} else { + $dTimeSinceLast = 0; +} + +// Output JSON format +echo json_encode(array('gettimesincelastblock' => $dTimeSinceLast)); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/password.inc.php b/public/include/pages/password.inc.php new file mode 100644 index 00000000..aecab054 --- /dev/null +++ b/public/include/pages/password.inc.php @@ -0,0 +1,9 @@ +assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/password/change.inc.php b/public/include/pages/password/change.inc.php new file mode 100644 index 00000000..8b5f4064 --- /dev/null +++ b/public/include/pages/password/change.inc.php @@ -0,0 +1,17 @@ +useToken($_POST['token'], $_POST['newPassword'], $_POST['newPassword2'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Password reset complete! Please login.'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg'); + } +} + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/password/reset.inc.php b/public/include/pages/password/reset.inc.php new file mode 100644 index 00000000..796f5811 --- /dev/null +++ b/public/include/pages/password/reset.inc.php @@ -0,0 +1,16 @@ +resetPassword($_POST['username'], $smarty)) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Please check your mail account to finish your password reset'); +} else { + $_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg'); +} + +// Tempalte specifics, user default template by parent page +$smarty->assign("CONTENT", "../default.tpl"); +?> diff --git a/public/include/pages/statistics.inc.php b/public/include/pages/statistics.inc.php index c3f1e298..c465f091 100644 --- a/public/include/pages/statistics.inc.php +++ b/public/include/pages/statistics.inc.php @@ -10,7 +10,7 @@ if ($bitcoin->can_connect() === true){ } else { $iDifficulty = 1; $iBlock = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to pushpool service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); } $smarty->assign("CURRENTBLOCK", $iBlock); diff --git a/public/include/pages/statistics/blocks.inc.php b/public/include/pages/statistics/blocks.inc.php new file mode 100644 index 00000000..7ba0bfdd --- /dev/null +++ b/public/include/pages/statistics/blocks.inc.php @@ -0,0 +1,22 @@ +getBlocksFound($iLimit); +$aBlockData = $aBlocksFoundData[0]; + +// Propagate content our template +$smarty->assign("BLOCKSFOUND", $aBlocksFoundData); +$smarty->assign("BLOCKLIMIT", $iLimit); + +if ($_SESSION['AUTHENTICATED']) { + $smarty->assign("CONTENT", "blocks_found.tpl"); +} else { + $smarty->assign("CONTENT", "default.tpl"); +} +?> diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index af0a4ebf..0c9c2eb7 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -6,28 +6,23 @@ if (!defined('SECURITY')) // Fetch data from litecoind if ($bitcoin->can_connect() === true){ - if (!$dDifficulty = $memcache->get('dDifficulty')) { - $dDifficulty = $bitcoin->query('getdifficulty'); - $memcache->set('dDifficulty', $dDifficulty); - } - if (!$iBlock = $memcache->get('iBlock')) { - $iBlock = $bitcoin->query('getblockcount'); - $memcache->set('iBlock', $iBlock); - } + $dDifficulty = $bitcoin->getdifficulty(); + $iBlock = $bitcoin->getblockcount(); } else { - $iDifficulty = 1; + $dDifficulty = 1; $iBlock = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to pushpool service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); } // Top share contributors $aContributorsShares = $statistics->getTopContributors('shares', 15); // Top hash contributors - $aContributorsHashes = $statistics->getTopContributors('hashes', 15); +$aContributorsHashes = $statistics->getTopContributors('hashes', 15); // Grab the last 10 blocks found -$aBlocksFoundData = $statistics->getBlocksFound(10); +$iLimit = 10; +$aBlocksFoundData = $statistics->getBlocksFound($iLimit); $aBlockData = $aBlocksFoundData[0]; // Estimated time to find the next block @@ -47,6 +42,7 @@ if (!empty($aBlockData)) { $smarty->assign("ESTTIME", $iEstTime); $smarty->assign("TIMESINCELAST", $dTimeSinceLast); $smarty->assign("BLOCKSFOUND", $aBlocksFoundData); +$smarty->assign("BLOCKLIMIT", $iLimit); $smarty->assign("CONTRIBSHARES", $aContributorsShares); $smarty->assign("CONTRIBHASHES", $aContributorsHashes); $smarty->assign("CURRENTBLOCK", $iBlock); diff --git a/public/include/pages/statistics/user.inc.php b/public/include/pages/statistics/user.inc.php index 41316beb..41440b99 100644 --- a/public/include/pages/statistics/user.inc.php +++ b/public/include/pages/statistics/user.inc.php @@ -6,18 +6,12 @@ if (!defined('SECURITY')) // Fetch data from litecoind if ($bitcoin->can_connect() === true){ - if (!$dDifficulty = $memcache->get('dDifficulty')) { - $dDifficulty = $bitcoin->query('getdifficulty'); - $memcache->set('dDifficulty', $dDifficulty); - } - if (!$iBlock = $memcache->get('iBlock')) { - $iBlock = $bitcoin->query('getblockcount'); - $memcache->set('iBlock', $iBlock); - } + $dDifficulty = $bitcoin->getdifficulty(); + $iBlock = $bitcoin->getblockcount(); } else { $iDifficulty = 1; $iBlock = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to pushpool service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); } $aHourlyHashRates = $statistics->getHourlyHashrateByAccount($_SESSION['USERDATA']['id']); diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index e17f82be..f34b3b4a 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -22,22 +22,27 @@ $aGlobal = array( 'roundshares' => $aRoundShares, 'fees' => $config['fees'], 'confirmations' => $config['confirmations'], - 'reward' => $config['reward'] + 'reward' => $config['reward'], + 'price' => $setting->getValue('price'), + 'blockexplorer' => $config['blockexplorer'], + 'chaininfo' => $config['chaininfo'] ); // We don't want these session infos cached -$aGlobal['userdata'] = $_SESSION['USERDATA']['id'] ? $user->getUserData($_SESSION['USERDATA']['id']) : array(); -$aGlobal['userdata']['balance'] = $transaction->getBalance($_SESSION['USERDATA']['id']); +if (@$_SESSION['USERDATA']['id']) { + $aGlobal['userdata'] = $_SESSION['USERDATA']['id'] ? $user->getUserData($_SESSION['USERDATA']['id']) : array(); + $aGlobal['userdata']['balance'] = $transaction->getBalance($_SESSION['USERDATA']['id']); -// Other userdata that we can cache savely -$aGlobal['userdata']['shares'] = $statistics->getUserShares($_SESSION['USERDATA']['id']); -$aGlobal['userdata']['hashrate'] = $statistics->getUserHashrate($_SESSION['USERDATA']['id']); + // Other userdata that we can cache savely + $aGlobal['userdata']['shares'] = $statistics->getUserShares($_SESSION['USERDATA']['id']); + $aGlobal['userdata']['hashrate'] = $statistics->getUserHashrate($_SESSION['USERDATA']['id']); -// Some estimations -$aGlobal['userdata']['est_block'] = round(( (int)$aGlobal['userdata']['shares']['valid'] / (int)$aRoundShares['valid'] ) * (int)$config['reward'], 3); -$aGlobal['userdata']['est_fee'] = round(($config['fees'] / 100) * $aGlobal['userdata']['est_block'], 3); -$aGlobal['userdata']['est_donation'] = round((( $aGlobal['userdata']['donate_percent'] / 100) * ($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_fee'])), 3); -$aGlobal['userdata']['est_payout'] = round($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_donation'] - $aGlobal['userdata']['est_fee'], 3); + // Some estimations + $aGlobal['userdata']['est_block'] = round(( (int)$aGlobal['userdata']['shares']['valid'] / (int)$aRoundShares['valid'] ) * (int)$config['reward'], 3); + $aGlobal['userdata']['est_fee'] = round(($config['fees'] / 100) * $aGlobal['userdata']['est_block'], 3); + $aGlobal['userdata']['est_donation'] = round((( $aGlobal['userdata']['donate_percent'] / 100) * ($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_fee'])), 3); + $aGlobal['userdata']['est_payout'] = round($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_donation'] - $aGlobal['userdata']['est_fee'], 3); +} // Make it available in Smarty $smarty->assign('PATH', 'site_assets/' . THEME); diff --git a/public/site_assets/mmcFE/css/style.css b/public/site_assets/mmcFE/css/style.css index a2cc7ac1..a41aef4c 100644 --- a/public/site_assets/mmcFE/css/style.css +++ b/public/site_assets/mmcFE/css/style.css @@ -1,4 +1,3 @@ - * { padding:0; margin:0; } body { @@ -460,6 +459,14 @@ a:hover { font-size: 11px; } +.block table tr td.right{ + text-align: right; +} +.block table tr th.right{ + text-align: right; +} + + .block table tr td.delete a { color: #666; } .block table tr td.delete a:hover { color: #dd0000; } @@ -854,7 +861,7 @@ a:hover { /* Block sidebar */ .block.withsidebar .bendl { - /* width: 171px; */ + width: 191px; background: url(../images/bendsb.gif) bottom left no-repeat; } @@ -865,7 +872,7 @@ a:hover { } .block.withsidebar .block_content .sidebar { - width: 190px; + width: 210px; float: left; font-size: 11px; } diff --git a/public/templates/mail/body.tpl b/public/templates/mail/body.tpl new file mode 100644 index 00000000..a80b579a --- /dev/null +++ b/public/templates/mail/body.tpl @@ -0,0 +1,10 @@ + + +

Hello {$USERNAME},


+

You have requested a password reset through our online form. In order to complete the request please follow this link:

+

http://{$smarty.server.SERVER_NAME}{$smarty.server.PHP_SELF}?page=password&action=change&token={$TOKEN}

+

You will be asked to change your password. You can then use this new password to login to your account.

+

Cheers,

+

Website Administration

+ + diff --git a/public/templates/mail/subject.tpl b/public/templates/mail/subject.tpl new file mode 100644 index 00000000..665e26f5 --- /dev/null +++ b/public/templates/mail/subject.tpl @@ -0,0 +1 @@ +[ {$WEBSITENAME} ] Password Reset Request diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index c57fe6f4..9d885b16 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -6,6 +6,7 @@ + diff --git a/public/templates/mmcFE/account/transactions/default.tpl b/public/templates/mmcFE/account/transactions/default.tpl index 35ec2cc4..8f5282e1 100644 --- a/public/templates/mmcFE/account/transactions/default.tpl +++ b/public/templates/mmcFE/account/transactions/default.tpl @@ -1,4 +1,4 @@ -{include file="global/block_header.tpl" BLOCK_HEADER="Transaction Log" BUTTONS=array(Confirmed,Unconfirmed)} +{include file="global/block_header.tpl" BLOCK_HEADER="Transaction Log" BUTTONS=array(Confirmed,Unconfirmed,Orphan)}
Username: {$GLOBAL.userdata.username}
User Id: {$GLOBAL.userdata.id}
API Key: {$GLOBAL.userdata.api_key}
Payment Address:
Donation %: [donation amount in percent (example: 0.5)]
Automatic Payout Threshold: [1-250 LTC. Set to '0' for no auto payout]
@@ -77,11 +77,55 @@ {/section} - +
Unconfirmed Totals:{$credits - $debits}{$credits|default - $debits|default}

Listed are your estimated rewards and donations/fees for all blocks awaiting {$GLOBAL.confirmations} confirmations.

+
+
+ + + + + + + + + + + + +{section transaction $TRANSACTIONS} + {if ( + $TRANSACTIONS[transaction].type == 'Orphan_Credit' + or $TRANSACTIONS[transaction].type == 'Orphan_Donation' + or $TRANSACTIONS[transaction].type == 'Orphan_Fee' + )} + + + + + + + + + {if $TRANSACTIONS[transaction].type == Orphan_Credit} + {assign var="orphan_credits" value="`$orphan_credits+$TRANSACTIONS[transaction].amount`"} + {else} + {assign var="orphan_debits" value="`$orphan_debits+$TRANSACTIONS[transaction].amount`"} + {/if} + {/if} +{/section} + + + + + +
TX #DateTX TypePayment AddressBlock #Amount
{$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].timestamp}{$TRANSACTIONS[transaction].type}{$TRANSACTIONS[transaction].coin_address}{if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount}
Orphaned Totals:{$orphan_credits|default - $orphan_debits|default}
+

Listed are your orphaned transactions for blocks not part of the main blockchain.

+
+
{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/gettingstarted/default.tpl b/public/templates/mmcFE/gettingstarted/default.tpl index 6e63157e..7defdb1a 100644 --- a/public/templates/mmcFE/gettingstarted/default.tpl +++ b/public/templates/mmcFE/gettingstarted/default.tpl @@ -11,8 +11,8 @@
  • Create account.
  • Download a miner. @@ -49,7 +49,7 @@ Start your miner using the following parameters: If you use a command-line miner, type:
    	./minerd --url http://pool.grewe.ca:8337/ --userpass Weblogin.Worker:Worker password
     
    - If you want, you can create additional slaves with usernames and passwords of your choice Here

    + If you want, you can create additional workers with usernames and passwords of your choice Here

  • Create a Litecoin address to recieve payments.