mirror of
https://github.com/kvazar-network/keva-stratum.git
synced 2025-01-25 22:34:28 +00:00
Add initial Web-UI
This commit is contained in:
parent
98f11a6f13
commit
f181a9d925
@ -1,44 +1,51 @@
|
|||||||
{
|
{
|
||||||
"address": "46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em",
|
"address": "46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em",
|
||||||
"bypassAddressValidation": true,
|
"bypassAddressValidation": true,
|
||||||
"bypassShareValidation": true,
|
"bypassShareValidation": true,
|
||||||
|
|
||||||
"threads": 2,
|
"threads": 2,
|
||||||
|
|
||||||
"stratum": {
|
"stratum": {
|
||||||
"timeout": "15m",
|
"timeout": "15m",
|
||||||
"blockRefreshInterval": "1s",
|
"blockRefreshInterval": "1s",
|
||||||
|
|
||||||
"listen": [
|
"listen": [
|
||||||
{
|
{
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 1111,
|
"port": 1111,
|
||||||
"diff": 8000,
|
"diff": 8000,
|
||||||
"maxConn": 32768
|
"maxConn": 32768
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 3333,
|
"port": 3333,
|
||||||
"diff": 16000,
|
"diff": 16000,
|
||||||
"maxConn": 32768
|
"maxConn": 32768
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 5555,
|
"port": 5555,
|
||||||
"diff": 16000,
|
"diff": 16000,
|
||||||
"maxConn": 32768
|
"maxConn": 32768
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"daemon": {
|
"frontend": {
|
||||||
"host": "127.0.0.1",
|
"listen": "0.0.0.0:8082",
|
||||||
"port": 18081,
|
"login": "admin",
|
||||||
"timeout": "10s"
|
"password": "",
|
||||||
},
|
"hideIP": false
|
||||||
|
},
|
||||||
|
|
||||||
"newrelicEnabled": false,
|
"daemon": {
|
||||||
"newrelicName": "MyStratum",
|
"host": "127.0.0.1",
|
||||||
"newrelicKey": "SECRET_KEY",
|
"port": 18081,
|
||||||
"newrelicVerbose": false
|
"timeout": "10s"
|
||||||
|
},
|
||||||
|
|
||||||
|
"newrelicEnabled": false,
|
||||||
|
"newrelicName": "MyStratum",
|
||||||
|
"newrelicKey": "SECRET_KEY",
|
||||||
|
"newrelicVerbose": false
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
package pool
|
package pool
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
BypassAddressValidation bool `json:"bypassAddressValidation"`
|
BypassAddressValidation bool `json:"bypassAddressValidation"`
|
||||||
BypassShareValidation bool `json:"bypassShareValidation"`
|
BypassShareValidation bool `json:"bypassShareValidation"`
|
||||||
Stratum Stratum `json:"stratum"`
|
Stratum Stratum `json:"stratum"`
|
||||||
Daemon Daemon `json:"daemon"`
|
Daemon Daemon `json:"daemon"`
|
||||||
|
LuckWindow string `json:"luckWindow"`
|
||||||
Threads int `json:"threads"`
|
LargeLuckWindow string `json:"largeLuckWindow"`
|
||||||
|
Threads int `json:"threads"`
|
||||||
NewrelicName string `json:"newrelicName"`
|
Frontend Frontend `json:"frontend"`
|
||||||
NewrelicKey string `json:"newrelicKey"`
|
NewrelicName string `json:"newrelicName"`
|
||||||
NewrelicVerbose bool `json:"newrelicVerbose"`
|
NewrelicKey string `json:"newrelicKey"`
|
||||||
NewrelicEnabled bool `json:"newrelicEnabled"`
|
NewrelicVerbose bool `json:"newrelicVerbose"`
|
||||||
|
NewrelicEnabled bool `json:"newrelicEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Stratum struct {
|
type Stratum struct {
|
||||||
@ -33,3 +34,10 @@ type Daemon struct {
|
|||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Timeout string `json:"timeout"`
|
Timeout string `json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Frontend struct {
|
||||||
|
Listen string `json:"listen"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
HideIP bool `json:"hideIP"`
|
||||||
|
}
|
||||||
|
115
go-pool/stratum/api.go
Normal file
115
go-pool/stratum/api.go
Normal file
@ -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
|
||||||
|
}
|
@ -37,7 +37,7 @@ func (s *StratumServer) fetchBlockTemplate() bool {
|
|||||||
}
|
}
|
||||||
t := s.currentBlockTemplate()
|
t := s.currentBlockTemplate()
|
||||||
|
|
||||||
if t.PrevHash == reply.PrevHash {
|
if t != nil && t.PrevHash == reply.PrevHash {
|
||||||
// Fallback to height comparison
|
// Fallback to height comparison
|
||||||
if len(reply.PrevHash) == 0 && reply.Height > t.Height {
|
if len(reply.PrevHash) == 0 && reply.Height > t.Height {
|
||||||
log.Printf("New block to mine at height %v, diff: %v", reply.Height, reply.Difficulty)
|
log.Printf("New block to mine at height %v, diff: %v", reply.Height, reply.Difficulty)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"../util"
|
"../util"
|
||||||
)
|
)
|
||||||
@ -57,17 +58,20 @@ func (s *StratumServer) handleSubmitRPC(cs *Session, e *Endpoint, params *Submit
|
|||||||
job := miner.findJob(params.JobId)
|
job := miner.findJob(params.JobId)
|
||||||
if job == nil {
|
if job == nil {
|
||||||
errorReply = &ErrorReply{Code: -1, Message: "Invalid job id", Close: true}
|
errorReply = &ErrorReply{Code: -1, Message: "Invalid job id", Close: true}
|
||||||
|
atomic.AddUint64(&miner.invalidShares, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !noncePattern.MatchString(params.Nonce) {
|
if !noncePattern.MatchString(params.Nonce) {
|
||||||
errorReply = &ErrorReply{Code: -1, Message: "Malformed nonce", Close: true}
|
errorReply = &ErrorReply{Code: -1, Message: "Malformed nonce", Close: true}
|
||||||
|
atomic.AddUint64(&miner.invalidShares, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nonce := strings.ToLower(params.Nonce)
|
nonce := strings.ToLower(params.Nonce)
|
||||||
exist := job.submit(nonce)
|
exist := job.submit(nonce)
|
||||||
if exist {
|
if exist {
|
||||||
errorReply = &ErrorReply{Code: -1, Message: "Duplicate share", Close: true}
|
errorReply = &ErrorReply{Code: -1, Message: "Duplicate share", Close: true}
|
||||||
|
atomic.AddUint64(&miner.invalidShares, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +79,7 @@ func (s *StratumServer) handleSubmitRPC(cs *Session, e *Endpoint, params *Submit
|
|||||||
if job.Height != t.Height {
|
if job.Height != t.Height {
|
||||||
log.Printf("Block expired for height %v %s@%s", job.Height, miner.Login, miner.IP)
|
log.Printf("Block expired for height %v %s@%s", job.Height, miner.Login, miner.IP)
|
||||||
errorReply = &ErrorReply{Code: -1, Message: "Block expired", Close: false}
|
errorReply = &ErrorReply{Code: -1, Message: "Block expired", Close: false}
|
||||||
|
atomic.AddUint64(&miner.staleShares, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"../../cnutil"
|
"../../cnutil"
|
||||||
"../../hashing"
|
"../../hashing"
|
||||||
@ -36,6 +37,14 @@ type Miner struct {
|
|||||||
LastBeat int64
|
LastBeat int64
|
||||||
Session *Session
|
Session *Session
|
||||||
Endpoint *Endpoint
|
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 {
|
func (job *Job) submit(nonce string) bool {
|
||||||
@ -90,6 +99,39 @@ func (m *Miner) heartbeat() {
|
|||||||
atomic.StoreInt64(&m.LastBeat, now)
|
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 {
|
func (m *Miner) findJob(id string) *Job {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
defer m.RUnlock()
|
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 {
|
if !s.config.BypassShareValidation && hex.EncodeToString(hashBytes) != result {
|
||||||
log.Printf("Bad hash from miner %v@%v", m.Login, m.IP)
|
log.Printf("Bad hash from miner %v@%v", m.Login, m.IP)
|
||||||
|
atomic.AddUint64(&m.invalidShares, 1)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
hashDiff := util.GetHashDifficulty(hashBytes).Int64() // FIXME: Will return max int64 value if overflows
|
hashDiff := util.GetHashDifficulty(hashBytes).Int64() // FIXME: Will return max int64 value if overflows
|
||||||
|
atomic.AddInt64(&s.roundShares, hashDiff)
|
||||||
block := hashDiff >= t.Difficulty
|
block := hashDiff >= t.Difficulty
|
||||||
if block {
|
if block {
|
||||||
_, err := s.rpc.SubmitBlock(hex.EncodeToString(shareBuff))
|
_, err := s.rpc.SubmitBlock(hex.EncodeToString(shareBuff))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
atomic.AddUint64(&m.rejects, 1)
|
||||||
log.Printf("Block submission failure at height %v: %v", t.Height, err)
|
log.Printf("Block submission failure at height %v: %v", t.Height, err)
|
||||||
} else {
|
} else {
|
||||||
if len(convertedBlob) == 0 {
|
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))
|
blockFastHash := hex.EncodeToString(hashing.FastHash(convertedBlob))
|
||||||
// Immediately refresh current BT and send new jobs
|
// Immediately refresh current BT and send new jobs
|
||||||
s.refreshBlockTemplate(true)
|
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)
|
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 {
|
} else if hashDiff < job.Difficulty {
|
||||||
log.Printf("Rejected low difficulty share of %v from %v@%v", hashDiff, m.Login, m.IP)
|
log.Printf("Rejected low difficulty share of %v from %v@%v", hashDiff, m.Login, m.IP)
|
||||||
|
atomic.AddUint64(&m.invalidShares, 1)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Valid share at difficulty %v/%v", e.config.Difficulty, hashDiff)
|
log.Printf("Valid share at difficulty %v/%v", e.config.Difficulty, hashDiff)
|
||||||
|
atomic.AddUint64(&m.validShares, 1)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StratumServer struct {
|
type StratumServer struct {
|
||||||
config *pool.Config
|
config *pool.Config
|
||||||
miners MinersMap
|
miners MinersMap
|
||||||
blockTemplate atomic.Value
|
blockTemplate atomic.Value
|
||||||
rpc *rpc.RPCClient
|
rpc *rpc.RPCClient
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
estimationWindow time.Duration
|
||||||
|
blocksMu sync.RWMutex
|
||||||
|
blockStats map[int64]float64
|
||||||
|
luckWindow int64
|
||||||
|
luckLargeWindow int64
|
||||||
|
roundShares int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type Endpoint struct {
|
type Endpoint struct {
|
||||||
@ -51,7 +57,6 @@ func NewStratum(cfg *pool.Config) *StratumServer {
|
|||||||
stratum.timeout = timeout
|
stratum.timeout = timeout
|
||||||
|
|
||||||
// Init block template
|
// Init block template
|
||||||
stratum.blockTemplate.Store(&BlockTemplate{})
|
|
||||||
stratum.refreshBlockTemplate(false)
|
stratum.refreshBlockTemplate(false)
|
||||||
|
|
||||||
refreshIntv, _ := time.ParseDuration(cfg.Stratum.BlockRefreshInterval)
|
refreshIntv, _ := time.ParseDuration(cfg.Stratum.BlockRefreshInterval)
|
||||||
@ -261,7 +266,10 @@ func (s *StratumServer) removeMiner(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *StratumServer) currentBlockTemplate() *BlockTemplate {
|
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) {
|
func checkError(err error) {
|
||||||
|
20
main.go
20
main.go
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -12,6 +13,8 @@ import (
|
|||||||
"./go-pool/pool"
|
"./go-pool/pool"
|
||||||
"./go-pool/stratum"
|
"./go-pool/stratum"
|
||||||
|
|
||||||
|
"github.com/goji/httpauth"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"github.com/yvasiyarov/gorelic"
|
"github.com/yvasiyarov/gorelic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,9 +31,26 @@ func startStratum() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := stratum.NewStratum(&cfg)
|
s := stratum.NewStratum(&cfg)
|
||||||
|
go startFrontend(&cfg, s)
|
||||||
s.Listen()
|
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() {
|
func startNewrelic() {
|
||||||
// Run NewRelic
|
// Run NewRelic
|
||||||
if cfg.NewrelicEnabled {
|
if cfg.NewrelicEnabled {
|
||||||
|
2
www/handlebars-intl.min.js
vendored
Normal file
2
www/handlebars-intl.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
182
www/index.html
Normal file
182
www/index.html
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>MoneroProxy</title>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
|
||||||
|
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
|
||||||
|
<script src="//cdn.polyfill.io/v2/polyfill.min.js"></script>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js"></script>
|
||||||
|
<script src="handlebars-intl.min.js"></script>
|
||||||
|
<link href="style.css" rel="stylesheet">
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script id="stats-template" type="text/x-handlebars-template">
|
||||||
|
<div class="row marketing">
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Hashrate</dt>
|
||||||
|
<dd><span class="badge alert-info">{{formatNumber hashrate maximumFractionDigits=4}}</span></dd>
|
||||||
|
<dt>Hashrate 24h</dt>
|
||||||
|
<dd><span class="badge alert-info">{{formatNumber hashrate24h maximumFractionDigits=4}}</span></dd>
|
||||||
|
<dt>Total Miners</dt>
|
||||||
|
<dd><span class="badge alert-info">{{totalMiners}}</span></dd>
|
||||||
|
<dt>Miners Online</dt>
|
||||||
|
<dd><span class="badge alert-success">{{totalOnline}}</span></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
{{#if current}}
|
||||||
|
<dt>Accepted</dt>
|
||||||
|
<dd><span class="badge alert-success">{{formatNumber current.accepts}}</span></dd>
|
||||||
|
<dt>Rejected</dt>
|
||||||
|
<dd><span class="badge alert-danger">{{formatNumber current.rejects}}</span></dd>
|
||||||
|
{{/if}}
|
||||||
|
<dt>Miners Timed Out</dt>
|
||||||
|
<dd><span class="badge alert-danger">{{formatNumber timedOut}}</span></dd>
|
||||||
|
{{#if current.lastSubmissionAt}}
|
||||||
|
<dt>Last Submission</dt>
|
||||||
|
<dd><span class="badge alert-info">{{formatRelative current.lastSubmissionAt now=now}}</span></dd>
|
||||||
|
{{/if}}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12">
|
||||||
|
{{#if errors}}
|
||||||
|
<div id="alert" class="alert alert-danger" role="alert">
|
||||||
|
<strong>{{errors}}</strong>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if info}}
|
||||||
|
<p>
|
||||||
|
{{#if template}}
|
||||||
|
<strong>Block height:</strong> <span class="label label-primary">{{height}}</span>
|
||||||
|
<strong>Difficulty:</strong> <span class="label label-primary">{{formatNumber diff maximumFractionDigits=4}}</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if info}}
|
||||||
|
{{#if testnet}}
|
||||||
|
<span class="label label-danger">TESTNET</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="label label-success">MAINNET</span>
|
||||||
|
{{/if}}
|
||||||
|
<strong>Connections:</strong> <span class="label label-primary">{{formatNumber connections}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<p>
|
||||||
|
<strong>Blocks {{luck.window}}:</strong> <span class="label label-primary">{{formatNumber luck.blocksCount}}</span>
|
||||||
|
<strong>Shares/Diff {{luck.window}}:</strong>
|
||||||
|
<span class="label label-primary">{{formatNumber luck.variance style="percent" minimumFractionDigits=2 maximumFractionDigits=2}}</span>
|
||||||
|
<strong>Blocks {{luck.largeWindow}}:</strong> <span class="label label-primary">{{formatNumber luck.totalBlocksCount}}</span>
|
||||||
|
<strong>Shares/Diff {{luck.largeWindow}}:</strong>
|
||||||
|
<span class="label label-primary">{{formatNumber luck.totalVariance style="percent" minimumFractionDigits=2 maximumFractionDigits=2}}</span>
|
||||||
|
{{#if template}}
|
||||||
|
<strong>Round Progress:</strong>
|
||||||
|
<span class="label label-primary">{{formatNumber variance style="percent" minimumFractionDigits=2 maximumFractionDigits=2}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<h4>Upstream</h4>
|
||||||
|
<table class="table table-condensed">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Url</th>
|
||||||
|
<th>Accepted</th>
|
||||||
|
<th>Rejected</th>
|
||||||
|
<th>Fails</th>
|
||||||
|
</tr>
|
||||||
|
{{#each upstreams}}
|
||||||
|
{{#if sick}}
|
||||||
|
<tr class="danger">
|
||||||
|
{{else}}
|
||||||
|
<tr class="success">
|
||||||
|
{{/if}}
|
||||||
|
{{#if current}}
|
||||||
|
<td><strong>{{name}}</strong></td>
|
||||||
|
{{else}}
|
||||||
|
<td>{{name}}</td>
|
||||||
|
{{/if}}
|
||||||
|
<td>{{url}}</td>
|
||||||
|
<td>{{formatNumber accepts}}</td>
|
||||||
|
<td><strong>{{formatNumber rejects}}</strong></td>
|
||||||
|
<td>{{failsCount}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<h4>Miners</h4>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-condensed">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>HR</th>
|
||||||
|
<th>HR 24h</th>
|
||||||
|
<th>Last Share</th>
|
||||||
|
<th>Accepted</th>
|
||||||
|
<th>Rejected</th>
|
||||||
|
<th>Blocks Accepted</th>
|
||||||
|
<th>Blocks Rejected</th>
|
||||||
|
</tr>
|
||||||
|
{{#each miners}}
|
||||||
|
{{#if timeout}}
|
||||||
|
<tr class="danger">
|
||||||
|
{{else}}
|
||||||
|
{{#if warning}}
|
||||||
|
<tr class="warning">
|
||||||
|
{{else}}
|
||||||
|
<tr class="success">
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
<td>{{name}}</td>
|
||||||
|
<td>
|
||||||
|
{{#if ip}}
|
||||||
|
{{ip}}
|
||||||
|
{{else}}
|
||||||
|
—
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
<td>{{formatNumber hashrate maximumFractionDigits=4}}</td>
|
||||||
|
<td>{{formatNumber hashrate24h maximumFractionDigits=4}}</td>
|
||||||
|
<td>{{formatRelative lastBeat now=../now}}</td>
|
||||||
|
<td>{{formatNumber validShares}}</td>
|
||||||
|
<td><strong>{{formatNumber invalidShares}}</strong></td>
|
||||||
|
<td>{{formatNumber accepts}}</td>
|
||||||
|
<td>{{formatNumber rejects}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="header clearfix">
|
||||||
|
<h3 class="text-muted">MoneroProxy</h3>
|
||||||
|
</div>
|
||||||
|
<div id="alert" class="alert alert-danger hide" role="alert">
|
||||||
|
<strong>An error occured while polling proxy state.</strong>
|
||||||
|
Make sure proxy is running.
|
||||||
|
</div>
|
||||||
|
<a name="stats"></a>
|
||||||
|
<div id="stats"></div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<p>
|
||||||
|
By <a href="https://github.com/sammy007" target="_blank">sammy007</a>.<br/>
|
||||||
|
<span><strong>XMR</strong>: 4Aag5kkRHmCFHM5aRUtfB2RF3c5NDmk5CVbGdg6fefszEhhFdXhnjiTCr81YxQ9bsi73CSHT3ZN3p82qyakHwZ2GHYqeaUr</span><br/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
2
www/robots.txt
Normal file
2
www/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
38
www/script.js
Normal file
38
www/script.js
Normal file
@ -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;
|
||||||
|
}
|
98
www/style.css
Normal file
98
www/style.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user