From e62293ed2e117144d6360da44c973203ba71587d Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 11 Mar 2014 13:12:46 -0600 Subject: [PATCH 01/18] Work on payment processing --- libs/paymentProcessor.js | 24 ++++++++++++++---------- libs/shareProcessor.js | 5 ++++- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 2813a5c..776edc1 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -80,17 +80,21 @@ function SetupForPool(logger, poolOptions){ daemon.cmd('gettransaction', [tx], function(results){ //console.dir(results[0].response.details[0].category); var status = results[0].response.details[0].category; - var confirmed = (status === 'generate'); + var amount = results[0].response.details[0].amount; + if (status !== 'generate') return; + var f = 'shares_' + coin + ':round' + blockHeight; + console.log(f); + redisClient.hgetall('shares_' + coin + ':round' + blockHeight, function(error, results){ + if (error || !results) return; + console.log('okay ' + JSON.stringify(results)); - /* next: - - get contributed shares - - get unsent payments - - calculate payments - - send payments - - put unsent payments in db - - remove tx from db - - remove shares from db - */ + //get balances_coin from redis for each address in this round + //add up total balances + //send necessary payments + //put left over balances in redis + //clean up (move block entry to processedBlocks_coin) so this logic isn't called again + + }); }); }; diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 43faee5..1d51afd 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -52,12 +52,15 @@ module.exports = function(logger, poolConfig){ if (!isValidShare) return; - connection.hincrby(['shares_' + coin + ':' + shareData.height, shareData.worker, shareData.difficulty], function(error, result){ + connection.hincrby(['shares_' + coin + ':roundCurrent', shareData.worker, shareData.difficulty], function(error, result){ if (error) logger.error('redis', 'Could not store worker share') }); if (isValidBlock){ + connection.rename('shares_' + coin + ':roundCurrent', 'shares_' + coin + ':round' + shareData.height, function(result){ + console.log('rename result: ' + result); + }); connection.sadd(['blocks_' + coin, shareData.tx + ':' + shareData.height], function(error, result){ if (error) logger.error('redis', 'Could not store block data'); From 6860cd50cb58c684d091fb756e2b47570b0025ea Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 11 Mar 2014 19:56:19 -0600 Subject: [PATCH 02/18] Work on payment processing --- libs/paymentProcessor.js | 139 +++++++++++++++++++++++++++++++-------- libs/shareProcessor.js | 6 +- package.json | 3 +- 3 files changed, 117 insertions(+), 31 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 776edc1..7421af2 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -1,8 +1,11 @@ var redis = require('redis'); +var async = require('async'); var Stratum = require('stratum-pool'); + + module.exports = function(logger){ var poolConfigs = JSON.parse(process.env.pools); @@ -76,17 +79,91 @@ function SetupForPool(logger, poolOptions){ connectToRedis(); - var checkTx = function(tx, blockHeight){ - daemon.cmd('gettransaction', [tx], function(results){ - //console.dir(results[0].response.details[0].category); - var status = results[0].response.details[0].category; - var amount = results[0].response.details[0].amount; - if (status !== 'generate') return; - var f = 'shares_' + coin + ':round' + blockHeight; - console.log(f); - redisClient.hgetall('shares_' + coin + ':round' + blockHeight, function(error, results){ - if (error || !results) return; - console.log('okay ' + JSON.stringify(results)); + + + var processPayments = function(){ + async.waterfall([ + + /* Check redis for all pending block submissions, then pass along each object with: + { + transHash1: {height: blockHeight1}, + transHash2: {height: blockHeight2} + } + */ + function(callback){ + redisClient.smembers(coin + '_blocks', function(error, results){ + if (error){ + logger.error('redis', 'Could get blocks from redis ' + JSON.stringify(error)); + callback('done - redis error for getting blocks'); + return; + } + if (results.length === 0){ + callback('done - no pending blocks in redis'); + return; + } + + var txs = {}; + results.forEach(function(item){ + var details = item.split(':'); + var txHash = details[0]; + var height = details[1]; + txs[txHash] = {height: height}; + }); + callback(null, txs); + }); + }, + + /* Receives txs object with key, checks each key (the transHash) with block batch rpc call to daemon. + Each confirmed on get the amount added to transHash object as {amount: amount}, + Non confirmed txHashes get deleted from obj. Then remaining txHashes are passed along + */ + function(txs, callback){ + + var batchRPCcommand = []; + + for (var txHash in txs){ + batchRPCcommand.push(['gettranscation', [txHash]]); + } + + daemon.batchCmd(batchRPCcommand, function(error, txDetails){ + + txDetails.forEach(function (tx){ + var confirmedTxs = txDetails.filter(function(tx){ + var txDetails = tx.details[0]; + if (txDetails.categery === 'generate'){ + txs[txDetails.txid].amount = txDetails.amount; + } + else delete txs[txDetails.txid]; + + }); + if (Object.keys(txs).length === 0){ + callback('done - no confirmed transactions yet'); + return; + } + callback(null, txs); + }); + }); + }, + + /* Use height from each txHash to get worker shares from each round and pass along */ + function(txs, callback){ + + + var shareLooksup = []; + for (var hash in txs){ + var height = txs[hash].height; + shareLooksup.push(['hgetall', coin + '_shares:round' + height]); + } + + redisClient.multi(shareLooksup).exe(function(error, responses){ + if (error){ + callback('done - redis error with multi get rounds share') + return; + } + console.dir(response); + callback(response); + }); + //get balances_coin from redis for each address in this round //add up total balances @@ -94,28 +171,36 @@ function SetupForPool(logger, poolOptions){ //put left over balances in redis //clean up (move block entry to processedBlocks_coin) so this logic isn't called again - }); + }, + + /* Get worker existing balances from coin_balances hashset in redis*/ + function(confirmedTxs, callback){ + + /* Calculate if any payments are ready to be sent and trigger them sending + Get remaining balances for each address and pass it along as object of latest balances + such as {worker1: balance1, worker2, balance2} */ + + }, + + /* update remaining balances in coin_balance hashset in redis */ + function(updateBalances, callback){ + + }, + + //move this block enty to coin_processedBlocks so payments are not resent + function (none, callback){ + + } + + ], function(error, result){ + //log error completion }); }; setInterval(function(){ - redisClient.smembers('blocks_' + coin, function(error, results){ - if (error){ - logger.error('redis', 'Could get blocks from redis ' + JSON.stringify(error)); - return; - } - - results.forEach(function(item){ - var split = item.split(':'); - var tx = split[0]; - var blockHeight = split[1]; - checkTx(tx, blockHeight); - }); - - }); - + processPayments(); }, processingConfig.paymentInterval * 1000); diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 1d51afd..369b9d1 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -52,16 +52,16 @@ module.exports = function(logger, poolConfig){ if (!isValidShare) return; - connection.hincrby(['shares_' + coin + ':roundCurrent', shareData.worker, shareData.difficulty], function(error, result){ + connection.hincrby([coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty], function(error, result){ if (error) logger.error('redis', 'Could not store worker share') }); if (isValidBlock){ - connection.rename('shares_' + coin + ':roundCurrent', 'shares_' + coin + ':round' + shareData.height, function(result){ + connection.rename(coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height, function(result){ console.log('rename result: ' + result); }); - connection.sadd(['blocks_' + coin, shareData.tx + ':' + shareData.height], function(error, result){ + connection.sadd([coin + '_blocks', shareData.tx + ':' + shareData.height], function(error, result){ if (error) logger.error('redis', 'Could not store block data'); }); diff --git a/package.json b/package.json index 35cbc4c..e5610f1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "node-json-minify": "*", "posix": "*", "redis": "*", - "mysql": "*" + "mysql": "*", + "async": "*" }, "engines": { "node": ">=0.10" From 9e5e5b71411f170fa93be4aae24cf2dedcbb3973 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 11 Mar 2014 19:58:33 -0600 Subject: [PATCH 03/18] Work on payment processing --- libs/paymentProcessor.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 7421af2..4a93029 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -164,13 +164,6 @@ function SetupForPool(logger, poolOptions){ callback(response); }); - - //get balances_coin from redis for each address in this round - //add up total balances - //send necessary payments - //put left over balances in redis - //clean up (move block entry to processedBlocks_coin) so this logic isn't called again - }, /* Get worker existing balances from coin_balances hashset in redis*/ From 834cb6122911bb31a6ff6bcf05885902a97167b7 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 11 Mar 2014 21:47:14 -0600 Subject: [PATCH 04/18] More work on payment processing. Making super scalable database layers is exhausting... --- init.js | 8 +++- libs/paymentProcessor.js | 91 ++++++++++++++++++++++++++++------------ 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/init.js b/init.js index 75619e4..1a57431 100644 --- a/init.js +++ b/init.js @@ -99,7 +99,9 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){ }); worker.on('exit', function(code, signal){ logError('poolWorker', 'system', 'Fork ' + forkId + ' died, spawning replacement worker...'); - createPoolWorker(forkId); + setTimeout(function(){ + createPoolWorker(forkId); + }, 2000); }); }; @@ -142,7 +144,9 @@ var startPaymentProcessor = function(poolConfigs){ }); worker.on('exit', function(code, signal){ logError('paymentProcessor', 'system', 'Payment processor died, spawning replacement...'); - startPaymentProcessor(poolConfigs); + setTimeout(function(){ + startPaymentProcessor(poolConfigs); + }, 2000); }); }; diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 4a93029..cbc585a 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -80,6 +80,8 @@ function SetupForPool(logger, poolOptions){ + redisClient.hset('Litecoin_balances', 'zone117x.worker1', 434); + var processPayments = function(){ async.waterfall([ @@ -92,6 +94,7 @@ function SetupForPool(logger, poolOptions){ */ function(callback){ redisClient.smembers(coin + '_blocks', function(error, results){ + if (error){ logger.error('redis', 'Could get blocks from redis ' + JSON.stringify(error)); callback('done - redis error for getting blocks'); @@ -122,29 +125,34 @@ function SetupForPool(logger, poolOptions){ var batchRPCcommand = []; for (var txHash in txs){ - batchRPCcommand.push(['gettranscation', [txHash]]); + batchRPCcommand.push(['gettransaction', [txHash]]); } daemon.batchCmd(batchRPCcommand, function(error, txDetails){ - txDetails.forEach(function (tx){ - var confirmedTxs = txDetails.filter(function(tx){ - var txDetails = tx.details[0]; - if (txDetails.categery === 'generate'){ - txs[txDetails.txid].amount = txDetails.amount; - } - else delete txs[txDetails.txid]; + if (error || !txDetails){ + callback('done - daemon rpc error with batch gettransactions ' + JSON.stringify(error)); + return; + } - }); - if (Object.keys(txs).length === 0){ - callback('done - no confirmed transactions yet'); - return; + txDetails.filter(function(tx){ + var txDetails = tx.result.details[0]; + if (txDetails.categery === 'generate'){ + txs[txDetails.txid].amount = txDetails.amount; } - callback(null, txs); + else delete txs[txDetails.txid]; + }); + if (Object.keys(txs).length === 0){ + callback('done - no confirmed transactions yet'); + return; + } + callback(null, txs); + }); }, + /* Use height from each txHash to get worker shares from each round and pass along */ function(txs, callback){ @@ -155,32 +163,64 @@ function SetupForPool(logger, poolOptions){ shareLooksup.push(['hgetall', coin + '_shares:round' + height]); } - redisClient.multi(shareLooksup).exe(function(error, responses){ + redisClient.multi(shareLooksup).exec(function(error, workerShares){ if (error){ callback('done - redis error with multi get rounds share') return; } - console.dir(response); - callback(response); + + var balancesForRounds = {}; + workerShares.forEach(function(item){ + for (var worker in item){ + var sharesAdded = parseInt(item[worker]); + if (worker in balancesForRounds) + balancesForRounds[worker] += sharesAdded; + else + balancesForRounds[worker] = sharesAdded; + } + }); + callback(null, balancesForRounds); }); }, /* Get worker existing balances from coin_balances hashset in redis*/ - function(confirmedTxs, callback){ + function(balancesForRounds, callback){ - /* Calculate if any payments are ready to be sent and trigger them sending - Get remaining balances for each address and pass it along as object of latest balances - such as {worker1: balance1, worker2, balance2} */ + var workerAddress = Object.keys(balancesForRounds); + + redisClient.hmget([coin + '_balances'].concat(workerAddress), function(error, results){ + if (error){ + callback('done - redis error with multi get rounds share') + return; + } + + for (var i = 0; i < results.length; i++){ + var shareInt = parseInt(results[i]); + if (shareInt) + balancesForRounds[workerAddress[i]] += shareInt; + + } + + callback(null, balancesForRounds) + }); + + + }, + + /* Calculate if any payments are ready to be sent and trigger them sending + Get remaining balances for each address and pass it along as object of latest balances + such as {worker1: balance1, worker2, balance2} */ + function(fullBalance, callback){ }, /* update remaining balances in coin_balance hashset in redis */ - function(updateBalances, callback){ + function(remainingBalance, callback){ }, - //move this block enty to coin_processedBlocks so payments are not resent + //move this block entry to coin_processedBlocks so payments are not resent function (none, callback){ } @@ -191,10 +231,7 @@ function SetupForPool(logger, poolOptions){ }; - setInterval(function(){ - - processPayments(); - - }, processingConfig.paymentInterval * 1000); + setInterval(processPayments, processingConfig.paymentInterval * 1000); + setTimeout(processPayments, 100); }; \ No newline at end of file From 5220ffdc3c4a238ab18b549ff18589c218b9366e Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 11 Mar 2014 22:01:33 -0600 Subject: [PATCH 05/18] Removed testing functions --- libs/paymentProcessor.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index cbc585a..2908494 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -80,9 +80,6 @@ function SetupForPool(logger, poolOptions){ - redisClient.hset('Litecoin_balances', 'zone117x.worker1', 434); - - var processPayments = function(){ async.waterfall([ @@ -184,6 +181,7 @@ function SetupForPool(logger, poolOptions){ }, + /* Get worker existing balances from coin_balances hashset in redis*/ function(balancesForRounds, callback){ @@ -208,19 +206,26 @@ function SetupForPool(logger, poolOptions){ }, + /* Calculate if any payments are ready to be sent and trigger them sending Get remaining balances for each address and pass it along as object of latest balances such as {worker1: balance1, worker2, balance2} */ function(fullBalance, callback){ + + /* if payments dont succeed (likely because daemon isnt responding to rpc), then cancel here + so that all of this can be tried again when the daemon is working. otherwise we will consider + payment sent after we cleaned up the db. + */ + }, - /* update remaining balances in coin_balance hashset in redis */ + /* clean DB: update remaining balances in coin_balance hashset in redis */ function(remainingBalance, callback){ }, - //move this block entry to coin_processedBlocks so payments are not resent + /* clean DB: move this block entry to coin_processedBlocks so payments are not resent */ function (none, callback){ } From 09443b4e6c6bfca410c1b194a5fa1ec20b92ebc5 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 11 Mar 2014 22:57:03 -0600 Subject: [PATCH 06/18] More comments on how to to payment processing and also stats for API --- libs/paymentProcessor.js | 33 ++++++++++++++++----------------- libs/shareProcessor.js | 7 +++++++ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 2908494..47e995a 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -90,6 +90,7 @@ function SetupForPool(logger, poolOptions){ } */ function(callback){ + redisClient.smembers(coin + '_blocks', function(error, results){ if (error){ @@ -113,6 +114,7 @@ function SetupForPool(logger, poolOptions){ }); }, + /* Receives txs object with key, checks each key (the transHash) with block batch rpc call to daemon. Each confirmed on get the amount added to transHash object as {amount: amount}, Non confirmed txHashes get deleted from obj. Then remaining txHashes are passed along @@ -153,7 +155,6 @@ function SetupForPool(logger, poolOptions){ /* Use height from each txHash to get worker shares from each round and pass along */ function(txs, callback){ - var shareLooksup = []; for (var hash in txs){ var height = txs[hash].height; @@ -176,14 +177,13 @@ function SetupForPool(logger, poolOptions){ balancesForRounds[worker] = sharesAdded; } }); - callback(null, balancesForRounds); + callback(null, balancesForRounds, txs); }); - }, /* Get worker existing balances from coin_balances hashset in redis*/ - function(balancesForRounds, callback){ + function(balancesForRounds, txs, callback){ var workerAddress = Object.keys(balancesForRounds); @@ -200,18 +200,18 @@ function SetupForPool(logger, poolOptions){ } - callback(null, balancesForRounds) + callback(null, txs, balancesForRounds) }); - - }, /* Calculate if any payments are ready to be sent and trigger them sending - Get remaining balances for each address and pass it along as object of latest balances - such as {worker1: balance1, worker2, balance2} */ - function(fullBalance, callback){ - + Get balance different for each address and pass it along as object of latest balances such as + {worker1: balance1, worker2, balance2} + when deciding the sent balance, it the difference should be -1*amount they had in db, + if not sending the balance, the differnce should be +(the amount they earned this round) + */ + function(fullBalance, txs, callback){ /* if payments dont succeed (likely because daemon isnt responding to rpc), then cancel here so that all of this can be tried again when the daemon is working. otherwise we will consider @@ -220,16 +220,15 @@ function SetupForPool(logger, poolOptions){ }, - /* clean DB: update remaining balances in coin_balance hashset in redis */ - function(remainingBalance, callback){ - }, + /* clean DB: update remaining balances in coin_balance hashset in redis + */ + function(balanceDifference, txs, callback){ - /* clean DB: move this block entry to coin_processedBlocks so payments are not resent */ - function (none, callback){ + //SMOVE each tx key from coin_blocks to coin_processedBlocks + //HINCRBY to apply balance different for coin_balances worker1 } - ], function(error, result){ //log error completion }); diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 369b9d1..2d9f19a 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -52,6 +52,13 @@ module.exports = function(logger, poolConfig){ if (!isValidShare) return; + /*use http://redis.io/commands/zrangebyscore to store shares with timestamps + so we can use the min-max to get shares from the last x minutes to determine hash rate :) + also use a hash like coin_stats:{ invalidShares, validShares, invalidBlocks, validBlocks, etc } + for more efficient stats + */ + + connection.hincrby([coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty], function(error, result){ if (error) logger.error('redis', 'Could not store worker share') From 191d81bd18dab6ea15b0c675b0f51993b391eee9 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 12 Mar 2014 16:33:29 -0600 Subject: [PATCH 07/18] More development for payment processing... getting there.. --- libs/paymentProcessor.js | 116 +++++++++++++++++++++++---------------- libs/shareProcessor.js | 2 +- 2 files changed, 69 insertions(+), 49 deletions(-) diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 47e995a..540db46 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -80,15 +80,12 @@ function SetupForPool(logger, poolOptions){ + var processPayments = function(){ async.waterfall([ - /* Check redis for all pending block submissions, then pass along each object with: - { - transHash1: {height: blockHeight1}, - transHash2: {height: blockHeight2} - } - */ + /* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted + blocks. */ function(callback){ redisClient.smembers(coin + '_blocks', function(error, results){ @@ -103,30 +100,25 @@ function SetupForPool(logger, poolOptions){ return; } - var txs = {}; + var rounds = []; results.forEach(function(item){ var details = item.split(':'); - var txHash = details[0]; - var height = details[1]; - txs[txHash] = {height: height}; + rounds.push({txHash: details[0], height: details[1], reward: details[2]}); }); - callback(null, txs); + callback(null, rounds); }); }, - /* Receives txs object with key, checks each key (the transHash) with block batch rpc call to daemon. - Each confirmed on get the amount added to transHash object as {amount: amount}, - Non confirmed txHashes get deleted from obj. Then remaining txHashes are passed along - */ - function(txs, callback){ + /* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet. + It also adds the block reward amount to the round object - which the daemon gives also gives us. */ + function(rounds, callback){ var batchRPCcommand = []; - for (var txHash in txs){ - batchRPCcommand.push(['gettransaction', [txHash]]); + for (var i = 0; i < rounds.length; i++){ + batchRPCcommand.push(['gettransaction', [rounds[i].txHash]]); } - daemon.batchCmd(batchRPCcommand, function(error, txDetails){ if (error || !txDetails){ @@ -134,57 +126,83 @@ function SetupForPool(logger, poolOptions){ return; } - txDetails.filter(function(tx){ + //Rounds that are not confirmed yet are removed from the round array + //We also get reward amount for each block from daemon reply + txDetails.forEach(function(tx){ + var txResult = tx.result; var txDetails = tx.result.details[0]; - if (txDetails.categery === 'generate'){ - txs[txDetails.txid].amount = txDetails.amount; + for (var i = 0; i < rounds.length; i++){ + if (rounds[i].txHash === txResult.txid){ + rounds[i].amount = txResult.amount; + rounds[i].magnitude = rounds[i].reward / txResult.amount; + if (txDetails.category !== 'generate') + rounds.splice(i, 1); + } } - else delete txs[txDetails.txid]; - }); - if (Object.keys(txs).length === 0){ + if (rounds.length === 0){ callback('done - no confirmed transactions yet'); return; } - callback(null, txs); + callback(null, rounds); }); }, - /* Use height from each txHash to get worker shares from each round and pass along */ - function(txs, callback){ + /* Does a batch redis call to get shares contributed to each round. Then calculates the reward + amount owned to each miner for each round. */ + function(rounds, callback){ var shareLooksup = []; - for (var hash in txs){ - var height = txs[hash].height; - shareLooksup.push(['hgetall', coin + '_shares:round' + height]); + for (var i = 0; i < rounds.length; i++){ + shareLooksup.push(['hgetall', coin + '_shares:round' + rounds[i].height]); } - redisClient.multi(shareLooksup).exec(function(error, workerShares){ + + + redisClient.multi(shareLooksup).exec(function(error, allWorkerShares){ if (error){ callback('done - redis error with multi get rounds share') return; } - var balancesForRounds = {}; - workerShares.forEach(function(item){ - for (var worker in item){ - var sharesAdded = parseInt(item[worker]); - if (worker in balancesForRounds) - balancesForRounds[worker] += sharesAdded; - else - balancesForRounds[worker] = sharesAdded; + var workerRewards = {}; + + for (var i = 0; i < rounds.length; i++){ + var round = rounds[i]; + var workerShares = allWorkerShares[i]; + + var reward = round.reward * (1 - processingConfig.feePercent); + + var totalShares = 0; + for (var worker in workerShares){ + totalShares += parseInt(workerShares[worker]); } - }); - callback(null, balancesForRounds, txs); + + for (var worker in workerShares){ + var singleWorkerShares = parseInt(workerShares[worker]); + var percent = singleWorkerShares / totalShares; + var workerRewardTotal = (reward * percent) / round.magnitude; + workerRewardTotal = Math.floor(workerRewardTotal * round.magnitude) / round.magnitude; + if (worker in workerRewards) + workerRewards[worker] += workerRewardTotal; + else + workerRewards[worker] = workerRewardTotal; + } + } + + + console.dir(workerRewards); + + callback(null, rounds); }); }, - /* Get worker existing balances from coin_balances hashset in redis*/ - function(balancesForRounds, txs, callback){ - + /* Does a batch call to redis to get worker existing balances from coin_balances*/ + function(rounds, callback){ + /* var workerAddress = Object.keys(balancesForRounds); redisClient.hmget([coin + '_balances'].concat(workerAddress), function(error, results){ @@ -200,8 +218,9 @@ function SetupForPool(logger, poolOptions){ } - callback(null, txs, balancesForRounds) + callback(null, rounds, balancesForRounds) }); + */ }, @@ -211,7 +230,7 @@ function SetupForPool(logger, poolOptions){ when deciding the sent balance, it the difference should be -1*amount they had in db, if not sending the balance, the differnce should be +(the amount they earned this round) */ - function(fullBalance, txs, callback){ + function(fullBalance, rounds, callback){ /* if payments dont succeed (likely because daemon isnt responding to rpc), then cancel here so that all of this can be tried again when the daemon is working. otherwise we will consider @@ -223,13 +242,14 @@ function SetupForPool(logger, poolOptions){ /* clean DB: update remaining balances in coin_balance hashset in redis */ - function(balanceDifference, txs, callback){ + function(balanceDifference, rounds, callback){ //SMOVE each tx key from coin_blocks to coin_processedBlocks //HINCRBY to apply balance different for coin_balances worker1 } ], function(error, result){ + console.log(error); //log error completion }); }; diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 2d9f19a..620f6d5 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -68,7 +68,7 @@ module.exports = function(logger, poolConfig){ connection.rename(coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height, function(result){ console.log('rename result: ' + result); }); - connection.sadd([coin + '_blocks', shareData.tx + ':' + shareData.height], function(error, result){ + connection.sadd([coin + '_blocks', shareData.tx + ':' + shareData.height + ':' + shareData.reward], function(error, result){ if (error) logger.error('redis', 'Could not store block data'); }); From 5acaaa568610fc9bae5a1d3e2931ad93f56eb4b5 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 12 Mar 2014 18:09:12 -0600 Subject: [PATCH 08/18] More development for payment processing. so close... --- README.md | 7 +- libs/paymentProcessor.js | 110 +++++++++++---------- pool_configs/litecoin_testnet_example.json | 2 +- 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 48a3ed5..9892a7e 100644 --- a/README.md +++ b/README.md @@ -128,15 +128,16 @@ Description of options: payments less frequently (they dislike). Opposite for a lower minimum payment. */ "minimumPayment": 0.001, + /* Minimum number of coins to keep in pool wallet. It is recommended to deposit at + at least this many coins into the pool wallet when first starting the pool. */ + "minimumReserve": 10, + /* (2% default) What percent fee your pool takes from the block reward. */ "feePercent": 0.02, /* Your address that receives pool revenue from fees */ "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", - /* Minimum number of coins to keep in pool wallet */ - "minimumReserve": 10, - /* How many coins from fee revenue must accumulate on top of the minimum reserve amount in order to trigger withdrawal to fee address. The higher this threshold, the less of your profit goes to transactions fees. */ diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 540db46..17b859d 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -100,11 +100,11 @@ function SetupForPool(logger, poolOptions){ return; } - var rounds = []; - results.forEach(function(item){ - var details = item.split(':'); - rounds.push({txHash: details[0], height: details[1], reward: details[2]}); + var rounds = results.map(function(r){ + var details = r.split(':'); + return {txHash: details[0], height: details[1], reward: details[2]}; }); + callback(null, rounds); }); }, @@ -114,11 +114,10 @@ function SetupForPool(logger, poolOptions){ It also adds the block reward amount to the round object - which the daemon gives also gives us. */ function(rounds, callback){ - var batchRPCcommand = []; + var batchRPCcommand = rounds.map(function(r){ + return ['gettransaction', [r.txHash]]; + }); - for (var i = 0; i < rounds.length; i++){ - batchRPCcommand.push(['gettransaction', [rounds[i].txHash]]); - } daemon.batchCmd(batchRPCcommand, function(error, txDetails){ if (error || !txDetails){ @@ -128,18 +127,14 @@ function SetupForPool(logger, poolOptions){ //Rounds that are not confirmed yet are removed from the round array //We also get reward amount for each block from daemon reply - txDetails.forEach(function(tx){ - var txResult = tx.result; - var txDetails = tx.result.details[0]; - for (var i = 0; i < rounds.length; i++){ - if (rounds[i].txHash === txResult.txid){ - rounds[i].amount = txResult.amount; - rounds[i].magnitude = rounds[i].reward / txResult.amount; - if (txDetails.category !== 'generate') - rounds.splice(i, 1); - } - } + rounds = rounds.filter(function(r){ + var tx = txDetails.filter(function(t){ return t.result.txid === r.txHash; })[0]; + if (tx.result.details[0].category !== 'generate') return false; + r.amount = tx.result.amount; + r.magnitude = r.reward / r.amount; + return true; }); + if (rounds.length === 0){ callback('done - no confirmed transactions yet'); return; @@ -154,14 +149,12 @@ function SetupForPool(logger, poolOptions){ amount owned to each miner for each round. */ function(rounds, callback){ - var shareLooksup = []; - for (var i = 0; i < rounds.length; i++){ - shareLooksup.push(['hgetall', coin + '_shares:round' + rounds[i].height]); - } + var shareLookups = rounds.map(function(r){ + return ['hgetall', coin + '_shares:round' + r.height] + }); - - redisClient.multi(shareLooksup).exec(function(error, allWorkerShares){ + redisClient.multi(shareLookups).exec(function(error, allWorkerShares){ if (error){ callback('done - redis error with multi get rounds share') return; @@ -169,58 +162,68 @@ function SetupForPool(logger, poolOptions){ var workerRewards = {}; + for (var i = 0; i < rounds.length; i++){ var round = rounds[i]; var workerShares = allWorkerShares[i]; var reward = round.reward * (1 - processingConfig.feePercent); - var totalShares = 0; - for (var worker in workerShares){ - totalShares += parseInt(workerShares[worker]); - } + var totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); + for (var worker in workerShares){ - var singleWorkerShares = parseInt(workerShares[worker]); - var percent = singleWorkerShares / totalShares; - var workerRewardTotal = (reward * percent) / round.magnitude; - workerRewardTotal = Math.floor(workerRewardTotal * round.magnitude) / round.magnitude; - if (worker in workerRewards) - workerRewards[worker] += workerRewardTotal; - else - workerRewards[worker] = workerRewardTotal; + var percent = parseInt(workerShares[worker]) / totalShares; + var workerRewardTotal = Math.floor(reward * percent); + if (!(worker in workerRewards)) workerRewards[worker] = 0; + workerRewards[worker] += workerRewardTotal; } } + //this calculates profit if you wanna see it + /* + var workerTotalRewards = Object.keys(workerRewards).reduce(function(p, c){ + return p + workerRewards[c]; + }, 0); - console.dir(workerRewards); + var poolTotalRewards = rounds.reduce(function(p, c){ + return p + c.amount; + }, 0); - callback(null, rounds); + console.log(workerRewards); + console.log('pool profit percent' + ((poolTotalRewards - workerTotalRewards) / poolTotalRewards)); + */ + + callback(null, rounds, workerRewards); }); }, /* Does a batch call to redis to get worker existing balances from coin_balances*/ - function(rounds, callback){ - /* - var workerAddress = Object.keys(balancesForRounds); + function(rounds, workerRewards, callback){ - redisClient.hmget([coin + '_balances'].concat(workerAddress), function(error, results){ + var workers = Object.keys(workerRewards); + + redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){ if (error){ callback('done - redis error with multi get rounds share') return; } + console.dir(workerRewards); - for (var i = 0; i < results.length; i++){ - var shareInt = parseInt(results[i]); - if (shareInt) - balancesForRounds[workerAddress[i]] += shareInt; + var workerBalances = {}; + for (var i = 0; i < workers.length; i++){ + workerBalances[workers[i]] = parseInt(results[i]) || 0; } - callback(null, rounds, balancesForRounds) + console.dir(workerBalances); + + callback(null, rounds, workerRewards, workerBalances) }); - */ + }, @@ -230,13 +233,20 @@ function SetupForPool(logger, poolOptions){ when deciding the sent balance, it the difference should be -1*amount they had in db, if not sending the balance, the differnce should be +(the amount they earned this round) */ - function(fullBalance, rounds, callback){ + function(rounds, workerRewards, workerBalances, callback){ /* if payments dont succeed (likely because daemon isnt responding to rpc), then cancel here so that all of this can be tried again when the daemon is working. otherwise we will consider payment sent after we cleaned up the db. */ + /* In here do daemon.getbalance, figure out how many payments should be sent, see if the + remaining balance after payments-to-be sent is greater than the min reserver, otherwise + put everything in worker balances to be paid next time. + + + */ + }, diff --git a/pool_configs/litecoin_testnet_example.json b/pool_configs/litecoin_testnet_example.json index 2e9430c..86ee96c 100644 --- a/pool_configs/litecoin_testnet_example.json +++ b/pool_configs/litecoin_testnet_example.json @@ -8,9 +8,9 @@ "validateWorkerAddress": true, "paymentInterval": 10, "minimumPayment": 0.001, + "minimumReserve": 10, "feePercent": 0.02, "feeReceiveAddress": "LZz44iyF4zLCXJTU8RxztyyJZBntdS6fvv", - "minimumReserve": 10, "feeWithdrawalThreshold": 5, "daemon": { "host": "localhost", From 8d2d4fe423c923268dff7b698f299455c2838739 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 12 Mar 2014 22:57:04 -0600 Subject: [PATCH 09/18] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9892a7e..14579bc 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ Usage #### Requirements -* Coin daemon(s) -* [Node.js](http://nodejs.org/) v0.10+ -* [Redis](http://redis.io/) key-value store/database v2.6+ +* Coin daemon(s) (find the coin's repo and build latest version from source) +* [Node.js](http://nodejs.org/) v0.10+ ([follow these installation instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager)) +* [Redis](http://redis.io/) key-value store v2.6+ ([follow these instructions](http://redis.io/topics/quickstart)) #### 1) Download From 6899186dc353ef7c0fa7cbdcef65c01489cb6a23 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 13 Mar 2014 00:37:27 -0600 Subject: [PATCH 10/18] Some work on website/api --- config.json | 5 +++ init.js | 25 ++++++++++++++- libs/{apis.js => api.js} | 2 +- libs/paymentProcessor.js | 3 +- libs/shareProcessor.js | 14 ++++++-- libs/website.js | 69 ++++++++++++++++++++++++++++++++++++---- package.json | 4 ++- 7 files changed, 107 insertions(+), 15 deletions(-) rename libs/{apis.js => api.js} (89%) diff --git a/config.json b/config.json index 2a2fdfd..c1c566f 100644 --- a/config.json +++ b/config.json @@ -7,5 +7,10 @@ "enabled": true, "port": 8117, "password": "test" + }, + "website": { + "enabled": true, + "port": 80, + "liveStats": true } } \ No newline at end of file diff --git a/init.js b/init.js index 1a57431..e9f524b 100644 --- a/init.js +++ b/init.js @@ -9,6 +9,7 @@ var BlocknotifyListener = require('./libs/blocknotifyListener.js'); var WorkerListener = require('./libs/workerListener.js'); var PoolWorker = require('./libs/poolWorker.js'); var PaymentProcessor = require('./libs/paymentProcessor.js'); +var Website = require('./libs/website.js'); JSON.minify = JSON.minify || require("node-json-minify"); @@ -48,6 +49,9 @@ if (cluster.isWorker){ case 'paymentProcessor': new PaymentProcessor(loggerInstance); break; + case 'website': + new Website(loggerInstance); + break; } return; @@ -145,7 +149,24 @@ var startPaymentProcessor = function(poolConfigs){ worker.on('exit', function(code, signal){ logError('paymentProcessor', 'system', 'Payment processor died, spawning replacement...'); setTimeout(function(){ - startPaymentProcessor(poolConfigs); + startPaymentProcessor.apply(null, arguments); + }, 2000); + }); +}; + + +var startWebsite = function(portalConfig, poolConfigs){ + if (!portalConfig.website.enabled) return; + + var worker = cluster.fork({ + workerType: 'website', + pools: JSON.stringify(poolConfigs), + portalConfig: JSON.stringify(portalConfig) + }); + worker.on('exit', function(code, signal){ + logError('website', 'system', 'Website process died, spawning replacement...'); + setTimeout(function(){ + startWebsite.apply(null, arguments); }, 2000); }); }; @@ -165,4 +186,6 @@ var startPaymentProcessor = function(poolConfigs){ startWorkerListener(poolConfigs); + startWebsite(portalConfig, poolConfigs); + })(); \ No newline at end of file diff --git a/libs/apis.js b/libs/api.js similarity index 89% rename from libs/apis.js rename to libs/api.js index 966fc12..da646f8 100644 --- a/libs/apis.js +++ b/libs/api.js @@ -5,7 +5,7 @@ var app = express(); app.get('/getstatus', function (req, res) { res.send({ 'loadavg': os.loadavg(), - 'freemem': os.freemem(), + 'freemem': os.freemem() }); }); diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 17b859d..8952eda 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -211,7 +211,7 @@ function SetupForPool(logger, poolOptions){ callback('done - redis error with multi get rounds share') return; } - console.dir(workerRewards); + var workerBalances = {}; @@ -219,7 +219,6 @@ function SetupForPool(logger, poolOptions){ workerBalances[workers[i]] = parseInt(results[i]) || 0; } - console.dir(workerBalances); callback(null, rounds, workerRewards, workerBalances) }); diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index 620f6d5..ca9c055 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -19,9 +19,6 @@ module.exports = function(logger, poolConfig){ var redisConfig = internalConfig.redis; var coin = poolConfig.coin.name; - - - var connection; function connect(){ @@ -47,6 +44,13 @@ module.exports = function(logger, poolConfig){ connect(); + //Every 10 minutes clear out old hashrate stat data from redis + setInterval(function(){ + var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10); + connection.zremrangebyscore([coin + '_hashrate', '-inf', tenMinutesAgo]); + }, 10 * 60 * 1000); + + this.handleShare = function(isValidShare, isValidBlock, shareData){ @@ -58,6 +62,10 @@ module.exports = function(logger, poolConfig){ for more efficient stats */ + //store share diff, worker, and unique value with a score that is the timestamp + //unique value ensures it doesnt overwrite an existing entry + //the timestamp as score lets us query shares from last X minutes to generate hashrate for each worker and pool + connection.zadd(coin + '_hashrate', Date.now() / 1000 | 0, shareData.difficulty + ':' + shareData.worker + ':' + Math.random()); connection.hincrby([coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty], function(error, result){ if (error) diff --git a/libs/website.js b/libs/website.js index 068bf73..635f3d6 100644 --- a/libs/website.js +++ b/libs/website.js @@ -1,12 +1,67 @@ /* TODO -listen on port 80 for requests, maybe use express. -read website folder files into memory, and use fs.watch to reload changes to any files into memory -on some interval, apply a templating process to it with the latest api stats. on http requests, serve -this templated file and the other resources in memory. +Need to condense the entire website into a single html page. Embedding the javascript and css is easy. For images, +hopefully we can only use svg which can be embedded - otherwise we can convert the image into a data-url that can +be embedded, Favicon can also be a data-url which some javascript kungfu can display in browser. I'm focusing on +this mainly to help mitigate ddos and other kinds of attacks - and to just have a badass blazing fast project. -ideally, all css/js should be included in the html file (try to avoid images, uses embeddable svg) -this would give us one file to have to serve +Don't worry about doing any of that condensing yourself - go head and keep all the resources as separate files. +I will write a script for when the server starts to read all the files in the /website folder and minify and condense +it all together into one file, saved in memory. We will have 1 persistent condensed file that servers as our "template" +file that contains things like: +
Hashrate: {{=stats.hashrate}
- */ \ No newline at end of file +And then on some caching interval (maybe 5 seconds?) we will apply the template engine to generate the real html page +that we serve and hold in in memory - this is the file we serve to seo-bots (googlebot) and users when they first load +the page. + +Once the user loads the page we will have server-side event source connected to the portal api where it receives +updated stats on some interval (probably 5 seconds like template cache updater) and applies the changes to the already +displayed page. + +We will use fs.watch to detect changes to anything in the /website folder and update our stuff in memory. + + */ + + +var dot = require('dot'); +var express = require('express'); + + +module.exports = function(logger){ + + var portalConfig = JSON.parse(process.env.portalConfig); + var poolConfigs = JSON.parse(process.env.pools); + + var logIdentify = 'Website'; + + var websiteLogger = { + debug: function(key, text){ + logger.logDebug(logIdentify, key, text); + }, + warning: function(key, text){ + logger.logWarning(logIdentify, key, text); + }, + error: function(key, text){ + logger.logError(logIdentify, key, text); + } + }; + + var app = express(); + + app.get('/', function(req, res){ + res.send('hello'); + }); + + app.use(function(err, req, res, next){ + console.error(err.stack); + res.send(500, 'Something broke!'); + }); + + app.listen(portalConfig.website.port, function(){ + websiteLogger.debug('system', 'Website started on port ' + portalConfig.website.port); + }); + + +}; \ No newline at end of file diff --git a/package.json b/package.json index e5610f1..cf62501 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "posix": "*", "redis": "*", "mysql": "*", - "async": "*" + "async": "*", + "express": "*", + "dot": "*" }, "engines": { "node": ">=0.10" From 94c643a99ffc59f01c41a3643b37fca077437aef Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 13 Mar 2014 15:02:50 -0600 Subject: [PATCH 11/18] Update README.md --- README.md | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 14579bc..6d09500 100644 --- a/README.md +++ b/README.md @@ -61,19 +61,47 @@ git clone https://github.com/zone117x/node-stratum-portal.git npm update ``` -#### 2) Setup +#### 2) Configuration ##### Portal config -Inside the `config.json` file, ensure the default configuration will work for your environment. The `clustering.forks` -option is set to `"auto"` by default which will spawn one process/fork/worker for each CPU core in your system. -Each of these workers will run a separate instance of your pool(s), and the kernel will load balance miners -using these forks. Optionally, the `clustering.forks` field can be a number for how many forks you wish to spawn. +Inside the `config.json` file, ensure the default configuration will work for your environment. -With `blockNotifyListener` enabled, the master process will start listening on the configured port for messages from -the `scripts/blockNotify.js` script which your coin daemons can be configured to run when a new block is available. -When a blocknotify message is received, the master process uses IPC (inter-process communication) to notify each -worker process about the message. Each worker process then sends the message to the appropriate coin pool. -See "Setting up blocknotify" below to set up your daemon to use this feature. +Explanation for each field: +````javascript +{ + /* Specifies the level of log output verbosity. Anything more severy than the level specified + will also be logged. */ + "logLevel": "debug", //or "warning", "error" + + /* By default 'forks' is set to "auto" which will spawn one process/fork/worker for each CPU + core in your system. Each of these workers will run a separate instance of your pool(s), + and the kernel will load balance miners using these forks. Optionally, the 'forks' field + can be a number for how many forks will be spawned. */ + "clustering": { + "enabled": true, + "forks": "auto" + }, + + /* With this enabled, the master process will start listening on the configured port for + messages from the 'scripts/blockNotify.js' script which your coin daemons can be configured + to run when a new block is available. When a blocknotify message is received, the master + process uses IPC (inter-process communication) to notify each worker process about the + message. Each worker process then sends the message to the appropriate coin pool. See + "Setting up blocknotify" below to set up your daemon to use this feature. */ + "blockNotifyListener": { + "enabled": true, + "port": 8117, + "password": "test" + }, + + /* This is the front-end. Its not finished. When it is finished, this comment will say so. */ + "website": { + "enabled": true, + "port": 80, + "liveStats": true + } +} +```` ##### Coin config From 0edfbdf0e1fd4a0b5262621f240ac0967a478001 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 13 Mar 2014 15:03:28 -0600 Subject: [PATCH 12/18] Added logLevel to portal config --- config.json | 1 + init.js | 11 +++-------- libs/api.js | 24 ++++++++++------------- libs/{logutils.js => logUtil.js} | 33 ++++++++++++-------------------- libs/paymentProcessor.js | 2 +- libs/shareProcessor.js | 6 ------ libs/website.js | 1 + 7 files changed, 28 insertions(+), 50 deletions(-) rename libs/{logutils.js => logUtil.js} (63%) diff --git a/config.json b/config.json index c1c566f..a790452 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,5 @@ { + "logLevel": "debug", "clustering": { "enabled": true, "forks": "auto" diff --git a/init.js b/init.js index e9f524b..bbaefbc 100644 --- a/init.js +++ b/init.js @@ -4,7 +4,7 @@ var cluster = require('cluster'); var posix = require('posix'); -var PoolLogger = require('./libs/logutils.js'); +var PoolLogger = require('./libs/logUtil.js'); var BlocknotifyListener = require('./libs/blocknotifyListener.js'); var WorkerListener = require('./libs/workerListener.js'); var PoolWorker = require('./libs/poolWorker.js'); @@ -14,15 +14,11 @@ var Website = require('./libs/website.js'); JSON.minify = JSON.minify || require("node-json-minify"); +var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'}))); var loggerInstance = new PoolLogger({ - 'default': true, - 'keys': { - //'client' : 'warning', - 'system' : true, - 'submitblock' : true - } + logLevel: portalConfig.logLevel }); var logDebug = loggerInstance.logDebug; @@ -174,7 +170,6 @@ var startWebsite = function(portalConfig, poolConfigs){ (function init(){ - var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'}))); var poolConfigs = buildPoolConfigs(); diff --git a/libs/api.js b/libs/api.js index da646f8..9fcac7d 100644 --- a/libs/api.js +++ b/libs/api.js @@ -1,18 +1,14 @@ -var express = require('express'); var os = require('os'); -var app = express(); - -app.get('/getstatus', function (req, res) { - res.send({ - 'loadavg': os.loadavg(), - 'freemem': os.freemem() - }); -}); -module.exports = { - start: function () { - app.listen(9000); - } -} +module.exports = function(logger, poolConfigs){ + + //Every 10 minutes clear out old hashrate stat data from redis + setInterval(function(){ + var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10); + connection.zremrangebyscore([coin + '_hashrate', '-inf', tenMinutesAgo]); + }, 10 * 60 * 1000); + + +}; diff --git a/libs/logutils.js b/libs/logUtil.js similarity index 63% rename from libs/logutils.js rename to libs/logUtil.js index 5ef890d..134d118 100644 --- a/libs/logutils.js +++ b/libs/logUtil.js @@ -39,35 +39,26 @@ var getSeverityColor = function(severity) { var PoolLogger = function (configuration) { + var logLevelInt = severityToInt(configuration.logLevel); + // privates var shouldLog = function(key, severity) { - var keyValue = configuration.keys[key]; - if (typeof(keyValue) === 'undefined') { - keyValue = configuration.default; - } - - if (typeof(keyValue) === 'boolean') { - return keyValue; - } else if (typeof(keyValue) === 'string') { - return severityToInt(severity) >= severityToInt(keyValue); - } - } + var severity = severityToInt(severity); + return severity >= logLevelInt; + }; var log = function(severity, key, poolName, text) { - if ( ! shouldLog(key, severity) ) { - // if this tag is set to not be logged or the default value is false then drop it! - //console.log(key+"DROPPED "+text + 'SEV' + severity); + if (!shouldLog(key, severity)) return; - } var desc = poolName ? '[' + poolName + '] ' : ''; console.log( - '\u001b['+getSeverityColor(severity)+'m' + - dateFormat(new Date(), 'yyyy-mm-dd HH:mm:ss') + - " ["+key+"]" + '\u001b[39m: ' + "\t" + - desc + - text); - } + '\u001b[' + getSeverityColor(severity) + 'm' + + dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') + + " [" + key + "]" + '\u001b[39m: ' + "\t" + + desc + text + ); + }; // public diff --git a/libs/paymentProcessor.js b/libs/paymentProcessor.js index 8952eda..b9b9d6e 100644 --- a/libs/paymentProcessor.js +++ b/libs/paymentProcessor.js @@ -189,7 +189,7 @@ function SetupForPool(logger, poolOptions){ }, 0); var poolTotalRewards = rounds.reduce(function(p, c){ - return p + c.amount; + return p + c.amount * c.magnitude; }, 0); console.log(workerRewards); diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index ca9c055..dd58848 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -44,12 +44,6 @@ module.exports = function(logger, poolConfig){ connect(); - //Every 10 minutes clear out old hashrate stat data from redis - setInterval(function(){ - var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10); - connection.zremrangebyscore([coin + '_hashrate', '-inf', tenMinutesAgo]); - }, 10 * 60 * 1000); - this.handleShare = function(isValidShare, isValidBlock, shareData){ diff --git a/libs/website.js b/libs/website.js index 635f3d6..c4fd2c0 100644 --- a/libs/website.js +++ b/libs/website.js @@ -29,6 +29,7 @@ var dot = require('dot'); var express = require('express'); + module.exports = function(logger){ var portalConfig = JSON.parse(process.env.portalConfig); From c05abc9da7e728d43626f2b0cd9209fe5cdc00e7 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 13 Mar 2014 15:09:54 -0600 Subject: [PATCH 13/18] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d09500..979f718 100644 --- a/README.md +++ b/README.md @@ -315,8 +315,11 @@ blocknotify="scripts/blockNotify.js localhost:8117 mySuperSecurePassword dogecoi node init.js ``` -Optionally, use something like [forever](https://github.com/nodejitsu/forever) to keep the node script running +###### Optional enhancements for your awesome new mining pool server setup: +* Use something like [forever](https://github.com/nodejitsu/forever) to keep the node script running in case the master process crashes. +* Use something like [redis-commander](https://github.com/joeferner/redis-commander) to have a nice GUI +for exploring your redis database. Donations From d707284b74ca7959fd0effbd01610e3064af3101 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 13 Mar 2014 15:13:20 -0600 Subject: [PATCH 14/18] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 979f718..07d5250 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,8 @@ node init.js in case the master process crashes. * Use something like [redis-commander](https://github.com/joeferner/redis-commander) to have a nice GUI for exploring your redis database. +* Use something like [logrotator](http://www.thegeekstuff.com/2010/07/logrotate-examples/) to rotate log +output from NOMP. Donations From 6dff8b20cd5a5dc2160572b476e01c745f957e9a Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 13 Mar 2014 15:26:54 -0600 Subject: [PATCH 15/18] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 07d5250..48c39a2 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,14 @@ of this software. The switching can be controlled using a coin profitability API -#### Community +#### Community / Support For support and general discussion join IRC #nomp: https://webchat.freenode.net/?channels=#nomp For development discussion join #nomp-dev: https://webchat.freenode.net/?channels=#nomp-dev +If you're having a problem getting the portal running due to some module dependency error - its probably because you +didn't follow the instructions in this README. Please __read the usage instructions__ including [requirements](#requirements) and [downloading/installing](#1-downloading--installing). If you've followed the instructions completely and are still having problems then open an issue here on github or join our #nomp IRC channel and explain your problem :). + If your pool uses NOMP let us know and we will list your website here. @@ -52,7 +55,7 @@ Usage * [Redis](http://redis.io/) key-value store v2.6+ ([follow these instructions](http://redis.io/topics/quickstart)) -#### 1) Download +#### 1) Downloading & Installing Clone the repository and run `npm update` for all the dependencies to be installed: From 8f5dbe997a4ca8c9be5edfa629359a20d8257f74 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Thu, 13 Mar 2014 15:30:33 -0600 Subject: [PATCH 16/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48c39a2..876a09a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ For support and general discussion join IRC #nomp: https://webchat.freenode.net/ For development discussion join #nomp-dev: https://webchat.freenode.net/?channels=#nomp-dev -If you're having a problem getting the portal running due to some module dependency error - its probably because you +*Having problems getting the portal running due to some module dependency error?* It's probably because you didn't follow the instructions in this README. Please __read the usage instructions__ including [requirements](#requirements) and [downloading/installing](#1-downloading--installing). If you've followed the instructions completely and are still having problems then open an issue here on github or join our #nomp IRC channel and explain your problem :). If your pool uses NOMP let us know and we will list your website here. From 6e41473f018103311dcbd8cb35887894e7474514 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 13 Mar 2014 17:20:57 -0600 Subject: [PATCH 17/18] More efficient share processing wtih redis --- libs/shareProcessor.js | 49 +++++++++++++++++++++++------------------- website/index.html | 16 ++++++++++++++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/libs/shareProcessor.js b/libs/shareProcessor.js index dd58848..6b9844c 100644 --- a/libs/shareProcessor.js +++ b/libs/shareProcessor.js @@ -48,33 +48,38 @@ module.exports = function(logger, poolConfig){ this.handleShare = function(isValidShare, isValidBlock, shareData){ - if (!isValidShare) return; + var redisCommands = []; - /*use http://redis.io/commands/zrangebyscore to store shares with timestamps - so we can use the min-max to get shares from the last x minutes to determine hash rate :) - also use a hash like coin_stats:{ invalidShares, validShares, invalidBlocks, validBlocks, etc } - for more efficient stats - */ + if (isValidShare){ + redisCommands.push(['hincrby', coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty]); + redisCommands.push(['hincrby', coin + '_stats', 'validShares', 1]); - //store share diff, worker, and unique value with a score that is the timestamp - //unique value ensures it doesnt overwrite an existing entry - //the timestamp as score lets us query shares from last X minutes to generate hashrate for each worker and pool - connection.zadd(coin + '_hashrate', Date.now() / 1000 | 0, shareData.difficulty + ':' + shareData.worker + ':' + Math.random()); - - connection.hincrby([coin + '_shares:roundCurrent', shareData.worker, shareData.difficulty], function(error, result){ - if (error) - logger.error('redis', 'Could not store worker share') - }); + /* Stores share diff, worker, and unique value with a score that is the timestamp. Unique value ensures it + doesn't overwrite an existing entry, and timestamp as score lets us query shares from last X minutes to + generate hashrate for each worker and pool. */ + redisCommands.push(['zadd', coin + '_hashrate', Date.now() / 1000 | 0, [shareData.difficulty, shareData.worker, Math.random()].join(':')]); + } + else{ + redisCommands.push(['hincrby', coin + '_stats', 'invalidShares', 1]); + } if (isValidBlock){ - connection.rename(coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height, function(result){ - console.log('rename result: ' + result); - }); - connection.sadd([coin + '_blocks', shareData.tx + ':' + shareData.height + ':' + shareData.reward], function(error, result){ - if (error) - logger.error('redis', 'Could not store block data'); - }); + redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]); + redisCommands.push(['sadd', coin + '_blocks', shareData.tx + ':' + shareData.height + ':' + shareData.reward]); + redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]); } + else if (shareData.solution){ + redisCommands.push(['hincrby', coin + '_stats', 'invalidBlocks', 1]); + } + + connection.multi(redisCommands).exec(function(err, replies){ + if (err) + console.log('error with share processor multi ' + JSON.stringify(err)); + else{ + console.log(JSON.stringify(replies)); + } + }); + }; diff --git a/website/index.html b/website/index.html index e69de29..7affeff 100644 --- a/website/index.html +++ b/website/index.html @@ -0,0 +1,16 @@ + + + + + + + Title + + + + + + + + + \ No newline at end of file From 0db53a296f9b77ad6ff76b5f06c7156d5366a777 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 14 Mar 2014 01:18:51 -0600 Subject: [PATCH 18/18] Work on front-end --- libs/api.js | 45 +++++++++++++++++++--- libs/website.js | 89 +++++++++++++++++++++++++++++++++++++++++++- website/Chart.min.js | 39 +++++++++++++++++++ website/index.html | 6 ++- website/main.js | 0 website/pure-min.css | 11 ++++++ website/style.css | 0 website/zepto.min.js | 2 + 8 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 website/Chart.min.js create mode 100644 website/main.js create mode 100644 website/pure-min.css create mode 100644 website/style.css create mode 100644 website/zepto.min.js diff --git a/libs/api.js b/libs/api.js index 9fcac7d..53ec46c 100644 --- a/libs/api.js +++ b/libs/api.js @@ -1,14 +1,49 @@ +var redis = require('redis'); var os = require('os'); module.exports = function(logger, poolConfigs){ - //Every 10 minutes clear out old hashrate stat data from redis - setInterval(function(){ - var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10); - connection.zremrangebyscore([coin + '_hashrate', '-inf', tenMinutesAgo]); - }, 10 * 60 * 1000); + var redisClients = []; + Object.keys(poolConfigs).forEach(function(coin){ + var poolConfig = poolConfigs[coin]; + var internalConfig = poolConfig.shareProcessing.internal; + var redisConfig = internalConfig.redis; + for (var i = 0; i < redisClients.length; i++){ + var client = redisClients[i]; + if (client.client.port === redisConfig.port && client.client.host === redisConfig.host){ + client.coins.push(coin); + return; + } + } + redisClients.push({ + coins: [coin], + client: redis.createClient(redisConfig.port, redisConfig.host) + }); + }); + + //Every 10 minutes clear out old hashrate stats for each coin from redis + var clearExpiredHashrates = function(){ + redisClients.forEach(function(client){ + var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10); + var redisCommands = client.coins.map(function(coin){ + return ['zremrangebyscore', coin + '_hashrate', '-inf', tenMinutesAgo]; + }); + client.client.multi(redisCommands).exec(function(err, replies){ + if (err) + console.log('error with clearing old hashrates ' + JSON.stringify(err)); + }); + }); + }; + setInterval(clearExpiredHashrates, 10 * 60 * 1000); + clearExpiredHashrates(); + + this.getStats = function(callback){ + + //get stats like hashrate and in/valid shares/blocks and workers in current round + + }; }; diff --git a/libs/website.js b/libs/website.js index c4fd2c0..17031ff 100644 --- a/libs/website.js +++ b/libs/website.js @@ -24,10 +24,14 @@ We will use fs.watch to detect changes to anything in the /website folder and up */ +var fs = require('fs'); +var path = require('path'); +var async = require('async'); var dot = require('dot'); var express = require('express'); +var api = require('./api.js'); module.exports = function(logger){ @@ -35,6 +39,9 @@ module.exports = function(logger){ var portalConfig = JSON.parse(process.env.portalConfig); var poolConfigs = JSON.parse(process.env.pools); + + var portalApi = new api(logger, poolConfigs); + var logIdentify = 'Website'; var websiteLogger = { @@ -49,10 +56,90 @@ module.exports = function(logger){ } }; + var pageResources = ''; + var pageTemplate; + var pageProcessed = ''; + + var loadWebPage = function(callback){ + fs.readdir('website', function(err, files){ + async.map(files, function(fileName, callback){ + var filePath = 'website/' + fileName; + fs.readFile(filePath, 'utf8', function(err, data){ + callback(null, {name: fileName, data: data, ext: path.extname(filePath)}); + }); + }, function(err, fileObjects){ + + var indexPage = fileObjects.filter(function(f){ + return f.name === 'index.html'; + })[0].data; + + var jsCode = ''; + cssCode += ''; + + var bodyIndex = indexPage.indexOf(''); + pageTemplate = dot.template(indexPage.slice(bodyIndex)); + + + pageResources = indexPage.slice(0, bodyIndex); + var headIndex = pageResources.indexOf(''); + pageResources = pageResources.slice(0, headIndex) + + jsCode + '\n\n\n\n' + + cssCode + '\n\n\n\n' + + pageResources.slice(headIndex); + + applyTemplateInfo(); + callback || function(){}(); + }) + }); + }; + + loadWebPage(); + + var applyTemplateInfo = function(){ + + portalApi.getStats(function(stats){ + + //need to give template info about pools and stats + + pageProcessed = pageTemplate({test: 'visitor', time: Date.now()}); + }); + }; + + setInterval(function(){ + applyTemplateInfo(); + }, 5000); + + + var reloadTimeout; + fs.watch('website', function(){ + clearTimeout(reloadTimeout); + reloadTimeout = setTimeout(function(){ + loadWebPage(); + }, 500); + }); + + var app = express(); + //need to create a stats api endpoint for eventsource live stat updates which are triggered on the applytemplateinfo interval + + + + app.get('/', function(req, res){ - res.send('hello'); + res.send(pageResources + pageProcessed); }); app.use(function(err, req, res, next){ diff --git a/website/Chart.min.js b/website/Chart.min.js new file mode 100644 index 0000000..ab63588 --- /dev/null +++ b/website/Chart.min.js @@ -0,0 +1,39 @@ +var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= +Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& +isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? +b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? +0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== +a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* +(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* +a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, +scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", +animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", +scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, +c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, +onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, +pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", +scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); +d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; +h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< +h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= +Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0Title - + +
Hello {{=it.test}}!
+
The unix time at page generation is {{=it.time}}
+
Need to build a pretty page here...
+ \ No newline at end of file diff --git a/website/main.js b/website/main.js new file mode 100644 index 0000000..e69de29 diff --git a/website/pure-min.css b/website/pure-min.css new file mode 100644 index 0000000..420907c --- /dev/null +++ b/website/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v0.4.2 +Copyright 2014 Yahoo! Inc. All rights reserved. +Licensed under the BSD License. +https://github.com/yui/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v1.1.3 | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v1.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}[hidden]{display:none!important}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-g-r{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g-r{word-spacing:-.43em}.pure-g-r [class *="pure-u"]{font-family:sans-serif}.pure-g-r img{max-width:100%;height:auto}@media (min-width:980px){.pure-visible-phone{display:none}.pure-visible-tablet{display:none}.pure-hidden-desktop{display:none}}@media (max-width:480px){.pure-g-r>.pure-u,.pure-g-r>[class *="pure-u-"]{width:100%}}@media (max-width:767px){.pure-g-r>.pure-u,.pure-g-r>[class *="pure-u-"]{width:100%}.pure-hidden-phone{display:none}.pure-visible-desktop{display:none}}@media (min-width:768px) and (max-width:979px){.pure-hidden-tablet{display:none}.pure-visible-desktop{display:none}}.pure-button{display:inline-block;*display:inline;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;*font-size:90%;*overflow:visible;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);*color:#444;border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin dotted #333;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#ee5f5b}.pure-form input:focus:invalid:focus,.pure-form textarea:focus:invalid:focus,.pure-form select:focus:invalid:focus{border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 10em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input{display:block;padding:10px;margin:0;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus{z-index:2}.pure-form .pure-group input:first-child{top:1px;border-radius:4px 4px 0 0}.pure-form .pure-group input:last-child{top:-2px;border-radius:0 0 4px 4px}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu ul{position:absolute;visibility:hidden}.pure-menu.pure-menu-open{visibility:visible;z-index:2;width:100%}.pure-menu ul{left:-10000px;list-style:none;margin:0;padding:0;top:-10000px;z-index:1}.pure-menu>ul{position:relative}.pure-menu-open>ul{left:0;top:0;visibility:visible}.pure-menu-open>ul:focus{outline:0}.pure-menu li{position:relative}.pure-menu a,.pure-menu .pure-menu-heading{display:block;color:inherit;line-height:1.5em;padding:5px 20px;text-decoration:none;white-space:nowrap}.pure-menu.pure-menu-horizontal>.pure-menu-heading{display:inline-block;*display:inline;zoom:1;margin:0;vertical-align:middle}.pure-menu.pure-menu-horizontal>ul{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu li a{padding:5px 20px}.pure-menu-can-have-children>.pure-menu-label:after{content:'\25B8';float:right;font-family:'Lucida Grande','Lucida Sans Unicode','DejaVu Sans',sans-serif;margin-right:-20px;margin-top:-1px}.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-separator{background-color:#dfdfdf;display:block;height:1px;font-size:0;margin:7px 2px;overflow:hidden}.pure-menu-hidden{display:none}.pure-menu-fixed{position:fixed;top:0;left:0;width:100%}.pure-menu-horizontal li{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-horizontal li li{display:block}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label:after{content:"\25BE"}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-horizontal li.pure-menu-separator{height:50%;width:1px;margin:0 7px}.pure-menu-horizontal li li.pure-menu-separator{height:1px;width:auto;margin:7px 2px}.pure-menu.pure-menu-open,.pure-menu.pure-menu-horizontal li .pure-menu-children{background:#fff;border:1px solid #b7b7b7}.pure-menu.pure-menu-horizontal,.pure-menu.pure-menu-horizontal .pure-menu-heading{border:0}.pure-menu a{border:1px solid transparent;border-left:0;border-right:0}.pure-menu a,.pure-menu .pure-menu-can-have-children>li:after{color:#777}.pure-menu .pure-menu-can-have-children>li:hover:after{color:#fff}.pure-menu .pure-menu-open{background:#dedede}.pure-menu li a:hover,.pure-menu li a:focus{background:#eee}.pure-menu li.pure-menu-disabled a:hover,.pure-menu li.pure-menu-disabled a:focus{background:#fff;color:#bfbfbf}.pure-menu .pure-menu-disabled>a{background-image:none;border-color:transparent;cursor:default}.pure-menu .pure-menu-disabled>a,.pure-menu .pure-menu-can-have-children.pure-menu-disabled>a:after{color:#bfbfbf}.pure-menu .pure-menu-heading{color:#565d64;text-transform:uppercase;font-size:90%;margin-top:.5em;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#dfdfdf}.pure-menu .pure-menu-selected a{color:#000}.pure-menu.pure-menu-open.pure-menu-fixed{border:0;border-bottom:1px solid #b7b7b7}.pure-paginator{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;list-style:none;margin:0;padding:0}.opera-only :-o-prefocus,.pure-paginator{word-spacing:-.43em}.pure-paginator li{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-paginator .pure-button{border-radius:0;padding:.8em 1.4em;vertical-align:top;height:1.1em}.pure-paginator .pure-button:focus,.pure-paginator .pure-button:active{outline-style:none}.pure-paginator .prev,.pure-paginator .next{color:#C0C1C3;text-shadow:0 -1px 0 rgba(0,0,0,.45)}.pure-paginator .prev{border-radius:2px 0 0 2px}.pure-paginator .next{border-radius:0 2px 2px 0}@media (max-width:480px){.pure-menu-horizontal{width:100%}.pure-menu-children li{display:block;border-bottom:1px solid #000}}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:6px 12px}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child td,.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0} \ No newline at end of file diff --git a/website/style.css b/website/style.css new file mode 100644 index 0000000..e69de29 diff --git a/website/zepto.min.js b/website/zepto.min.js new file mode 100644 index 0000000..0b2f97a --- /dev/null +++ b/website/zepto.min.js @@ -0,0 +1,2 @@ +/* Zepto v1.1.3 - zepto event ajax form ie - zeptojs.com/license */ +var Zepto=function(){function L(t){return null==t?String(t):j[T.call(t)]||"object"}function Z(t){return"function"==L(t)}function $(t){return null!=t&&t==t.window}function _(t){return null!=t&&t.nodeType==t.DOCUMENT_NODE}function D(t){return"object"==L(t)}function R(t){return D(t)&&!$(t)&&Object.getPrototypeOf(t)==Object.prototype}function M(t){return"number"==typeof t.length}function k(t){return s.call(t,function(t){return null!=t})}function z(t){return t.length>0?n.fn.concat.apply([],t):t}function F(t){return t.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function q(t){return t in f?f[t]:f[t]=new RegExp("(^|\\s)"+t+"(\\s|$)")}function H(t,e){return"number"!=typeof e||c[F(t)]?e:e+"px"}function I(t){var e,n;return u[t]||(e=a.createElement(t),a.body.appendChild(e),n=getComputedStyle(e,"").getPropertyValue("display"),e.parentNode.removeChild(e),"none"==n&&(n="block"),u[t]=n),u[t]}function V(t){return"children"in t?o.call(t.children):n.map(t.childNodes,function(t){return 1==t.nodeType?t:void 0})}function U(n,i,r){for(e in i)r&&(R(i[e])||A(i[e]))?(R(i[e])&&!R(n[e])&&(n[e]={}),A(i[e])&&!A(n[e])&&(n[e]=[]),U(n[e],i[e],r)):i[e]!==t&&(n[e]=i[e])}function B(t,e){return null==e?n(t):n(t).filter(e)}function J(t,e,n,i){return Z(e)?e.call(t,n,i):e}function X(t,e,n){null==n?t.removeAttribute(e):t.setAttribute(e,n)}function W(e,n){var i=e.className,r=i&&i.baseVal!==t;return n===t?r?i.baseVal:i:void(r?i.baseVal=n:e.className=n)}function Y(t){var e;try{return t?"true"==t||("false"==t?!1:"null"==t?null:/^0/.test(t)||isNaN(e=Number(t))?/^[\[\{]/.test(t)?n.parseJSON(t):t:e):t}catch(i){return t}}function G(t,e){e(t);for(var n in t.childNodes)G(t.childNodes[n],e)}var t,e,n,i,C,N,r=[],o=r.slice,s=r.filter,a=window.document,u={},f={},c={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},l=/^\s*<(\w+|!)[^>]*>/,h=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,p=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,d=/^(?:body|html)$/i,m=/([A-Z])/g,g=["val","css","html","text","data","width","height","offset"],v=["after","prepend","before","append"],y=a.createElement("table"),x=a.createElement("tr"),b={tr:a.createElement("tbody"),tbody:y,thead:y,tfoot:y,td:x,th:x,"*":a.createElement("div")},w=/complete|loaded|interactive/,E=/^[\w-]*$/,j={},T=j.toString,S={},O=a.createElement("div"),P={tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},A=Array.isArray||function(t){return t instanceof Array};return S.matches=function(t,e){if(!e||!t||1!==t.nodeType)return!1;var n=t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.matchesSelector;if(n)return n.call(t,e);var i,r=t.parentNode,o=!r;return o&&(r=O).appendChild(t),i=~S.qsa(r,e).indexOf(t),o&&O.removeChild(t),i},C=function(t){return t.replace(/-+(.)?/g,function(t,e){return e?e.toUpperCase():""})},N=function(t){return s.call(t,function(e,n){return t.indexOf(e)==n})},S.fragment=function(e,i,r){var s,u,f;return h.test(e)&&(s=n(a.createElement(RegExp.$1))),s||(e.replace&&(e=e.replace(p,"<$1>")),i===t&&(i=l.test(e)&&RegExp.$1),i in b||(i="*"),f=b[i],f.innerHTML=""+e,s=n.each(o.call(f.childNodes),function(){f.removeChild(this)})),R(r)&&(u=n(s),n.each(r,function(t,e){g.indexOf(t)>-1?u[t](e):u.attr(t,e)})),s},S.Z=function(t,e){return t=t||[],t.__proto__=n.fn,t.selector=e||"",t},S.isZ=function(t){return t instanceof S.Z},S.init=function(e,i){var r;if(!e)return S.Z();if("string"==typeof e)if(e=e.trim(),"<"==e[0]&&l.test(e))r=S.fragment(e,RegExp.$1,i),e=null;else{if(i!==t)return n(i).find(e);r=S.qsa(a,e)}else{if(Z(e))return n(a).ready(e);if(S.isZ(e))return e;if(A(e))r=k(e);else if(D(e))r=[e],e=null;else if(l.test(e))r=S.fragment(e.trim(),RegExp.$1,i),e=null;else{if(i!==t)return n(i).find(e);r=S.qsa(a,e)}}return S.Z(r,e)},n=function(t,e){return S.init(t,e)},n.extend=function(t){var e,n=o.call(arguments,1);return"boolean"==typeof t&&(e=t,t=n.shift()),n.forEach(function(n){U(t,n,e)}),t},S.qsa=function(t,e){var n,i="#"==e[0],r=!i&&"."==e[0],s=i||r?e.slice(1):e,a=E.test(s);return _(t)&&a&&i?(n=t.getElementById(s))?[n]:[]:1!==t.nodeType&&9!==t.nodeType?[]:o.call(a&&!i?r?t.getElementsByClassName(s):t.getElementsByTagName(e):t.querySelectorAll(e))},n.contains=function(t,e){return t!==e&&t.contains(e)},n.type=L,n.isFunction=Z,n.isWindow=$,n.isArray=A,n.isPlainObject=R,n.isEmptyObject=function(t){var e;for(e in t)return!1;return!0},n.inArray=function(t,e,n){return r.indexOf.call(e,t,n)},n.camelCase=C,n.trim=function(t){return null==t?"":String.prototype.trim.call(t)},n.uuid=0,n.support={},n.expr={},n.map=function(t,e){var n,r,o,i=[];if(M(t))for(r=0;r=0?e:e+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){null!=this.parentNode&&this.parentNode.removeChild(this)})},each:function(t){return r.every.call(this,function(e,n){return t.call(e,n,e)!==!1}),this},filter:function(t){return Z(t)?this.not(this.not(t)):n(s.call(this,function(e){return S.matches(e,t)}))},add:function(t,e){return n(N(this.concat(n(t,e))))},is:function(t){return this.length>0&&S.matches(this[0],t)},not:function(e){var i=[];if(Z(e)&&e.call!==t)this.each(function(t){e.call(this,t)||i.push(this)});else{var r="string"==typeof e?this.filter(e):M(e)&&Z(e.item)?o.call(e):n(e);this.forEach(function(t){r.indexOf(t)<0&&i.push(t)})}return n(i)},has:function(t){return this.filter(function(){return D(t)?n.contains(this,t):n(this).find(t).size()})},eq:function(t){return-1===t?this.slice(t):this.slice(t,+t+1)},first:function(){var t=this[0];return t&&!D(t)?t:n(t)},last:function(){var t=this[this.length-1];return t&&!D(t)?t:n(t)},find:function(t){var e,i=this;return e="object"==typeof t?n(t).filter(function(){var t=this;return r.some.call(i,function(e){return n.contains(e,t)})}):1==this.length?n(S.qsa(this[0],t)):this.map(function(){return S.qsa(this,t)})},closest:function(t,e){var i=this[0],r=!1;for("object"==typeof t&&(r=n(t));i&&!(r?r.indexOf(i)>=0:S.matches(i,t));)i=i!==e&&!_(i)&&i.parentNode;return n(i)},parents:function(t){for(var e=[],i=this;i.length>0;)i=n.map(i,function(t){return(t=t.parentNode)&&!_(t)&&e.indexOf(t)<0?(e.push(t),t):void 0});return B(e,t)},parent:function(t){return B(N(this.pluck("parentNode")),t)},children:function(t){return B(this.map(function(){return V(this)}),t)},contents:function(){return this.map(function(){return o.call(this.childNodes)})},siblings:function(t){return B(this.map(function(t,e){return s.call(V(e.parentNode),function(t){return t!==e})}),t)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(t){return n.map(this,function(e){return e[t]})},show:function(){return this.each(function(){"none"==this.style.display&&(this.style.display=""),"none"==getComputedStyle(this,"").getPropertyValue("display")&&(this.style.display=I(this.nodeName))})},replaceWith:function(t){return this.before(t).remove()},wrap:function(t){var e=Z(t);if(this[0]&&!e)var i=n(t).get(0),r=i.parentNode||this.length>1;return this.each(function(o){n(this).wrapAll(e?t.call(this,o):r?i.cloneNode(!0):i)})},wrapAll:function(t){if(this[0]){n(this[0]).before(t=n(t));for(var e;(e=t.children()).length;)t=e.first();n(t).append(this)}return this},wrapInner:function(t){var e=Z(t);return this.each(function(i){var r=n(this),o=r.contents(),s=e?t.call(this,i):t;o.length?o.wrapAll(s):r.append(s)})},unwrap:function(){return this.parent().each(function(){n(this).replaceWith(n(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(e){return this.each(function(){var i=n(this);(e===t?"none"==i.css("display"):e)?i.show():i.hide()})},prev:function(t){return n(this.pluck("previousElementSibling")).filter(t||"*")},next:function(t){return n(this.pluck("nextElementSibling")).filter(t||"*")},html:function(t){return 0===arguments.length?this.length>0?this[0].innerHTML:null:this.each(function(e){var i=this.innerHTML;n(this).empty().append(J(this,t,e,i))})},text:function(e){return 0===arguments.length?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=e===t?"":""+e})},attr:function(n,i){var r;return"string"==typeof n&&i===t?0==this.length||1!==this[0].nodeType?t:"value"==n&&"INPUT"==this[0].nodeName?this.val():!(r=this[0].getAttribute(n))&&n in this[0]?this[0][n]:r:this.each(function(t){if(1===this.nodeType)if(D(n))for(e in n)X(this,e,n[e]);else X(this,n,J(this,i,t,this.getAttribute(n)))})},removeAttr:function(t){return this.each(function(){1===this.nodeType&&X(this,t)})},prop:function(e,n){return e=P[e]||e,n===t?this[0]&&this[0][e]:this.each(function(t){this[e]=J(this,n,t,this[e])})},data:function(e,n){var i=this.attr("data-"+e.replace(m,"-$1").toLowerCase(),n);return null!==i?Y(i):t},val:function(t){return 0===arguments.length?this[0]&&(this[0].multiple?n(this[0]).find("option").filter(function(){return this.selected}).pluck("value"):this[0].value):this.each(function(e){this.value=J(this,t,e,this.value)})},offset:function(t){if(t)return this.each(function(e){var i=n(this),r=J(this,t,e,i.offset()),o=i.offsetParent().offset(),s={top:r.top-o.top,left:r.left-o.left};"static"==i.css("position")&&(s.position="relative"),i.css(s)});if(0==this.length)return null;var e=this[0].getBoundingClientRect();return{left:e.left+window.pageXOffset,top:e.top+window.pageYOffset,width:Math.round(e.width),height:Math.round(e.height)}},css:function(t,i){if(arguments.length<2){var r=this[0],o=getComputedStyle(r,"");if(!r)return;if("string"==typeof t)return r.style[C(t)]||o.getPropertyValue(t);if(A(t)){var s={};return n.each(A(t)?t:[t],function(t,e){s[e]=r.style[C(e)]||o.getPropertyValue(e)}),s}}var a="";if("string"==L(t))i||0===i?a=F(t)+":"+H(t,i):this.each(function(){this.style.removeProperty(F(t))});else for(e in t)t[e]||0===t[e]?a+=F(e)+":"+H(e,t[e])+";":this.each(function(){this.style.removeProperty(F(e))});return this.each(function(){this.style.cssText+=";"+a})},index:function(t){return t?this.indexOf(n(t)[0]):this.parent().children().indexOf(this[0])},hasClass:function(t){return t?r.some.call(this,function(t){return this.test(W(t))},q(t)):!1},addClass:function(t){return t?this.each(function(e){i=[];var r=W(this),o=J(this,t,e,r);o.split(/\s+/g).forEach(function(t){n(this).hasClass(t)||i.push(t)},this),i.length&&W(this,r+(r?" ":"")+i.join(" "))}):this},removeClass:function(e){return this.each(function(n){return e===t?W(this,""):(i=W(this),J(this,e,n,i).split(/\s+/g).forEach(function(t){i=i.replace(q(t)," ")}),void W(this,i.trim()))})},toggleClass:function(e,i){return e?this.each(function(r){var o=n(this),s=J(this,e,r,W(this));s.split(/\s+/g).forEach(function(e){(i===t?!o.hasClass(e):i)?o.addClass(e):o.removeClass(e)})}):this},scrollTop:function(e){if(this.length){var n="scrollTop"in this[0];return e===t?n?this[0].scrollTop:this[0].pageYOffset:this.each(n?function(){this.scrollTop=e}:function(){this.scrollTo(this.scrollX,e)})}},scrollLeft:function(e){if(this.length){var n="scrollLeft"in this[0];return e===t?n?this[0].scrollLeft:this[0].pageXOffset:this.each(n?function(){this.scrollLeft=e}:function(){this.scrollTo(e,this.scrollY)})}},position:function(){if(this.length){var t=this[0],e=this.offsetParent(),i=this.offset(),r=d.test(e[0].nodeName)?{top:0,left:0}:e.offset();return i.top-=parseFloat(n(t).css("margin-top"))||0,i.left-=parseFloat(n(t).css("margin-left"))||0,r.top+=parseFloat(n(e[0]).css("border-top-width"))||0,r.left+=parseFloat(n(e[0]).css("border-left-width"))||0,{top:i.top-r.top,left:i.left-r.left}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent||a.body;t&&!d.test(t.nodeName)&&"static"==n(t).css("position");)t=t.offsetParent;return t})}},n.fn.detach=n.fn.remove,["width","height"].forEach(function(e){var i=e.replace(/./,function(t){return t[0].toUpperCase()});n.fn[e]=function(r){var o,s=this[0];return r===t?$(s)?s["inner"+i]:_(s)?s.documentElement["scroll"+i]:(o=this.offset())&&o[e]:this.each(function(t){s=n(this),s.css(e,J(this,r,t,s[e]()))})}}),v.forEach(function(t,e){var i=e%2;n.fn[t]=function(){var t,o,r=n.map(arguments,function(e){return t=L(e),"object"==t||"array"==t||null==e?e:S.fragment(e)}),s=this.length>1;return r.length<1?this:this.each(function(t,a){o=i?a:a.parentNode,a=0==e?a.nextSibling:1==e?a.firstChild:2==e?a:null,r.forEach(function(t){if(s)t=t.cloneNode(!0);else if(!o)return n(t).remove();G(o.insertBefore(t,a),function(t){null==t.nodeName||"SCRIPT"!==t.nodeName.toUpperCase()||t.type&&"text/javascript"!==t.type||t.src||window.eval.call(window,t.innerHTML)})})})},n.fn[i?t+"To":"insert"+(e?"Before":"After")]=function(e){return n(e)[t](this),this}}),S.Z.prototype=n.fn,S.uniq=N,S.deserializeValue=Y,n.zepto=S,n}();window.Zepto=Zepto,void 0===window.$&&(window.$=Zepto),function(t){function l(t){return t._zid||(t._zid=e++)}function h(t,e,n,i){if(e=p(e),e.ns)var r=d(e.ns);return(s[l(t)]||[]).filter(function(t){return!(!t||e.e&&t.e!=e.e||e.ns&&!r.test(t.ns)||n&&l(t.fn)!==l(n)||i&&t.sel!=i)})}function p(t){var e=(""+t).split(".");return{e:e[0],ns:e.slice(1).sort().join(" ")}}function d(t){return new RegExp("(?:^| )"+t.replace(" "," .* ?")+"(?: |$)")}function m(t,e){return t.del&&!u&&t.e in f||!!e}function g(t){return c[t]||u&&f[t]||t}function v(e,i,r,o,a,u,f){var h=l(e),d=s[h]||(s[h]=[]);i.split(/\s/).forEach(function(i){if("ready"==i)return t(document).ready(r);var s=p(i);s.fn=r,s.sel=a,s.e in c&&(r=function(e){var n=e.relatedTarget;return!n||n!==this&&!t.contains(this,n)?s.fn.apply(this,arguments):void 0}),s.del=u;var l=u||r;s.proxy=function(t){if(t=j(t),!t.isImmediatePropagationStopped()){t.data=o;var i=l.apply(e,t._args==n?[t]:[t].concat(t._args));return i===!1&&(t.preventDefault(),t.stopPropagation()),i}},s.i=d.length,d.push(s),"addEventListener"in e&&e.addEventListener(g(s.e),s.proxy,m(s,f))})}function y(t,e,n,i,r){var o=l(t);(e||"").split(/\s/).forEach(function(e){h(t,e,n,i).forEach(function(e){delete s[o][e.i],"removeEventListener"in t&&t.removeEventListener(g(e.e),e.proxy,m(e,r))})})}function j(e,i){return(i||!e.isDefaultPrevented)&&(i||(i=e),t.each(E,function(t,n){var r=i[t];e[t]=function(){return this[n]=x,r&&r.apply(i,arguments)},e[n]=b}),(i.defaultPrevented!==n?i.defaultPrevented:"returnValue"in i?i.returnValue===!1:i.getPreventDefault&&i.getPreventDefault())&&(e.isDefaultPrevented=x)),e}function T(t){var e,i={originalEvent:t};for(e in t)w.test(e)||t[e]===n||(i[e]=t[e]);return j(i,t)}var n,e=1,i=Array.prototype.slice,r=t.isFunction,o=function(t){return"string"==typeof t},s={},a={},u="onfocusin"in window,f={focus:"focusin",blur:"focusout"},c={mouseenter:"mouseover",mouseleave:"mouseout"};a.click=a.mousedown=a.mouseup=a.mousemove="MouseEvents",t.event={add:v,remove:y},t.proxy=function(e,n){if(r(e)){var i=function(){return e.apply(n,arguments)};return i._zid=l(e),i}if(o(n))return t.proxy(e[n],e);throw new TypeError("expected function")},t.fn.bind=function(t,e,n){return this.on(t,e,n)},t.fn.unbind=function(t,e){return this.off(t,e)},t.fn.one=function(t,e,n,i){return this.on(t,e,n,i,1)};var x=function(){return!0},b=function(){return!1},w=/^([A-Z]|returnValue$|layer[XY]$)/,E={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};t.fn.delegate=function(t,e,n){return this.on(e,t,n)},t.fn.undelegate=function(t,e,n){return this.off(e,t,n)},t.fn.live=function(e,n){return t(document.body).delegate(this.selector,e,n),this},t.fn.die=function(e,n){return t(document.body).undelegate(this.selector,e,n),this},t.fn.on=function(e,s,a,u,f){var c,l,h=this;return e&&!o(e)?(t.each(e,function(t,e){h.on(t,s,a,e,f)}),h):(o(s)||r(u)||u===!1||(u=a,a=s,s=n),(r(a)||a===!1)&&(u=a,a=n),u===!1&&(u=b),h.each(function(n,r){f&&(c=function(t){return y(r,t.type,u),u.apply(this,arguments)}),s&&(l=function(e){var n,o=t(e.target).closest(s,r).get(0);return o&&o!==r?(n=t.extend(T(e),{currentTarget:o,liveFired:r}),(c||u).apply(o,[n].concat(i.call(arguments,1)))):void 0}),v(r,e,u,a,s,l||c)}))},t.fn.off=function(e,i,s){var a=this;return e&&!o(e)?(t.each(e,function(t,e){a.off(t,i,e)}),a):(o(i)||r(s)||s===!1||(s=i,i=n),s===!1&&(s=b),a.each(function(){y(this,e,s,i)}))},t.fn.trigger=function(e,n){return e=o(e)||t.isPlainObject(e)?t.Event(e):j(e),e._args=n,this.each(function(){"dispatchEvent"in this?this.dispatchEvent(e):t(this).triggerHandler(e,n)})},t.fn.triggerHandler=function(e,n){var i,r;return this.each(function(s,a){i=T(o(e)?t.Event(e):e),i._args=n,i.target=a,t.each(h(a,e.type||e),function(t,e){return r=e.proxy(i),i.isImmediatePropagationStopped()?!1:void 0})}),r},"focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(e){t.fn[e]=function(t){return t?this.bind(e,t):this.trigger(e)}}),["focus","blur"].forEach(function(e){t.fn[e]=function(t){return t?this.bind(e,t):this.each(function(){try{this[e]()}catch(t){}}),this}}),t.Event=function(t,e){o(t)||(e=t,t=e.type);var n=document.createEvent(a[t]||"Events"),i=!0;if(e)for(var r in e)"bubbles"==r?i=!!e[r]:n[r]=e[r];return n.initEvent(t,i,!0),j(n)}}(Zepto),function(t){function l(e,n,i){var r=t.Event(n);return t(e).trigger(r,i),!r.isDefaultPrevented()}function h(t,e,i,r){return t.global?l(e||n,i,r):void 0}function p(e){e.global&&0===t.active++&&h(e,null,"ajaxStart")}function d(e){e.global&&!--t.active&&h(e,null,"ajaxStop")}function m(t,e){var n=e.context;return e.beforeSend.call(n,t,e)===!1||h(e,n,"ajaxBeforeSend",[t,e])===!1?!1:void h(e,n,"ajaxSend",[t,e])}function g(t,e,n,i){var r=n.context,o="success";n.success.call(r,t,o,e),i&&i.resolveWith(r,[t,o,e]),h(n,r,"ajaxSuccess",[e,n,t]),y(o,e,n)}function v(t,e,n,i,r){var o=i.context;i.error.call(o,n,e,t),r&&r.rejectWith(o,[n,e,t]),h(i,o,"ajaxError",[n,i,t||e]),y(e,n,i)}function y(t,e,n){var i=n.context;n.complete.call(i,e,t),h(n,i,"ajaxComplete",[e,n]),d(n)}function x(){}function b(t){return t&&(t=t.split(";",2)[0]),t&&(t==f?"html":t==u?"json":s.test(t)?"script":a.test(t)&&"xml")||"text"}function w(t,e){return""==e?t:(t+"&"+e).replace(/[&?]{1,2}/,"?")}function E(e){e.processData&&e.data&&"string"!=t.type(e.data)&&(e.data=t.param(e.data,e.traditional)),!e.data||e.type&&"GET"!=e.type.toUpperCase()||(e.url=w(e.url,e.data),e.data=void 0)}function j(e,n,i,r){return t.isFunction(n)&&(r=i,i=n,n=void 0),t.isFunction(i)||(r=i,i=void 0),{url:e,data:n,success:i,dataType:r}}function S(e,n,i,r){var o,s=t.isArray(n),a=t.isPlainObject(n);t.each(n,function(n,u){o=t.type(u),r&&(n=i?r:r+"["+(a||"object"==o||"array"==o?n:"")+"]"),!r&&s?e.add(u.name,u.value):"array"==o||!i&&"object"==o?S(e,u,i,n):e.add(n,u)})}var i,r,e=0,n=window.document,o=/)<[^<]*)*<\/script>/gi,s=/^(?:text|application)\/javascript/i,a=/^(?:text|application)\/xml/i,u="application/json",f="text/html",c=/^\s*$/;t.active=0,t.ajaxJSONP=function(i,r){if(!("type"in i))return t.ajax(i);var f,h,o=i.jsonpCallback,s=(t.isFunction(o)?o():o)||"jsonp"+ ++e,a=n.createElement("script"),u=window[s],c=function(e){t(a).triggerHandler("error",e||"abort")},l={abort:c};return r&&r.promise(l),t(a).on("load error",function(e,n){clearTimeout(h),t(a).off().remove(),"error"!=e.type&&f?g(f[0],l,i,r):v(null,n||"error",l,i,r),window[s]=u,f&&t.isFunction(u)&&u(f[0]),u=f=void 0}),m(l,i)===!1?(c("abort"),l):(window[s]=function(){f=arguments},a.src=i.url.replace(/\?(.+)=\?/,"?$1="+s),n.head.appendChild(a),i.timeout>0&&(h=setTimeout(function(){c("timeout")},i.timeout)),l)},t.ajaxSettings={type:"GET",beforeSend:x,success:x,error:x,complete:x,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript, application/x-javascript",json:u,xml:"application/xml, text/xml",html:f,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0},t.ajax=function(e){var n=t.extend({},e||{}),o=t.Deferred&&t.Deferred();for(i in t.ajaxSettings)void 0===n[i]&&(n[i]=t.ajaxSettings[i]);p(n),n.crossDomain||(n.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(n.url)&&RegExp.$2!=window.location.host),n.url||(n.url=window.location.toString()),E(n),n.cache===!1&&(n.url=w(n.url,"_="+Date.now()));var s=n.dataType,a=/\?.+=\?/.test(n.url);if("jsonp"==s||a)return a||(n.url=w(n.url,n.jsonp?n.jsonp+"=?":n.jsonp===!1?"":"callback=?")),t.ajaxJSONP(n,o);var j,u=n.accepts[s],f={},l=function(t,e){f[t.toLowerCase()]=[t,e]},h=/^([\w-]+:)\/\//.test(n.url)?RegExp.$1:window.location.protocol,d=n.xhr(),y=d.setRequestHeader;if(o&&o.promise(d),n.crossDomain||l("X-Requested-With","XMLHttpRequest"),l("Accept",u||"*/*"),(u=n.mimeType||u)&&(u.indexOf(",")>-1&&(u=u.split(",",2)[0]),d.overrideMimeType&&d.overrideMimeType(u)),(n.contentType||n.contentType!==!1&&n.data&&"GET"!=n.type.toUpperCase())&&l("Content-Type",n.contentType||"application/x-www-form-urlencoded"),n.headers)for(r in n.headers)l(r,n.headers[r]);if(d.setRequestHeader=l,d.onreadystatechange=function(){if(4==d.readyState){d.onreadystatechange=x,clearTimeout(j);var e,i=!1;if(d.status>=200&&d.status<300||304==d.status||0==d.status&&"file:"==h){s=s||b(n.mimeType||d.getResponseHeader("content-type")),e=d.responseText;try{"script"==s?(1,eval)(e):"xml"==s?e=d.responseXML:"json"==s&&(e=c.test(e)?null:t.parseJSON(e))}catch(r){i=r}i?v(i,"parsererror",d,n,o):g(e,d,n,o)}else v(d.statusText||null,d.status?"error":"abort",d,n,o)}},m(d,n)===!1)return d.abort(),v(null,"abort",d,n,o),d;if(n.xhrFields)for(r in n.xhrFields)d[r]=n.xhrFields[r];var T="async"in n?n.async:!0;d.open(n.type,n.url,T,n.username,n.password);for(r in f)y.apply(d,f[r]);return n.timeout>0&&(j=setTimeout(function(){d.onreadystatechange=x,d.abort(),v(null,"timeout",d,n,o)},n.timeout)),d.send(n.data?n.data:null),d},t.get=function(){return t.ajax(j.apply(null,arguments))},t.post=function(){var e=j.apply(null,arguments);return e.type="POST",t.ajax(e)},t.getJSON=function(){var e=j.apply(null,arguments);return e.dataType="json",t.ajax(e)},t.fn.load=function(e,n,i){if(!this.length)return this;var a,r=this,s=e.split(/\s/),u=j(e,n,i),f=u.success;return s.length>1&&(u.url=s[0],a=s[1]),u.success=function(e){r.html(a?t("
").html(e.replace(o,"")).find(a):e),f&&f.apply(r,arguments)},t.ajax(u),this};var T=encodeURIComponent;t.param=function(t,e){var n=[];return n.add=function(t,e){this.push(T(t)+"="+T(e))},S(n,t,e),n.join("&").replace(/%20/g,"+")}}(Zepto),function(t){t.fn.serializeArray=function(){var n,e=[];return t([].slice.call(this.get(0).elements)).each(function(){n=t(this);var i=n.attr("type");"fieldset"!=this.nodeName.toLowerCase()&&!this.disabled&&"submit"!=i&&"reset"!=i&&"button"!=i&&("radio"!=i&&"checkbox"!=i||this.checked)&&e.push({name:n.attr("name"),value:n.val()})}),e},t.fn.serialize=function(){var t=[];return this.serializeArray().forEach(function(e){t.push(encodeURIComponent(e.name)+"="+encodeURIComponent(e.value))}),t.join("&")},t.fn.submit=function(e){if(e)this.bind("submit",e);else if(this.length){var n=t.Event("submit");this.eq(0).trigger(n),n.isDefaultPrevented()||this.get(0).submit()}return this}}(Zepto),function(t){"__proto__"in{}||t.extend(t.zepto,{Z:function(e,n){return e=e||[],t.extend(e,t.fn),e.selector=n||"",e.__Z=!0,e},isZ:function(e){return"array"===t.type(e)&&"__Z"in e}});try{getComputedStyle(void 0)}catch(e){var n=getComputedStyle;window.getComputedStyle=function(t){try{return n(t)}catch(e){return null}}}}(Zepto);