From f181a9d925bc3d54616d6720ef7520e6963e70e3 Mon Sep 17 00:00:00 2001 From: Sammy Libre Date: Tue, 6 Dec 2016 19:25:07 +0500 Subject: [PATCH] Add initial Web-UI --- config.example.json | 81 ++++++++-------- go-pool/pool/pool.go | 32 ++++--- go-pool/stratum/api.go | 115 +++++++++++++++++++++++ go-pool/stratum/blocks.go | 2 +- go-pool/stratum/handlers.go | 5 + go-pool/stratum/miner.go | 48 ++++++++++ go-pool/stratum/stratum.go | 22 +++-- main.go | 20 ++++ www/handlebars-intl.min.js | 2 + www/index.html | 182 ++++++++++++++++++++++++++++++++++++ www/robots.txt | 2 + www/script.js | 38 ++++++++ www/style.css | 98 +++++++++++++++++++ 13 files changed, 590 insertions(+), 57 deletions(-) create mode 100644 go-pool/stratum/api.go create mode 100644 www/handlebars-intl.min.js create mode 100644 www/index.html create mode 100644 www/robots.txt create mode 100644 www/script.js create mode 100644 www/style.css diff --git a/config.example.json b/config.example.json index 967a280..cad1b09 100644 --- a/config.example.json +++ b/config.example.json @@ -1,44 +1,51 @@ { - "address": "46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em", - "bypassAddressValidation": true, - "bypassShareValidation": true, + "address": "46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em", + "bypassAddressValidation": true, + "bypassShareValidation": true, - "threads": 2, + "threads": 2, - "stratum": { - "timeout": "15m", - "blockRefreshInterval": "1s", + "stratum": { + "timeout": "15m", + "blockRefreshInterval": "1s", - "listen": [ - { - "host": "0.0.0.0", - "port": 1111, - "diff": 8000, - "maxConn": 32768 - }, - { - "host": "0.0.0.0", - "port": 3333, - "diff": 16000, - "maxConn": 32768 - }, - { - "host": "0.0.0.0", - "port": 5555, - "diff": 16000, - "maxConn": 32768 - } - ] - }, + "listen": [ + { + "host": "0.0.0.0", + "port": 1111, + "diff": 8000, + "maxConn": 32768 + }, + { + "host": "0.0.0.0", + "port": 3333, + "diff": 16000, + "maxConn": 32768 + }, + { + "host": "0.0.0.0", + "port": 5555, + "diff": 16000, + "maxConn": 32768 + } + ] + }, - "daemon": { - "host": "127.0.0.1", - "port": 18081, - "timeout": "10s" - }, + "frontend": { + "listen": "0.0.0.0:8082", + "login": "admin", + "password": "", + "hideIP": false + }, - "newrelicEnabled": false, - "newrelicName": "MyStratum", - "newrelicKey": "SECRET_KEY", - "newrelicVerbose": false + "daemon": { + "host": "127.0.0.1", + "port": 18081, + "timeout": "10s" + }, + + "newrelicEnabled": false, + "newrelicName": "MyStratum", + "newrelicKey": "SECRET_KEY", + "newrelicVerbose": false } diff --git a/go-pool/pool/pool.go b/go-pool/pool/pool.go index eb7e011..cf3cb2f 100644 --- a/go-pool/pool/pool.go +++ b/go-pool/pool/pool.go @@ -1,18 +1,19 @@ package pool type Config struct { - Address string `json:"address"` - BypassAddressValidation bool `json:"bypassAddressValidation"` - BypassShareValidation bool `json:"bypassShareValidation"` - Stratum Stratum `json:"stratum"` - Daemon Daemon `json:"daemon"` - - Threads int `json:"threads"` - - NewrelicName string `json:"newrelicName"` - NewrelicKey string `json:"newrelicKey"` - NewrelicVerbose bool `json:"newrelicVerbose"` - NewrelicEnabled bool `json:"newrelicEnabled"` + Address string `json:"address"` + BypassAddressValidation bool `json:"bypassAddressValidation"` + BypassShareValidation bool `json:"bypassShareValidation"` + Stratum Stratum `json:"stratum"` + Daemon Daemon `json:"daemon"` + LuckWindow string `json:"luckWindow"` + LargeLuckWindow string `json:"largeLuckWindow"` + Threads int `json:"threads"` + Frontend Frontend `json:"frontend"` + NewrelicName string `json:"newrelicName"` + NewrelicKey string `json:"newrelicKey"` + NewrelicVerbose bool `json:"newrelicVerbose"` + NewrelicEnabled bool `json:"newrelicEnabled"` } type Stratum struct { @@ -33,3 +34,10 @@ type Daemon struct { Port int `json:"port"` Timeout string `json:"timeout"` } + +type Frontend struct { + Listen string `json:"listen"` + Login string `json:"login"` + Password string `json:"password"` + HideIP bool `json:"hideIP"` +} diff --git a/go-pool/stratum/api.go b/go-pool/stratum/api.go new file mode 100644 index 0000000..c020c02 --- /dev/null +++ b/go-pool/stratum/api.go @@ -0,0 +1,115 @@ +package stratum + +import ( + "encoding/json" + "net/http" + "sync/atomic" + "time" + + "../util" +) + +func (s *StratumServer) StatsIndex(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + + hashrate, hashrate24h, totalOnline, miners := s.collectMinersStats() + stats := map[string]interface{}{ + "miners": miners, + "hashrate": hashrate, + "hashrate24h": hashrate24h, + "totalMiners": len(miners), + "totalOnline": totalOnline, + "timedOut": len(miners) - totalOnline, + "now": util.MakeTimestamp(), + } + + stats["luck"] = s.getLuckStats() + + if t := s.currentBlockTemplate(); t != nil { + stats["height"] = t.Height + stats["diff"] = t.Difficulty + roundShares := atomic.LoadInt64(&s.roundShares) + stats["variance"] = roundShares / t.Difficulty + stats["template"] = true + } + json.NewEncoder(w).Encode(stats) +} + +func (s *StratumServer) collectMinersStats() (float64, float64, int, []interface{}) { + now := util.MakeTimestamp() + var result []interface{} + totalhashrate := float64(0) + totalhashrate24h := float64(0) + totalOnline := 0 + window24h := 24 * time.Hour + + for m := range s.miners.Iter() { + stats := make(map[string]interface{}) + lastBeat := m.Val.getLastBeat() + hashrate := m.Val.hashrate(s.estimationWindow) + hashrate24h := m.Val.hashrate(window24h) + totalhashrate += hashrate + totalhashrate24h += hashrate24h + stats["name"] = m.Key + stats["hashrate"] = hashrate + stats["hashrate24h"] = hashrate24h + stats["lastBeat"] = lastBeat + stats["validShares"] = atomic.LoadUint64(&m.Val.validShares) + stats["invalidShares"] = atomic.LoadUint64(&m.Val.invalidShares) + stats["accepts"] = atomic.LoadUint64(&m.Val.accepts) + stats["rejects"] = atomic.LoadUint64(&m.Val.rejects) + if !s.config.Frontend.HideIP { + stats["ip"] = m.Val.IP + } + + if now-lastBeat > (int64(s.timeout/2) / 1000000) { + stats["warning"] = true + } + if now-lastBeat > (int64(s.timeout) / 1000000) { + stats["timeout"] = true + } else { + totalOnline++ + } + result = append(result, stats) + } + return totalhashrate, totalhashrate24h, totalOnline, result +} + +func (s *StratumServer) getLuckStats() map[string]interface{} { + now := util.MakeTimestamp() + var variance float64 + var totalVariance float64 + var blocksCount int + var totalBlocksCount int + + s.blocksMu.Lock() + defer s.blocksMu.Unlock() + + for k, v := range s.blockStats { + if k >= now-int64(s.luckWindow) { + blocksCount++ + variance += v + } + if k >= now-int64(s.luckLargeWindow) { + totalBlocksCount++ + totalVariance += v + } else { + delete(s.blockStats, k) + } + } + if blocksCount != 0 { + variance = variance / float64(blocksCount) + } + if totalBlocksCount != 0 { + totalVariance = totalVariance / float64(totalBlocksCount) + } + result := make(map[string]interface{}) + result["variance"] = variance + result["blocksCount"] = blocksCount + result["window"] = s.config.LuckWindow + result["totalVariance"] = totalVariance + result["totalBlocksCount"] = totalBlocksCount + result["largeWindow"] = s.config.LargeLuckWindow + return result +} diff --git a/go-pool/stratum/blocks.go b/go-pool/stratum/blocks.go index 3b6a0a0..015c048 100644 --- a/go-pool/stratum/blocks.go +++ b/go-pool/stratum/blocks.go @@ -37,7 +37,7 @@ func (s *StratumServer) fetchBlockTemplate() bool { } t := s.currentBlockTemplate() - if t.PrevHash == reply.PrevHash { + if t != nil && t.PrevHash == reply.PrevHash { // Fallback to height comparison if len(reply.PrevHash) == 0 && reply.Height > t.Height { log.Printf("New block to mine at height %v, diff: %v", reply.Height, reply.Difficulty) diff --git a/go-pool/stratum/handlers.go b/go-pool/stratum/handlers.go index d477526..0275155 100644 --- a/go-pool/stratum/handlers.go +++ b/go-pool/stratum/handlers.go @@ -4,6 +4,7 @@ import ( "log" "regexp" "strings" + "sync/atomic" "../util" ) @@ -57,17 +58,20 @@ func (s *StratumServer) handleSubmitRPC(cs *Session, e *Endpoint, params *Submit job := miner.findJob(params.JobId) if job == nil { errorReply = &ErrorReply{Code: -1, Message: "Invalid job id", Close: true} + atomic.AddUint64(&miner.invalidShares, 1) return } if !noncePattern.MatchString(params.Nonce) { errorReply = &ErrorReply{Code: -1, Message: "Malformed nonce", Close: true} + atomic.AddUint64(&miner.invalidShares, 1) return } nonce := strings.ToLower(params.Nonce) exist := job.submit(nonce) if exist { errorReply = &ErrorReply{Code: -1, Message: "Duplicate share", Close: true} + atomic.AddUint64(&miner.invalidShares, 1) return } @@ -75,6 +79,7 @@ func (s *StratumServer) handleSubmitRPC(cs *Session, e *Endpoint, params *Submit if job.Height != t.Height { log.Printf("Block expired for height %v %s@%s", job.Height, miner.Login, miner.IP) errorReply = &ErrorReply{Code: -1, Message: "Block expired", Close: false} + atomic.AddUint64(&miner.staleShares, 1) return } diff --git a/go-pool/stratum/miner.go b/go-pool/stratum/miner.go index 5f5372f..e75c552 100644 --- a/go-pool/stratum/miner.go +++ b/go-pool/stratum/miner.go @@ -7,6 +7,7 @@ import ( "log" "sync" "sync/atomic" + "time" "../../cnutil" "../../hashing" @@ -36,6 +37,14 @@ type Miner struct { LastBeat int64 Session *Session Endpoint *Endpoint + + startedAt int64 + validShares uint64 + invalidShares uint64 + staleShares uint64 + accepts uint64 + rejects uint64 + shares map[int64]int64 } func (job *Job) submit(nonce string) bool { @@ -90,6 +99,39 @@ func (m *Miner) heartbeat() { atomic.StoreInt64(&m.LastBeat, now) } +func (m *Miner) getLastBeat() int64 { + return atomic.LoadInt64(&m.LastBeat) +} + +func (m *Miner) storeShare(diff int64) { + now := util.MakeTimestamp() + m.Lock() + m.shares[now] += diff + m.Unlock() +} + +func (m *Miner) hashrate(hashrateWindow time.Duration) float64 { + now := util.MakeTimestamp() + totalShares := int64(0) + window := int64(hashrateWindow / time.Millisecond) + boundary := now - m.startedAt + + if boundary > window { + boundary = window + } + + m.Lock() + for k, v := range m.shares { + if k < now-86400000 { + delete(m.shares, k) + } else if k >= now-window { + totalShares += v + } + } + m.Unlock() + return float64(totalShares) / float64(boundary) +} + func (m *Miner) findJob(id string) *Job { m.RLock() defer m.RUnlock() @@ -124,14 +166,17 @@ func (m *Miner) processShare(s *StratumServer, e *Endpoint, job *Job, t *BlockTe if !s.config.BypassShareValidation && hex.EncodeToString(hashBytes) != result { log.Printf("Bad hash from miner %v@%v", m.Login, m.IP) + atomic.AddUint64(&m.invalidShares, 1) return false } hashDiff := util.GetHashDifficulty(hashBytes).Int64() // FIXME: Will return max int64 value if overflows + atomic.AddInt64(&s.roundShares, hashDiff) block := hashDiff >= t.Difficulty if block { _, err := s.rpc.SubmitBlock(hex.EncodeToString(shareBuff)) if err != nil { + atomic.AddUint64(&m.rejects, 1) log.Printf("Block submission failure at height %v: %v", t.Height, err) } else { if len(convertedBlob) == 0 { @@ -140,13 +185,16 @@ func (m *Miner) processShare(s *StratumServer, e *Endpoint, job *Job, t *BlockTe blockFastHash := hex.EncodeToString(hashing.FastHash(convertedBlob)) // Immediately refresh current BT and send new jobs s.refreshBlockTemplate(true) + atomic.AddUint64(&m.accepts, 1) log.Printf("Block %v found at height %v by miner %v@%v", blockFastHash[0:6], t.Height, m.Login, m.IP) } } else if hashDiff < job.Difficulty { log.Printf("Rejected low difficulty share of %v from %v@%v", hashDiff, m.Login, m.IP) + atomic.AddUint64(&m.invalidShares, 1) return false } log.Printf("Valid share at difficulty %v/%v", e.config.Difficulty, hashDiff) + atomic.AddUint64(&m.validShares, 1) return true } diff --git a/go-pool/stratum/stratum.go b/go-pool/stratum/stratum.go index 9506dbd..a58ed36 100644 --- a/go-pool/stratum/stratum.go +++ b/go-pool/stratum/stratum.go @@ -18,11 +18,17 @@ import ( ) type StratumServer struct { - config *pool.Config - miners MinersMap - blockTemplate atomic.Value - rpc *rpc.RPCClient - timeout time.Duration + config *pool.Config + miners MinersMap + blockTemplate atomic.Value + rpc *rpc.RPCClient + timeout time.Duration + estimationWindow time.Duration + blocksMu sync.RWMutex + blockStats map[int64]float64 + luckWindow int64 + luckLargeWindow int64 + roundShares int64 } type Endpoint struct { @@ -51,7 +57,6 @@ func NewStratum(cfg *pool.Config) *StratumServer { stratum.timeout = timeout // Init block template - stratum.blockTemplate.Store(&BlockTemplate{}) stratum.refreshBlockTemplate(false) refreshIntv, _ := time.ParseDuration(cfg.Stratum.BlockRefreshInterval) @@ -261,7 +266,10 @@ func (s *StratumServer) removeMiner(id string) { } func (s *StratumServer) currentBlockTemplate() *BlockTemplate { - return s.blockTemplate.Load().(*BlockTemplate) + if t := s.blockTemplate.Load(); t != nil { + return t.(*BlockTemplate) + } + return nil } func checkError(err error) { diff --git a/main.go b/main.go index 6e87ab5..452b5ba 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "log" "math/rand" + "net/http" "os" "path/filepath" "runtime" @@ -12,6 +13,8 @@ import ( "./go-pool/pool" "./go-pool/stratum" + "github.com/goji/httpauth" + "github.com/gorilla/mux" "github.com/yvasiyarov/gorelic" ) @@ -28,9 +31,26 @@ func startStratum() { } s := stratum.NewStratum(&cfg) + go startFrontend(&cfg, s) s.Listen() } +func startFrontend(cfg *pool.Config, s *stratum.StratumServer) { + r := mux.NewRouter() + r.HandleFunc("/stats", s.StatsIndex) + r.PathPrefix("/").Handler(http.FileServer(http.Dir("./www/"))) + var err error + if len(cfg.Frontend.Password) > 0 { + auth := httpauth.SimpleBasicAuth(cfg.Frontend.Login, cfg.Frontend.Password) + err = http.ListenAndServe(cfg.Frontend.Listen, auth(r)) + } else { + err = http.ListenAndServe(cfg.Frontend.Listen, r) + } + if err != nil { + log.Fatal(err) + } +} + func startNewrelic() { // Run NewRelic if cfg.NewrelicEnabled { diff --git a/www/handlebars-intl.min.js b/www/handlebars-intl.min.js new file mode 100644 index 0000000..c77fa71 --- /dev/null +++ b/www/handlebars-intl.min.js @@ -0,0 +1,2 @@ +(function(){"use strict";function a(a){var b,c,d,e,f=Array.prototype.slice.call(arguments,1);for(b=0,c=f.length;c>b;b+=1)if(d=f[b])for(e in d)p.call(d,e)&&(a[e]=d[e]);return a}function b(a,b,c){this.locales=a,this.formats=b,this.pluralFn=c}function c(a){this.id=a}function d(a,b,c,d,e){this.id=a,this.useOrdinal=b,this.offset=c,this.options=d,this.pluralFn=e}function e(a,b,c,d){this.id=a,this.offset=b,this.numberFormat=c,this.string=d}function f(a,b){this.id=a,this.options=b}function g(a,b,c){var d="string"==typeof a?g.__parse(a):a;if(!d||"messageFormatPattern"!==d.type)throw new TypeError("A message must be provided as a String or AST.");c=this._mergeFormats(g.formats,c),r(this,"_locale",{value:this._resolveLocale(b)});var e=this._findPluralRuleFunction(this._locale),f=this._compilePattern(d,b,c,e),h=this;this.format=function(a){return h._format(f,a)}}function h(a){return 400*a/146097}function i(a,b){b=b||{},G(a)&&(a=a.concat()),D(this,"_locale",{value:this._resolveLocale(a)}),D(this,"_options",{value:{style:this._resolveStyle(b.style),units:this._isValidUnits(b.units)&&b.units}}),D(this,"_locales",{value:a}),D(this,"_fields",{value:this._findFields(this._locale)}),D(this,"_messages",{value:E(null)});var c=this;this.format=function(a,b){return c._format(a,b)}}function j(a){var b=Q(null);return function(){var c=Array.prototype.slice.call(arguments),d=k(c),e=d&&b[d];return e||(e=Q(a.prototype),a.apply(e,c),d&&(b[d]=e)),e}}function k(a){if("undefined"!=typeof JSON){var b,c,d,e=[];for(b=0,c=a.length;c>b;b+=1)d=a[b],e.push(d&&"object"==typeof d?l(d):d);return JSON.stringify(e)}}function l(a){var b,c,d,e,f=[],g=[];for(b in a)a.hasOwnProperty(b)&&g.push(b);var h=g.sort();for(c=0,d=h.length;d>c;c+=1)b=h[c],e={},e[b]=a[b],f[c]=e;return f}function m(a){var b,c,d,e,f=Array.prototype.slice.call(arguments,1);for(b=0,c=f.length;c>b;b+=1)if(d=f[b])for(e in d)d.hasOwnProperty(e)&&(a[e]=d[e]);return a}function n(a){function b(a,b){return function(){return"undefined"!=typeof console&&"function"==typeof console.warn&&console.warn("{{"+a+"}} is deprecated, use: {{"+b.name+"}}"),b.apply(this,arguments)}}function c(a){if(!a.fn)throw new Error("{{#intl}} must be invoked as a block helper");var b=p(a.data),c=m({},b.intl,a.hash);return b.intl=c,a.fn(this,{data:b})}function d(a,b){var c,d,e,f=b.data&&b.data.intl,g=a.split(".");try{for(e=0,d=g.length;d>e;e++)c=f=f[g[e]]}finally{if(void 0===c)throw new ReferenceError("Could not find Intl object: "+a)}return c}function e(a,b,c){a=new Date(a),k(a,"A date or timestamp must be provided to {{formatDate}}"),c||(c=b,b=null);var d=c.data.intl&&c.data.intl.locales,e=n("date",b,c);return T(d,e).format(a)}function f(a,b,c){a=new Date(a),k(a,"A date or timestamp must be provided to {{formatTime}}"),c||(c=b,b=null);var d=c.data.intl&&c.data.intl.locales,e=n("time",b,c);return T(d,e).format(a)}function g(a,b,c){a=new Date(a),k(a,"A date or timestamp must be provided to {{formatRelative}}"),c||(c=b,b=null);var d=c.data.intl&&c.data.intl.locales,e=n("relative",b,c),f=c.hash.now;return delete e.now,V(d,e).format(a,{now:f})}function h(a,b,c){l(a,"A number must be provided to {{formatNumber}}"),c||(c=b,b=null);var d=c.data.intl&&c.data.intl.locales,e=n("number",b,c);return S(d,e).format(a)}function i(a,b){b||(b=a,a=null);var c=b.hash;if(!a&&"string"!=typeof a&&!c.intlName)throw new ReferenceError("{{formatMessage}} must be provided a message or intlName");var e=b.data.intl||{},f=e.locales,g=e.formats;return!a&&c.intlName&&(a=d(c.intlName,b)),"function"==typeof a?a(c):("string"==typeof a&&(a=U(a,f,g)),a.format(c))}function j(){var a,b,c=[].slice.call(arguments).pop(),d=c.hash;for(a in d)d.hasOwnProperty(a)&&(b=d[a],"string"==typeof b&&(d[a]=q(b)));return new o(String(i.apply(this,arguments)))}function k(a,b){if(!isFinite(a))throw new TypeError(b)}function l(a,b){if("number"!=typeof a)throw new TypeError(b)}function n(a,b,c){var e,f=c.hash;return b?("string"==typeof b&&(e=d("formats."+a+"."+b,c)),e=m({},e,f)):e=f,e}var o=a.SafeString,p=a.createFrame,q=a.Utils.escapeExpression,r={intl:c,intlGet:d,formatDate:e,formatTime:f,formatRelative:g,formatNumber:h,formatMessage:i,formatHTMLMessage:j,intlDate:b("intlDate",e),intlTime:b("intlTime",f),intlNumber:b("intlNumber",h),intlMessage:b("intlMessage",i),intlHTMLMessage:b("intlHTMLMessage",j)};for(var s in r)r.hasOwnProperty(s)&&a.registerHelper(s,r[s])}function o(a){x.__addLocaleData(a),M.__addLocaleData(a)}var p=Object.prototype.hasOwnProperty,q=function(){try{return!!Object.defineProperty({},"a",{})}catch(a){return!1}}(),r=(!q&&!Object.prototype.__defineGetter__,q?Object.defineProperty:function(a,b,c){"get"in c&&a.__defineGetter__?a.__defineGetter__(b,c.get):(!p.call(a,b)||"value"in c)&&(a[b]=c.value)}),s=Object.create||function(a,b){function c(){}var d,e;c.prototype=a,d=new c;for(e in b)p.call(b,e)&&r(d,e,b[e]);return d},t=b;b.prototype.compile=function(a){return this.pluralStack=[],this.currentPlural=null,this.pluralNumberFormat=null,this.compileMessage(a)},b.prototype.compileMessage=function(a){if(!a||"messageFormatPattern"!==a.type)throw new Error('Message AST is not of type: "messageFormatPattern"');var b,c,d,e=a.elements,f=[];for(b=0,c=e.length;c>b;b+=1)switch(d=e[b],d.type){case"messageTextElement":f.push(this.compileMessageText(d));break;case"argumentElement":f.push(this.compileArgument(d));break;default:throw new Error("Message element does not have a valid type")}return f},b.prototype.compileMessageText=function(a){return this.currentPlural&&/(^|[^\\])#/g.test(a.value)?(this.pluralNumberFormat||(this.pluralNumberFormat=new Intl.NumberFormat(this.locales)),new e(this.currentPlural.id,this.currentPlural.format.offset,this.pluralNumberFormat,a.value)):a.value.replace(/\\#/g,"#")},b.prototype.compileArgument=function(a){var b=a.format;if(!b)return new c(a.id);var e,g=this.formats,h=this.locales,i=this.pluralFn;switch(b.type){case"numberFormat":return e=g.number[b.style],{id:a.id,format:new Intl.NumberFormat(h,e).format};case"dateFormat":return e=g.date[b.style],{id:a.id,format:new Intl.DateTimeFormat(h,e).format};case"timeFormat":return e=g.time[b.style],{id:a.id,format:new Intl.DateTimeFormat(h,e).format};case"pluralFormat":return e=this.compileOptions(a),new d(a.id,b.ordinal,b.offset,e,i);case"selectFormat":return e=this.compileOptions(a),new f(a.id,e);default:throw new Error("Message element does not have a valid format type")}},b.prototype.compileOptions=function(a){var b=a.format,c=b.options,d={};this.pluralStack.push(this.currentPlural),this.currentPlural="pluralFormat"===b.type?a:null;var e,f,g;for(e=0,f=c.length;f>e;e+=1)g=c[e],d[g.selector]=this.compileMessage(g.value);return this.currentPlural=this.pluralStack.pop(),d},c.prototype.format=function(a){return a?"string"==typeof a?a:String(a):""},d.prototype.getOption=function(a){var b=this.options,c=b["="+a]||b[this.pluralFn(a-this.offset,this.useOrdinal)];return c||b.other},e.prototype.format=function(a){var b=this.numberFormat.format(a-this.offset);return this.string.replace(/(^|[^\\])#/g,"$1"+b).replace(/\\#/g,"#")},f.prototype.getOption=function(a){var b=this.options;return b[a]||b.other};var u=function(){function a(a,b){function c(){this.constructor=a}c.prototype=b.prototype,a.prototype=new c}function b(a,b,c,d,e,f){this.message=a,this.expected=b,this.found=c,this.offset=d,this.line=e,this.column=f,this.name="SyntaxError"}function c(a){function c(b){function c(b,c,d){var e,f;for(e=c;d>e;e++)f=a.charAt(e),"\n"===f?(b.seenCR||b.line++,b.column=1,b.seenCR=!1):"\r"===f||"\u2028"===f||"\u2029"===f?(b.line++,b.column=1,b.seenCR=!0):(b.column++,b.seenCR=!1)}return Ua!==b&&(Ua>b&&(Ua=0,Va={line:1,column:1,seenCR:!1}),c(Va,Ua,b),Ua=b),Va}function d(a){Wa>Sa||(Sa>Wa&&(Wa=Sa,Xa=[]),Xa.push(a))}function e(d,e,f){function g(a){var b=1;for(a.sort(function(a,b){return a.descriptionb.description?1:0});b1?g.slice(0,-1).join(", ")+" or "+g[a.length-1]:g[0],e=b?'"'+c(b)+'"':"end of input","Expected "+d+" but "+e+" found."}var i=c(f),j=f1?arguments[1]:{},E={},F={start:f},G=f,H=function(a){return{type:"messageFormatPattern",elements:a}},I=E,J=function(a){var b,c,d,e,f,g="";for(b=0,d=a.length;d>b;b+=1)for(e=a[b],c=0,f=e.length;f>c;c+=1)g+=e[c];return g},K=function(a){return{type:"messageTextElement",value:a}},L=/^[^ \t\n\r,.+={}#]/,M={type:"class",value:"[^ \\t\\n\\r,.+={}#]",description:"[^ \\t\\n\\r,.+={}#]"},N="{",O={type:"literal",value:"{",description:'"{"'},P=null,Q=",",R={type:"literal",value:",",description:'","'},S="}",T={type:"literal",value:"}",description:'"}"'},U=function(a,b){return{type:"argumentElement",id:a,format:b&&b[2]}},V="number",W={type:"literal",value:"number",description:'"number"'},X="date",Y={type:"literal",value:"date",description:'"date"'},Z="time",$={type:"literal",value:"time",description:'"time"'},_=function(a,b){return{type:a+"Format",style:b&&b[2]}},aa="plural",ba={type:"literal",value:"plural",description:'"plural"'},ca=function(a){return{type:a.type,ordinal:!1,offset:a.offset||0,options:a.options}},da="selectordinal",ea={type:"literal",value:"selectordinal",description:'"selectordinal"'},fa=function(a){return{type:a.type,ordinal:!0,offset:a.offset||0,options:a.options}},ga="select",ha={type:"literal",value:"select",description:'"select"'},ia=function(a){return{type:"selectFormat",options:a}},ja="=",ka={type:"literal",value:"=",description:'"="'},la=function(a,b){return{type:"optionalFormatPattern",selector:a,value:b}},ma="offset:",na={type:"literal",value:"offset:",description:'"offset:"'},oa=function(a){return a},pa=function(a,b){return{type:"pluralFormat",offset:a,options:b}},qa={type:"other",description:"whitespace"},ra=/^[ \t\n\r]/,sa={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},ta={type:"other",description:"optionalWhitespace"},ua=/^[0-9]/,va={type:"class",value:"[0-9]",description:"[0-9]"},wa=/^[0-9a-f]/i,xa={type:"class",value:"[0-9a-f]i",description:"[0-9a-f]i"},ya="0",za={type:"literal",value:"0",description:'"0"'},Aa=/^[1-9]/,Ba={type:"class",value:"[1-9]",description:"[1-9]"},Ca=function(a){return parseInt(a,10)},Da=/^[^{}\\\0-\x1F \t\n\r]/,Ea={type:"class",value:"[^{}\\\\\\0-\\x1F \\t\\n\\r]",description:"[^{}\\\\\\0-\\x1F \\t\\n\\r]"},Fa="\\#",Ga={type:"literal",value:"\\#",description:'"\\\\#"'},Ha=function(){return"\\#"},Ia="\\{",Ja={type:"literal",value:"\\{",description:'"\\\\{"'},Ka=function(){return"{"},La="\\}",Ma={type:"literal",value:"\\}",description:'"\\\\}"'},Na=function(){return"}"},Oa="\\u",Pa={type:"literal",value:"\\u",description:'"\\\\u"'},Qa=function(a){return String.fromCharCode(parseInt(a,16))},Ra=function(a){return a.join("")},Sa=0,Ta=0,Ua=0,Va={line:1,column:1,seenCR:!1},Wa=0,Xa=[],Ya=0;if("startRule"in D){if(!(D.startRule in F))throw new Error("Can't start parsing from rule \""+D.startRule+'".');G=F[D.startRule]}if(C=G(),C!==E&&Sa===a.length)return C;throw C!==E&&Sac;c+=1)if(e=a[c],"string"!=typeof e){if(f=e.id,!b||!p.call(b,f))throw new Error("A value must be provided for: "+f);g=b[f],h+=e.options?this._format(e.getOption(g),b):e.format(g)}else h+=e;return h},g.prototype._mergeFormats=function(b,c){var d,e,f={};for(d in b)p.call(b,d)&&(f[d]=e=s(b[d]),c&&p.call(c,d)&&a(e,c[d]));return f},g.prototype._resolveLocale=function(a){"string"==typeof a&&(a=[a]),a=(a||[]).concat(g.defaultLocale);var b,c,d,e,f=g.__localeData__;for(b=0,c=a.length;c>b;b+=1)for(d=a[b].toLowerCase().split("-");d.length;){if(e=f[d.join("-")])return e.locale;d.pop()}var h=a.pop();throw new Error("No locale data has been added to IntlMessageFormat for: "+a.join(", ")+", or the default locale: "+h)};var w={locale:"en",pluralRuleFunction:function(a,b){var c=String(a).split("."),d=!c[1],e=Number(c[0])==a,f=e&&c[0].slice(-1),g=e&&c[0].slice(-2);return b?1==f&&11!=g?"one":2==f&&12!=g?"two":3==f&&13!=g?"few":"other":1==a&&d?"one":"other"}};v.__addLocaleData(w),v.defaultLocale="en";var x=v,y=Math.round,z=function(a,b){a=+a,b=+b;var c=y(b-a),d=y(c/1e3),e=y(d/60),f=y(e/60),g=y(f/24),i=y(g/7),j=h(g),k=y(12*j),l=y(j);return{millisecond:c,second:d,minute:e,hour:f,day:g,week:i,month:k,year:l}},A=Object.prototype.hasOwnProperty,B=Object.prototype.toString,C=function(){try{return!!Object.defineProperty({},"a",{})}catch(a){return!1}}(),D=(!C&&!Object.prototype.__defineGetter__,C?Object.defineProperty:function(a,b,c){"get"in c&&a.__defineGetter__?a.__defineGetter__(b,c.get):(!A.call(a,b)||"value"in c)&&(a[b]=c.value)}),E=Object.create||function(a,b){function c(){}var d,e;c.prototype=a,d=new c;for(e in b)A.call(b,e)&&D(d,e,b[e]);return d},F=Array.prototype.indexOf||function(a,b){var c=this;if(!c.length)return-1;for(var d=b||0,e=c.length;e>d;d++)if(c[d]===a)return d;return-1},G=Array.isArray||function(a){return"[object Array]"===B.call(a)},H=Date.now||function(){return(new Date).getTime()},I=i,J=["second","minute","hour","day","month","year"],K=["best fit","numeric"];D(i,"__localeData__",{value:E(null)}),D(i,"__addLocaleData",{value:function(a){if(!a||!a.locale)throw new Error("Locale data provided to IntlRelativeFormat is missing a `locale` property value");i.__localeData__[a.locale.toLowerCase()]=a,x.__addLocaleData(a)}}),D(i,"defaultLocale",{enumerable:!0,writable:!0,value:void 0}),D(i,"thresholds",{enumerable:!0,value:{second:45,minute:45,hour:22,day:26,month:11}}),i.prototype.resolvedOptions=function(){return{locale:this._locale,style:this._options.style,units:this._options.units}},i.prototype._compileMessage=function(a){var b,c=this._locales,d=(this._locale,this._fields[a]),e=d.relativeTime,f="",g="";for(b in e.future)e.future.hasOwnProperty(b)&&(f+=" "+b+" {"+e.future[b].replace("{0}","#")+"}");for(b in e.past)e.past.hasOwnProperty(b)&&(g+=" "+b+" {"+e.past[b].replace("{0}","#")+"}");var h="{when, select, future {{0, plural, "+f+"}}past {{0, plural, "+g+"}}}";return new x(h,c)},i.prototype._getMessage=function(a){var b=this._messages;return b[a]||(b[a]=this._compileMessage(a)),b[a]},i.prototype._getRelativeUnits=function(a,b){var c=this._fields[b];return c.relative?c.relative[a]:void 0},i.prototype._findFields=function(a){for(var b=i.__localeData__,c=b[a.toLowerCase()];c;){if(c.fields)return c.fields;c=c.parentLocale&&b[c.parentLocale.toLowerCase()]}throw new Error("Locale data added to IntlRelativeFormat is missing `fields` for :"+a)},i.prototype._format=function(a,b){var c=b&&void 0!==b.now?b.now:H();if(void 0===a&&(a=c),!isFinite(c))throw new RangeError("The `now` option provided to IntlRelativeFormat#format() is not in valid range.");if(!isFinite(a))throw new RangeError("The date value provided to IntlRelativeFormat#format() is not in valid range.");var d=z(c,a),e=this._options.units||this._selectUnits(d),f=d[e];if("numeric"!==this._options.style){var g=this._getRelativeUnits(f,e);if(g)return g}return this._getMessage(e).format({0:Math.abs(f),when:0>f?"past":"future"})},i.prototype._isValidUnits=function(a){if(!a||F.call(J,a)>=0)return!0;if("string"==typeof a){var b=/s$/.test(a)&&a.substr(0,a.length-1);if(b&&F.call(J,b)>=0)throw new Error('"'+a+'" is not a valid IntlRelativeFormat `units` value, did you mean: '+b)}throw new Error('"'+a+'" is not a valid IntlRelativeFormat `units` value, it must be one of: "'+J.join('", "')+'"')},i.prototype._resolveLocale=function(a){"string"==typeof a&&(a=[a]),a=(a||[]).concat(i.defaultLocale);var b,c,d,e,f=i.__localeData__;for(b=0,c=a.length;c>b;b+=1)for(d=a[b].toLowerCase().split("-");d.length;){if(e=f[d.join("-")])return e.locale;d.pop()}var g=a.pop();throw new Error("No locale data has been added to IntlRelativeFormat for: "+a.join(", ")+", or the default locale: "+g)},i.prototype._resolveStyle=function(a){if(!a)return K[0];if(F.call(K,a)>=0)return a;throw new Error('"'+a+'" is not a valid IntlRelativeFormat `style` value, it must be one of: "'+K.join('", "')+'"')},i.prototype._selectUnits=function(a){var b,c,d;for(b=0,c=J.length;c>b&&(d=J[b],!(Math.abs(a[d]) + + + + + + MoneroProxy + + + + + + + + + + + + +
+
+

MoneroProxy

+
+ + +
+
+ + + diff --git a/www/robots.txt b/www/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/www/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/www/script.js b/www/script.js new file mode 100644 index 0000000..b7a5382 --- /dev/null +++ b/www/script.js @@ -0,0 +1,38 @@ +HandlebarsIntl.registerWith(Handlebars); + +$(function() { + window.state = {}; + var userLang = (navigator.language || navigator.userLanguage) || 'en-US'; + window.intlData = { locales: userLang }; + var source = $("#stats-template").html(); + var template = Handlebars.compile(source); + refreshStats(template); + + setInterval(function() { + refreshStats(template); + }, 5000) +}); + +function refreshStats(template) { + $.getJSON("/stats", function(stats) { + $("#alert").addClass('hide'); + + // Sort miners by ID + if (stats.miners) { + stats.miners = stats.miners.sort(compare) + } + // Repaint stats + var html = template(stats, { data: { intl: window.intlData } }); + $('#stats').html(html); + }).fail(function() { + $("#alert").removeClass('hide'); + }); +} + +function compare(a, b) { + if (a.name < b.name) + return -1; + if (a.name > b.name) + return 1; + return 0; +} diff --git a/www/style.css b/www/style.css new file mode 100644 index 0000000..72e388d --- /dev/null +++ b/www/style.css @@ -0,0 +1,98 @@ +/* Fixes */ +.dl-horizontal dt { + white-space: normal; +} + +/* Space out content a bit */ +body { + padding-top: 20px; + padding-bottom: 20px; +} + +/* Custom page header */ +.header { + padding-bottom: 20px; + border-bottom: 1px solid #e5e5e5; +} +/* Make the masthead heading the same height as the navigation */ +.header h3 { + margin-top: 0; + margin-bottom: 0; + line-height: 40px; +} + +a.logo { + background: #04191f; + padding: 3px; + text-decoration: none; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +span.logo-1 { + font-weight: 700; + color: #1994b8; +} +span.logo-2 { + font-weight: 300; + color: #fff; +} +span.logo-3 { + color: #fff; + font-weight: 100; +} + +/* Custom page footer */ +.footer { + padding-top: 15px; + color: #777; + border-top: 1px solid #e5e5e5; +} + +/* Customize container */ +@media (min-width: 1200px) { + .container { + width: 970px; + } +} + +.container-narrow > hr { + margin: 30px 0; +} + +/* Main marketing message and sign up button */ +.jumbotron { + text-align: center; + border-bottom: 1px solid #e5e5e5; +} +.jumbotron .btn { + padding: 14px 24px; + font-size: 21px; +} + +/* Supporting marketing content */ +.marketing { + margin: 40px 0; +} +.marketing p + h4 { + margin-top: 28px; +} + +/* Responsive: Portrait tablets and up */ +@media screen and (min-width: 768px) { + /* Remove the padding we set earlier */ + .header, + .marketing, + .footer { + padding-right: 0; + padding-left: 0; + } + /* Space out the masthead */ + .header { + margin-bottom: 30px; + } + /* Remove the bottom border on the jumbotron for visual effect */ + .jumbotron { + border-bottom: 0; + } +}