diff --git a/api/server.go b/api/server.go index dd021a1e3..c61d910dd 100644 --- a/api/server.go +++ b/api/server.go @@ -2,16 +2,21 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" "sort" + "strconv" "strings" "sync" "sync/atomic" "time" "github.com/gorilla/mux" + "github.com/robfig/cron" + "github.com/sammy007/open-ethereum-pool/payouts" + "github.com/sammy007/open-ethereum-pool/rpc" "github.com/sammy007/open-ethereum-pool/storage" "github.com/sammy007/open-ethereum-pool/util" ) @@ -19,6 +24,10 @@ import ( type ApiConfig struct { Enabled bool `json:"enabled"` Listen string `json:"listen"` + PoolCharts string `json:"poolCharts"` + PoolChartsNum int64 `json:"poolChartsNum"` + MinerChartsNum int64 `json:"minerChartsNum"` + MinerCharts string `json:"minerCharts"` StatsCollectInterval string `json:"statsCollectInterval"` HashrateWindow string `json:"hashrateWindow"` HashrateLargeWindow string `json:"hashrateLargeWindow"` @@ -30,6 +39,7 @@ type ApiConfig struct { } type ApiServer struct { + settings map[string]interface{} config *ApiConfig backend *storage.RedisClient hashrateWindow time.Duration @@ -38,22 +48,37 @@ type ApiServer struct { miners map[string]*Entry minersMu sync.RWMutex statsIntv time.Duration + rpc *rpc.RPCClient + genesisHash string } type Entry struct { stats map[string]interface{} updatedAt int64 + hasChart bool } -func NewApiServer(cfg *ApiConfig, backend *storage.RedisClient) *ApiServer { +func NewApiServer(cfg *ApiConfig, settings map[string]interface{}, backend *storage.RedisClient) *ApiServer { + rpcDaemon := settings["BlockUnlocker"].(map[string]interface{})["Daemon"].(string) + rpcTimeout := settings["BlockUnlocker"].(map[string]interface{})["Timeout"].(string) + rpc := rpc.NewRPCClient("BlockUnlocker", rpcDaemon, rpcTimeout) + + block, err := rpc.GetBlockByHeight(0) + if err != nil || block == nil { + log.Fatalf("Error while retrieving genesis block from node: %v", err) + } + hashrateWindow := util.MustParseDuration(cfg.HashrateWindow) hashrateLargeWindow := util.MustParseDuration(cfg.HashrateLargeWindow) return &ApiServer{ + settings: settings, config: cfg, backend: backend, hashrateWindow: hashrateWindow, hashrateLargeWindow: hashrateLargeWindow, miners: make(map[string]*Entry), + rpc: rpc, + genesisHash: block.Hash, } } @@ -96,18 +121,77 @@ func (s *ApiServer) Start() { } }() + go func() { + c := cron.New() + + poolCharts := s.config.PoolCharts + log.Printf("pool charts config is :%v", poolCharts) + c.AddFunc(poolCharts, func() { + s.collectPoolCharts() + }) + + minerCharts := s.config.MinerCharts + log.Printf("miner charts config is :%v", minerCharts) + c.AddFunc(minerCharts, func() { + + miners, err := s.backend.GetAllMinerAccount() + if err != nil { + log.Println("Get all miners account error: ", err) + } + for _, login := range miners { + miner, _ := s.backend.CollectWorkersStats(s.hashrateWindow, s.hashrateLargeWindow, login) + s.collectMinerCharts(login, miner["currentHashrate"].(int64), miner["hashrate"].(int64), miner["workersOnline"].(int64)) + } + }) + + c.Start() + }() + if !s.config.PurgeOnly { s.listen() } } +func (s *ApiServer) collectPoolCharts() { + ts := util.MakeTimestamp() / 1000 + now := time.Now() + year, month, day := now.Date() + hour, min, _ := now.Clock() + t2 := fmt.Sprintf("%d-%02d-%02d %02d_%02d", year, month, day, hour, min) + stats := s.getStats() + hash := fmt.Sprint(stats["hashrate"]) + log.Println("Pool Hash is ", ts, t2, hash) + err := s.backend.WritePoolCharts(ts, t2, hash) + if err != nil { + log.Printf("Failed to fetch pool charts from backend: %v", err) + return + } +} + +func (s *ApiServer) collectMinerCharts(login string, hash int64, largeHash int64, workerOnline int64) { + ts := util.MakeTimestamp() / 1000 + now := time.Now() + year, month, day := now.Date() + hour, min, _ := now.Clock() + t2 := fmt.Sprintf("%d-%02d-%02d %02d_%02d", year, month, day, hour, min) + + log.Println("Miner "+login+" Hash is", ts, t2, hash, largeHash) + err := s.backend.WriteMinerCharts(ts, t2, login, hash, largeHash, workerOnline) + if err != nil { + log.Printf("Failed to fetch miner %v charts from backend: %v", login, err) + } +} + func (s *ApiServer) listen() { r := mux.NewRouter() r.HandleFunc("/api/stats", s.StatsIndex) + r.HandleFunc("/api/stats/{chart:charts?}", s.StatsIndex) r.HandleFunc("/api/miners", s.MinersIndex) r.HandleFunc("/api/blocks", s.BlocksIndex) r.HandleFunc("/api/payments", s.PaymentsIndex) + r.HandleFunc("/api/settings", s.Settings) r.HandleFunc("/api/accounts/{login:0x[0-9a-fA-F]{40}}", s.AccountIndex) + r.HandleFunc("/api/accounts/{login:0x[0-9a-fA-F]{40}}/{chart:charts?}", s.AccountIndex) r.NotFoundHandler = http.HandlerFunc(notFound) err := http.ListenAndServe(s.config.Listen, r) if err != nil { @@ -146,6 +230,7 @@ func (s *ApiServer) collectStats() { return } } + stats["poolCharts"], err = s.backend.GetPoolCharts(s.config.PoolChartsNum) s.stats.Store(stats) log.Printf("Stats collection finished %s", time.Since(start)) } @@ -156,17 +241,27 @@ func (s *ApiServer) StatsIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache") w.WriteHeader(http.StatusOK) + chart := strings.ToLower(mux.Vars(r)["chart"]) + reply := make(map[string]interface{}) nodes, err := s.backend.GetNodeStates() if err != nil { log.Printf("Failed to get nodes stats from backend: %v", err) } reply["nodes"] = nodes + if nodes[0] != nil { + height := nodes[0]["height"].(string) + number, _ := strconv.ParseInt(height, 10, 64) + reply["blockReward"] = payouts.GetConstReward(number).String() + } stats := s.getStats() if stats != nil { reply["now"] = util.MakeTimestamp() reply["stats"] = stats["stats"] + if chart != "" { + reply["poolCharts"] = stats["poolCharts"] + } reply["hashrate"] = stats["hashrate"] reply["minersTotal"] = stats["minersTotal"] reply["maturedTotal"] = stats["maturedTotal"] @@ -250,6 +345,8 @@ func (s *ApiServer) AccountIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache") login := strings.ToLower(mux.Vars(r)["login"]) + chart := strings.ToLower(mux.Vars(r)["chart"]) + useChart := chart != "" s.minersMu.Lock() defer s.minersMu.Unlock() @@ -257,7 +354,7 @@ func (s *ApiServer) AccountIndex(w http.ResponseWriter, r *http.Request) { now := util.MakeTimestamp() cacheIntv := int64(s.statsIntv / time.Millisecond) // Refresh stats if stale - if !ok || reply.updatedAt < now-cacheIntv { + if !ok || reply.updatedAt < now-cacheIntv || (useChart && !reply.hasChart) { exist, err := s.backend.IsMinerExists(login) if !exist { w.WriteHeader(http.StatusNotFound) @@ -285,7 +382,23 @@ func (s *ApiServer) AccountIndex(w http.ResponseWriter, r *http.Request) { stats[key] = value } stats["pageSize"] = s.config.Payments - reply = &Entry{stats: stats, updatedAt: now} + if useChart { + charts, err := s.backend.GetMinerCharts(s.config.MinerChartsNum, login) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("Failed to fetch charts from backend: %v", err) + return + } + stats["minerCharts"] = charts + payments, err := s.backend.GetPaymentCharts(login) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("Failed to fetch payments from backend: %v", err) + return + } + stats["paymentCharts"] = payments + } + reply = &Entry{stats: stats, updatedAt: now, hasChart: useChart} s.miners[login] = reply } @@ -296,6 +409,40 @@ func (s *ApiServer) AccountIndex(w http.ResponseWriter, r *http.Request) { } } +func (s *ApiServer) Settings(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Cache-Control", "max-age=600") + w.WriteHeader(http.StatusOK) + + reply := make(map[string]interface{}) + + reply["HashLimit"] = s.settings["Proxy"].(map[string]interface{})["HashLimit"] + reply["Difficulty"] = s.settings["Proxy"].(map[string]interface{})["Difficulty"] + + reply["PoolFee"] = s.settings["BlockUnlocker"].(map[string]interface{})["PoolFee"] + reply["PoolFeeAddress"] = s.settings["BlockUnlocker"].(map[string]interface{})["PoolFeeAddress"] + reply["Donate"] = s.settings["BlockUnlocker"].(map[string]interface{})["Donate"] + reply["DonateFee"] = s.settings["BlockUnlocker"].(map[string]interface{})["DonateFee"] + reply["DonateAddress"] = s.settings["BlockUnlocker"].(map[string]interface{})["DonateAddress"] + reply["KeyTxFees"] = s.settings["BlockUnlocker"].(map[string]interface{})["KeepTxFees"] + reply["BlockUnlockDepth"] = s.settings["BlockUnlocker"].(map[string]interface{})["Depth"] + + reply["EthProxy"] = s.settings["Proxy"].(map[string]interface{})["Enabled"] + reply["EthProxyPool"] = s.settings["Proxy"].(map[string]interface{})["Listen"] + reply["Stratum"] = s.settings["Proxy"].(map[string]interface{})["Stratum"].(map[string]interface{})["Enabled"] + reply["StratumPool"] = s.settings["Proxy"].(map[string]interface{})["Stratum"].(map[string]interface{})["Listen"] + reply["PayoutThreshold"] = s.settings["Payouts"].(map[string]interface{})["Threshold"] + reply["PayoutInterval"] = s.settings["Payouts"].(map[string]interface{})["Interval"] + + reply["GenesisHash"] = s.genesisHash + + err := json.NewEncoder(w).Encode(reply) + if err != nil { + log.Println("Error serializing API response: ", err) + } +} + func (s *ApiServer) getStats() map[string]interface{} { stats := s.stats.Load() if stats != nil { diff --git a/config.example.json b/config.example.json index 1a264b8cc..be789a1d6 100644 --- a/config.example.json +++ b/config.example.json @@ -4,6 +4,7 @@ "name": "main", "proxy": { + "hashLimit" : 240000000, "enabled": true, "listen": "0.0.0.0:8888", "limitHeadersSize": 1024, @@ -56,7 +57,11 @@ "hashrateLargeWindow": "3h", "luckWindow": [64, 128, 256], "payments": 30, - "blocks": 50 + "blocks": 50, + "poolCharts":"0 */20 * * * *", + "poolChartsNum":74, + "minerCharts":"0 */20 * * * *", + "minerChartsNum":74 }, "upstreamCheckInterval": "5s", diff --git a/main.go b/main.go index faff57e60..f226e9115 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "runtime" "time" + "github.com/fatih/structs" "github.com/yvasiyarov/gorelic" "github.com/sammy007/open-ethereum-pool/api" @@ -28,7 +29,8 @@ func startProxy() { } func startApi() { - s := api.NewApiServer(&cfg.Api, backend) + settings := structs.Map(&cfg) + s := api.NewApiServer(&cfg.Api, settings, backend) s.Start() } diff --git a/payouts/unlocker.go b/payouts/unlocker.go index c073ef0b3..5b03bd85e 100644 --- a/payouts/unlocker.go +++ b/payouts/unlocker.go @@ -29,7 +29,8 @@ type UnlockerConfig struct { } const minDepth = 16 -const byzantiumHardForkHeight = 4370000 + +var byzantiumHardForkHeight int64 = 4370000 var homesteadReward = math.MustParseBig256("5000000000000000000") var byzantiumReward = math.MustParseBig256("3000000000000000000") @@ -58,6 +59,32 @@ func NewBlockUnlocker(cfg *UnlockerConfig, backend *storage.RedisClient) *BlockU } u := &BlockUnlocker{config: cfg, backend: backend} u.rpc = rpc.NewRPCClient("BlockUnlocker", cfg.Daemon, cfg.Timeout) + + block, err := u.rpc.GetBlockByHeight(0) + if err != nil || block == nil { + log.Fatalf("Error while retrieving genesis block from node: %v", err) + } + + // EtherSocial Network + if block.Hash == "0x310dd3c4ae84dd89f1b46cfdd5e26c8f904dfddddc73f323b468127272e20e9f" { + log.Printf("Found genesis.hash is %v", block.Hash) + byzantiumHardForkHeight = 600000 + homesteadReward = math.MustParseBig256("9000000000000000000") + byzantiumReward = math.MustParseBig256("5000000000000000000") + + log.Printf("Set byzantiumHardForkHeight to %v", byzantiumHardForkHeight) + log.Printf("Set homesteadReward to %v", homesteadReward) + log.Printf("Set byzantiumReward to %v", byzantiumReward) + } else if block.Hash == "0x82270b80fc90beb005505a9ef95039639968a0e81b2904ad30128c93d713d2c4" { + // CLO Network + log.Printf("Found genesis.hash is %v", block.Hash) + byzantiumHardForkHeight = 0 + byzantiumReward = math.MustParseBig256("420000000000000000000") + + log.Printf("Set byzantiumHardForkHeight(not used) to %v", byzantiumHardForkHeight) + log.Printf("Set homesteadReward(not used) to %v", homesteadReward) + log.Printf("Set byzantiumReward(CLO reward) to %v", byzantiumReward) + } return u } @@ -209,7 +236,7 @@ func (u *BlockUnlocker) handleBlock(block *rpc.GetBlockReply, candidate *storage return err } candidate.Height = correctHeight - reward := getConstReward(candidate.Height) + reward := GetConstReward(candidate.Height) // Add TX fees extraTxReward, err := u.getExtraRewardForTx(block) @@ -501,7 +528,7 @@ func weiToShannonInt64(wei *big.Rat) int64 { return value } -func getConstReward(height int64) *big.Int { +func GetConstReward(height int64) *big.Int { if height >= byzantiumHardForkHeight { return new(big.Int).Set(byzantiumReward) } @@ -509,12 +536,12 @@ func getConstReward(height int64) *big.Int { } func getRewardForUncle(height int64) *big.Int { - reward := getConstReward(height) + reward := GetConstReward(height) return new(big.Int).Div(reward, new(big.Int).SetInt64(32)) } func getUncleReward(uHeight, height int64) *big.Int { - reward := getConstReward(height) + reward := GetConstReward(height) k := height - uHeight reward.Mul(big.NewInt(8-k), reward) reward.Div(reward, big.NewInt(8)) diff --git a/proxy/config.go b/proxy/config.go index 6248c538c..06c024153 100644 --- a/proxy/config.go +++ b/proxy/config.go @@ -29,6 +29,7 @@ type Config struct { } type Proxy struct { + HashLimit int64 `json:"hashLimit"` Enabled bool `json:"enabled"` Listen string `json:"listen"` LimitHeadersSize int `json:"limitHeadersSize"` diff --git a/proxy/handlers.go b/proxy/handlers.go index 6f1fa902c..4ba4400bf 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -1,6 +1,7 @@ package proxy import ( + "fmt" "log" "regexp" "strings" @@ -69,14 +70,23 @@ func (s *ProxyServer) handleSubmitRPC(cs *Session, login, id string, params []st return false, &ErrorReply{Code: -1, Message: "Malformed PoW result"} } t := s.currentBlockTemplate() - exist, validShare := s.processShare(login, id, cs.ip, t, params) - ok := s.policy.ApplySharePolicy(cs.ip, !exist && validShare) + exist, validShare, extraErr := s.processShare(login, id, cs.ip, t, params) + ok := s.policy.ApplySharePolicy(cs.ip, !exist && validShare && extraErr == nil) if exist { log.Printf("Duplicate share from %s@%s %v", login, cs.ip, params) return false, &ErrorReply{Code: 22, Message: "Duplicate share"} } + if extraErr != nil { + log.Printf("Invalid share from %s@%s: %v", login, cs.ip, extraErr) + // Bad shares limit reached, return error and close + if !ok { + return false, &ErrorReply{Code: 23, Message: "Invalid share"} + } + return false, &ErrorReply{Code: 20, Message: fmt.Sprintf("Invalid share : %v", extraErr)} + } + if !validShare { log.Printf("Invalid share from %s@%s", login, cs.ip) // Bad shares limit reached, return error and close diff --git a/proxy/miner.go b/proxy/miner.go index 8d312f9d8..bddcfee3c 100644 --- a/proxy/miner.go +++ b/proxy/miner.go @@ -1,6 +1,7 @@ package proxy import ( + "fmt" "log" "math/big" "strconv" @@ -12,7 +13,7 @@ import ( var hasher = ethash.New() -func (s *ProxyServer) processShare(login, id, ip string, t *BlockTemplate, params []string) (bool, bool) { +func (s *ProxyServer) processShare(login, id, ip string, t *BlockTemplate, params []string) (bool, bool, error) { nonceHex := params[0] hashNoNonce := params[1] mixDigest := params[2] @@ -22,7 +23,7 @@ func (s *ProxyServer) processShare(login, id, ip string, t *BlockTemplate, param h, ok := t.headers[hashNoNonce] if !ok { log.Printf("Stale share from %v@%v", login, ip) - return false, false + return false, false, nil } share := Block{ @@ -42,7 +43,7 @@ func (s *ProxyServer) processShare(login, id, ip string, t *BlockTemplate, param } if !hasher.Verify(share) { - return false, false + return false, false, nil } if hasher.Verify(block) { @@ -51,12 +52,12 @@ func (s *ProxyServer) processShare(login, id, ip string, t *BlockTemplate, param log.Printf("Block submission failure at height %v for %v: %v", h.height, t.Header, err) } else if !ok { log.Printf("Block rejected at height %v for %v", h.height, t.Header) - return false, false + return false, false, nil } else { s.fetchBlockTemplate() exist, err := s.backend.WriteBlock(login, id, params, shareDiff, h.diff.Int64(), h.height, s.hashrateExpiration) if exist { - return true, false + return true, false, nil } if err != nil { log.Println("Failed to insert block candidate into backend:", err) @@ -66,13 +67,24 @@ func (s *ProxyServer) processShare(login, id, ip string, t *BlockTemplate, param log.Printf("Block found by miner %v@%v at height %d", login, ip, h.height) } } else { + // check hashrate limit + if s.config.Proxy.HashLimit > 0 { + currentHashrate, _ := s.backend.GetCurrentHashrate(login) + + if s.config.Proxy.HashLimit > 0 && currentHashrate > s.config.Proxy.HashLimit { + err := fmt.Errorf("hashLimit exceed: %v(current) > %v(hashLimit)", currentHashrate, s.config.Proxy.HashLimit) + log.Println("Failed to insert share data into backend:", err) + return false, false, err + } + } + exist, err := s.backend.WriteShare(login, id, params, shareDiff, h.height, s.hashrateExpiration) if exist { - return true, false + return true, false, nil } if err != nil { log.Println("Failed to insert share data into backend:", err) } } - return false, true + return false, true, nil } diff --git a/proxy/stratum.go b/proxy/stratum.go index ff3b61ac7..9ad26db64 100644 --- a/proxy/stratum.go +++ b/proxy/stratum.go @@ -114,13 +114,13 @@ func (cs *Session) handleTCPMessage(s *ProxyServer, req *StratumReq) error { if errReply != nil { return cs.sendTCPError(req.Id, errReply) } - return cs.sendTCPResult(req.Id, reply) + return cs.sendTCPResult(req.Id, reply, nil) case "eth_getWork": reply, errReply := s.handleGetWorkRPC(cs) if errReply != nil { return cs.sendTCPError(req.Id, errReply) } - return cs.sendTCPResult(req.Id, &reply) + return cs.sendTCPResult(req.Id, &reply, nil) case "eth_submitWork": var params []string err := json.Unmarshal(req.Params, ¶ms) @@ -130,22 +130,26 @@ func (cs *Session) handleTCPMessage(s *ProxyServer, req *StratumReq) error { } reply, errReply := s.handleTCPSubmitRPC(cs, req.Worker, params) if errReply != nil { - return cs.sendTCPError(req.Id, errReply) + if errReply.Code != 20 { + return cs.sendTCPError(req.Id, errReply) + } else { + return cs.sendTCPResult(req.Id, &reply, errReply) + } } - return cs.sendTCPResult(req.Id, &reply) + return cs.sendTCPResult(req.Id, &reply, nil) case "eth_submitHashrate": - return cs.sendTCPResult(req.Id, true) + return cs.sendTCPResult(req.Id, true, nil) default: errReply := s.handleUnknownRPC(cs, req.Method) return cs.sendTCPError(req.Id, errReply) } } -func (cs *Session) sendTCPResult(id json.RawMessage, result interface{}) error { +func (cs *Session) sendTCPResult(id json.RawMessage, result interface{}, reply *ErrorReply) error { cs.Lock() defer cs.Unlock() - message := JSONRpcResp{Id: id, Version: "2.0", Error: nil, Result: result} + message := JSONRpcResp{Id: id, Version: "2.0", Error: reply, Result: result} return cs.enc.Encode(&message) } diff --git a/storage/redis.go b/storage/redis.go index 449b58fcc..1d9ebf00a 100644 --- a/storage/redis.go +++ b/storage/redis.go @@ -2,6 +2,7 @@ package storage import ( "fmt" + "math" "math/big" "strconv" "strings" @@ -24,6 +25,26 @@ type RedisClient struct { prefix string } +type PoolCharts struct { + Timestamp int64 `json:"x"` + TimeFormat string `json:"timeFormat"` + PoolHash int64 `json:"y"` +} + +type MinerCharts struct { + Timestamp int64 `json:"x"` + TimeFormat string `json:"timeFormat"` + MinerHash int64 `json:"minerHash"` + MinerLargeHash int64 `json:"minerLargeHash"` + WorkerOnline string `json:"workerOnline"` +} + +type PaymentCharts struct { + Timestamp int64 `json:"x"` + TimeFormat string `json:"timeFormat"` + Amount int64 `json:"amount"` +} + type BlockData struct { Height int64 `json:"height"` Timestamp int64 `json:"timestamp"` @@ -79,12 +100,16 @@ type Worker struct { } func NewRedisClient(cfg *Config, prefix string) *RedisClient { - client := redis.NewClient(&redis.Options{ + options := redis.Options{ Addr: cfg.Endpoint, Password: cfg.Password, DB: cfg.Database, PoolSize: cfg.PoolSize, - }) + } + if cfg.Endpoint[0:1] == "/" { + options.Network = "unix" + } + client := redis.NewClient(&options) return &RedisClient{client: client, prefix: prefix} } @@ -118,6 +143,133 @@ func (r *RedisClient) GetWhitelist() ([]string, error) { return cmd.Val(), nil } +func (r *RedisClient) WritePoolCharts(time1 int64, time2 string, poolHash string) error { + s := join(time1, time2, poolHash) + cmd := r.client.ZAdd(r.formatKey("charts", "pool"), redis.Z{Score: float64(time1), Member: s}) + return cmd.Err() +} + +func (r *RedisClient) WriteMinerCharts(time1 int64, time2, k string, hash, largeHash, workerOnline int64) error { + s := join(time1, time2, hash, largeHash, workerOnline) + cmd := r.client.ZAdd(r.formatKey("charts", "miner", k), redis.Z{Score: float64(time1), Member: s}) + return cmd.Err() +} + +func (r *RedisClient) GetPoolCharts(poolHashLen int64) (stats []*PoolCharts, err error) { + + tx := r.client.Multi() + defer tx.Close() + + now := util.MakeTimestamp() / 1000 + + cmds, err := tx.Exec(func() error { + tx.ZRemRangeByScore(r.formatKey("charts", "pool"), "-inf", fmt.Sprint("(", now-172800)) + tx.ZRevRangeWithScores(r.formatKey("charts", "pool"), 0, poolHashLen) + return nil + }) + + if err != nil { + return nil, err + } + + stats = convertPoolChartsResults(cmds[1].(*redis.ZSliceCmd)) + return stats, nil +} + +func convertPoolChartsResults(raw *redis.ZSliceCmd) []*PoolCharts { + var result []*PoolCharts + for _, v := range raw.Val() { + // "Timestamp:TimeFormat:Hash" + pc := PoolCharts{} + pc.Timestamp = int64(v.Score) + str := v.Member.(string) + pc.TimeFormat = str[strings.Index(str, ":")+1 : strings.LastIndex(str, ":")] + pc.PoolHash, _ = strconv.ParseInt(str[strings.LastIndex(str, ":")+1:], 10, 64) + result = append(result, &pc) + } + var reverse []*PoolCharts + for i := len(result) - 1; i >= 0; i-- { + reverse = append(reverse, result[i]); + } + return reverse +} + +func convertMinerChartsResults(raw *redis.ZSliceCmd) []*MinerCharts { + var result []*MinerCharts + for _, v := range raw.Val() { + // "Timestamp:TimeFormat:Hash:largeHash:workerOnline" + mc := MinerCharts{} + mc.Timestamp = int64(v.Score) + str := v.Member.(string) + mc.TimeFormat = strings.Split(str, ":")[1] + mc.MinerHash, _ = strconv.ParseInt(strings.Split(str, ":")[2], 10, 64) + mc.MinerLargeHash, _ = strconv.ParseInt(strings.Split(str, ":")[3], 10, 64) + mc.WorkerOnline = strings.Split(str, ":")[4] + result = append(result, &mc) + } + var reverse []*MinerCharts + for i := len(result) - 1; i >= 0; i-- { + reverse = append(reverse, result[i]); + } + return reverse +} + +func (r *RedisClient) GetAllMinerAccount() (account []string, err error) { + var c int64 + for { + now := util.MakeTimestamp() / 1000 + c, keys, err := r.client.Scan(c, r.formatKey("miners", "*"), now).Result() + + if err != nil { + return account, err + } + for _, key := range keys { + m := strings.Split(key, ":") + //if ( len(m) >= 2 && strings.Index(strings.ToLower(m[2]), "0x") == 0) { + if len(m) >= 2 { + account = append(account, m[2]) + } + } + if c == 0 { + break + } + } + return account, nil +} + +func (r *RedisClient) GetMinerCharts(hashNum int64, login string) (stats []*MinerCharts, err error) { + + tx := r.client.Multi() + defer tx.Close() + now := util.MakeTimestamp() / 1000 + cmds, err := tx.Exec(func() error { + tx.ZRemRangeByScore(r.formatKey("charts", "miner", login), "-inf", fmt.Sprint("(", now-172800)) + tx.ZRevRangeWithScores(r.formatKey("charts", "miner", login), 0, hashNum) + return nil + }) + if err != nil { + return nil, err + } + stats = convertMinerChartsResults(cmds[1].(*redis.ZSliceCmd)) + return stats, nil +} + +func (r *RedisClient) GetPaymentCharts(login string) (stats []*PaymentCharts, err error) { + + tx := r.client.Multi() + defer tx.Close() + cmds, err := tx.Exec(func() error { + tx.ZRevRangeWithScores(r.formatKey("payments", login), 0, 360) + return nil + }) + if err != nil { + return nil, err + } + stats = convertPaymentChartsResults(cmds[0].(*redis.ZSliceCmd)) + //fmt.Println(stats) + return stats, nil +} + func (r *RedisClient) WriteNodeState(id string, height uint64, diff *big.Int) error { tx := r.client.Multi() defer tx.Close() @@ -761,6 +913,9 @@ func (r *RedisClient) CollectWorkersStats(sWindow, lWindow time.Duration, login stats["workersOffline"] = offline stats["hashrate"] = totalHashrate stats["currentHashrate"] = currentHashrate + + tx.HSet(r.formatKey("currenthashrate", login), "hashrate", strconv.FormatInt(currentHashrate, 10)) + tx.Expire(r.formatKey("currenthashrate", login), lWindow) return stats, nil } @@ -955,5 +1110,54 @@ func convertPaymentsResults(raw *redis.ZSliceCmd) []map[string]interface{} { } result = append(result, tx) } - return result + var reverse []map[string]interface{} + for i := len(result) - 1; i >= 0; i-- { + reverse = append(reverse, result[i]); + } + return reverse +} + +/* +Timestamp int64 `json:"x"` +TimeFormat string `json:"timeFormat"` +Amount int64 `json:"amount"` +*/ +func convertPaymentChartsResults(raw *redis.ZSliceCmd) []*PaymentCharts { + var result []*PaymentCharts + for _, v := range raw.Val() { + pc := PaymentCharts{} + pc.Timestamp = int64(v.Score) + tm := time.Unix(pc.Timestamp, 0) + pc.TimeFormat = tm.Format("2006-01-02") + " 00_00" + fields := strings.Split(v.Member.(string), ":") + pc.Amount, _ = strconv.ParseInt(fields[1], 10, 64) + //fmt.Printf("%d : %s : %d \n", pc.Timestamp, pc.TimeFormat, pc.Amount) + + var chkAppend bool + for _, pcc := range result { + if pcc.TimeFormat == pc.TimeFormat { + pcc.Amount += pc.Amount + chkAppend = true + } + } + if !chkAppend { + pc.Timestamp -= int64(math.Mod(float64(v.Score), float64(86400))) + result = append(result, &pc) + } + } + var reverse []*PaymentCharts + for i := len(result) - 1; i >= 0; i-- { + reverse = append(reverse, result[i]); + } + return reverse +} + +func (r *RedisClient) GetCurrentHashrate(login string) (int64, error) { + hashrate := r.client.HGet(r.formatKey("currenthashrate", login), "hashrate") + if hashrate.Err() == redis.Nil { + return 0, nil + } else if hashrate.Err() != nil { + return 0, hashrate.Err() + } + return hashrate.Int64() } diff --git a/www/app/controllers/account.js b/www/app/controllers/account.js index 79782f798..4100c8e60 100644 --- a/www/app/controllers/account.js +++ b/www/app/controllers/account.js @@ -2,8 +2,146 @@ import Ember from 'ember'; export default Ember.Controller.extend({ applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config'), stats: Ember.computed.reads('applicationController.model.stats'), + hashrate: Ember.computed.reads('applicationController.hashrate'), + chartOptions: Ember.computed("model.hashrate", { + get() { + var e = this, + t = e.getWithDefault("model.minerCharts"), + a = { + chart: { + backgroundColor: "rgba(255, 255, 255, 0.1)", + type: "spline", + marginRight: 10, + height: 400, + events: { + load: function() { + var series = this.series[0]; + setInterval(function() { + var now = new Date(); + var shift = false; + if (now - series.data[0].x > 6*60*60*1000) { + shift = true; + } + var x = now, + y = e.getWithDefault("model.currentHashrate"); + var d = x.toLocaleString(); + series.addPoint({x:x, y:y, d:d}, true, shift); + }, 10000); + } + } + }, + title: { + text: "" + }, + xAxis: { + ordinal: false, + type: "datetime", + dateTimeLabelFormats: { + millisecond: "%H:%M:%S", + second: "%H:%M:%S", + minute: "%H:%M", + hour: "%H:%M", + day: "%e. %b", + week: "%e. %b", + month: "%b '%y", + year: "%Y" + } + }, + yAxis: { + title: { + text: "HashRate" + }, + min: 0 + }, + plotLines: [{ + value: 0, + width: 1, + color: "#808080" + }], + legend: { + enabled: true + }, + tooltip: { + formatter: function() { + return this.y > 1000000000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000000000).toFixed(2) + " TH/s
" : this.y > 1000000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000000).toFixed(2) + " GH/s
" : this.y > 1000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000).toFixed(2) + " MH/s
" : "" + this.point.d + "
Hashrate " + this.y.toFixed(2) + " H/s"; + + }, + + useHTML: true + }, + exporting: { + enabled: false + }, + series: [{ + color: "#E99002", + name: "Average hashrate", + data: function() { + var e, a = []; + if (null != t) { + for (e = 0; e <= t.length - 1; e += 1) { + var n = 0, + r = 0, + l = 0; + r = new Date(1e3 * t[e].x); + l = r.toLocaleString(); + n = t[e].minerLargeHash; + a.push({ + x: r, + d: l, + y: n + }); + } + } else { + a.push({ + x: 0, + d: 0, + y: 0 + }); + } + return a; + }() + }, { + name: "Current hashrate", + data: function() { + var e, a = []; + if (null != t) { + for (e = 0; e <= t.length - 1; e += 1) { + var n = 0, + r = 0, + l = 0; + r = new Date(1e3 * t[e].x); + l = r.toLocaleString(); + n = t[e].minerHash; + a.push({ + x: r, + d: l, + y: n + }); + } + } else { + a.push({ + x: 0, + d: 0, + y: 0 + }); + } + return a; + }() + }] + }; + a.title.text = this.get('config.highcharts.account.title') || ""; + a.yAxis.title.text = this.get('config.highcharts.account.ytitle') || "Hashrate"; + a.chart.height = this.get('config.highcharts.account.height') || 300; + a.chart.type = this.get('config.highcharts.account.type') || 'spline'; + var colors = this.get('config.highcharts.account.color'); + a.series[0].color = colors[0] || '#e99002'; + a.series[1].color = colors[1] || '#1994b8'; + return a; + } + }), roundPercent: Ember.computed('stats', 'model', { get() { var percent = this.get('model.roundShares') / this.get('stats.roundShares'); @@ -12,5 +150,18 @@ export default Ember.Controller.extend({ } return percent; } + }), + + netHashrate: Ember.computed({ + get() { + return this.get('hashrate'); + } + }), + + earnPerDay: Ember.computed('model', { + get() { + return 24 * 60 * 60 / this.get('config').BlockTime * this.get('config').BlockReward * + this.getWithDefault('model.hashrate') / this.get('hashrate'); + } }) }); diff --git a/www/app/controllers/account/index.js b/www/app/controllers/account/index.js new file mode 100644 index 000000000..c345671bf --- /dev/null +++ b/www/app/controllers/account/index.js @@ -0,0 +1,133 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config'), + netstats: Ember.computed.reads('applicationController'), + stats: Ember.computed.reads('applicationController.model.stats'), + + chartOptions: Ember.computed("model", { + get() { + var now = new Date(); + var e = this, + t = e.getWithDefault("model.minerCharts"), + a = { + chart: { + backgroundColor: "rgba(255, 255, 255, 0.1)", + type: "spline", + marginRight: 10, + height: 200, + events: { + load: function() { + var self = this; + setInterval(function() { + var series = self.series; + if (!series) { + return; // FIXME + } + var now = new Date(); + var shift = false; + if (series && series[0] && series[0].data && series[0].data[0] && now - series[0].data[0].x > 6*60*60*1000) { + shift = true; + } + var y = e.getWithDefault("model.hashrate"), + z = e.getWithDefault("model.currentHashrate"); + var d = now.toLocaleString(); + self.series[0].addPoint({x: now, y: y, d: d}, true, shift); + self.series[1].addPoint({x: now, y: z, d: d}, true, shift); + }, e.get('config.highcharts.account.interval') || 120000); + } + } + }, + title: { + text: "" + }, + xAxis: { + ordinal: false, + type: "datetime", + dateTimeLabelFormats: { + millisecond: "%H:%M:%S", + second: "%H:%M:%S", + minute: "%H:%M", + hour: "%H:%M", + day: "%e. %b", + week: "%e. %b", + month: "%b '%y", + year: "%Y" + } + }, + yAxis: { + title: { + text: "Hashrate by Account" + }, + //softMin: e.getWithDefault("model.currentHashrate") / 1000000, + //softMax: e.getWithDefault("model.currentHashrate") / 1000000, + }, + plotLines: [{ + value: 0, + width: 1, + color: "#808080" + }], + legend: { + enabled: true + }, + tooltip: { + formatter: function() { + return this.y > 1000000000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000000000).toFixed(2) + " TH/s
" : this.y > 1000000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000000).toFixed(2) + " GH/s
" : this.y > 1000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000).toFixed(2) + " MH/s
" : "" + this.point.d + "
Hashrate " + this.y.toFixed(2) + " H/s"; + + }, + + useHTML: true + }, + exporting: { + enabled: false + }, + series: [{ + color: "#E99002", + name: "3 hours average hashrate", + data: function() { + var a = []; + if (null != t) { + t.forEach(function(e) { + var x = new Date(1000 * e.x); + var l = x.toLocaleString(); + var y = e.minerLargeHash; + a.push({x: x, y: y, d: l}); + }); + } + var l = now.toLocaleString(); + var y = e.getWithDefault("model.hashrate"); + var last = {x: now, y: y, d: l}; + var interval = e.get('config.highcharts.account.interval') || 120000; + if (a.length > 0 && now - a[a.length - 1].x > interval) { + a.push(last); + } + return a; + }() + }, { + name: "30 minutes average hashrate", + data: function() { + var a = []; + if (null != t) { + t.forEach(function(e) { + var x = new Date(1000 * e.x); + var l = x.toLocaleString(); + var y = e.minerHash; + a.push({x: x, y: y, d: l}); + }); + } + var l = now.toLocaleString(); + var y = e.getWithDefault("model.currentHashrate"); + var last = {x: now, y: y, d: l}; + var interval = e.get('config.highcharts.account.interval') || 120000; + if (a.length > 0 && now - a[a.length - 1].x > interval) { + a.push(last); + } + return a; + }() + }] + }; + return a; + } + }) +}); diff --git a/www/app/controllers/account/payouts.js b/www/app/controllers/account/payouts.js new file mode 100644 index 000000000..6188351d0 --- /dev/null +++ b/www/app/controllers/account/payouts.js @@ -0,0 +1,111 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config'), + stats: Ember.computed.reads('applicationController.model.stats'), + intl: Ember.inject.service(), + + chartPaymentText: Ember.computed('model', { + get() { + var outText = this.get('model.paymentCharts'); + if (!outText) { + return 0; + } + return outText; + } + }), + + chartPayment: Ember.computed('intl', 'model.paymentCharts', { + get() { + var e = this, + t = e.getWithDefault("model.paymentCharts"), + a = { + chart: { + backgroundColor: "rgba(255, 255, 255, 0.1)", + type: "column", + marginRight: 10, + height: 200, + events: { + load: function() { + var self = this; + setInterval(function() { + if (!self.series) { + return; // FIXME + } + t = e.getWithDefault("model.paymentCharts"); + var data = []; + t.forEach(function(d) { + var r = new Date(1000 * d.x); + var l = r.toLocaleString(); + var n = e.amount / 1000000000; + data.push({x: r, d: l, y: n}); + }); + self.series[0].setData(data, true, {}, true); + }, e.get('config.highcharts.account.paymentInterval') || 120000); + } + } + }, + title: { + text: "" + }, + xAxis: { + ordinal: false, + type: "datetime", + dateTimeLabelFormats: { + day: "%e. %b", + week: "%e. %b", + month: "%b '%y", + year: "%Y" + } + }, + yAxis: { + title: { + text: "Payment by Account" + } + }, + plotLines: [{ + value: 0, + width: 1, + color: "#808080" + }], + legend: { + enabled: true + }, + tooltip: { + formatter: function() { + return "" + Highcharts.dateFormat('%Y-%m-%d', new Date(this.x)) + "
Payment " + this.y.toFixed(4) + " " + e.get('config.Unit') + ""; + }, + useHTML: true + }, + exporting: { + enabled: false + }, + series: [{ + color: "#E99002", + name: "Payment Series", + data: function() { + var a = []; + if (null != t) { + t.forEach(function(d) { + var r = new Date(1000 * d.x); + var l = r.toLocaleString(); + var n = d.amount / 1000000000; + a.push({x: r, d: l, y: n}); + }); + } + var now = new Date(); + var l = now.toLocaleString(); + var last = {x: now, d: l, y: 0}; + var interval = e.get('config.highcharts.account.interval') || 120000; + if (a.length > 0 && now - a[a.length - 1].x > interval) { + a.push(last); + } + return a; + }() + }] + }; + return a; + } +}) +}); diff --git a/www/app/controllers/application.js b/www/app/controllers/application.js index 1e40cd9eb..c7a3436fd 100644 --- a/www/app/controllers/application.js +++ b/www/app/controllers/application.js @@ -2,6 +2,7 @@ import Ember from 'ember'; import config from '../config/environment'; export default Ember.Controller.extend({ + intl: Ember.inject.service(), get config() { return config.APP; }, @@ -65,6 +66,25 @@ export default Ember.Controller.extend({ } }), + languages: Ember.computed('model', { + get() { + return this.get('model.languages'); + } + }), + + selectedLanguage: Ember.computed({ + get() { + var langs = this.get('languages'); + var lang = Ember.$.cookie('lang'); + for (var i = 0; i < langs.length; i++) { + if (langs[i].value == lang) { + return langs[i].name; + } + } + return lang; + } + }), + roundVariance: Ember.computed('model', { get() { var percent = this.get('model.stats.roundShares') / this.get('difficulty'); diff --git a/www/app/controllers/blocks.js b/www/app/controllers/blocks.js new file mode 100644 index 000000000..384e3ac3b --- /dev/null +++ b/www/app/controllers/blocks.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config'), + settings: Ember.computed.reads('applicationController.model.settings'), + + BlockUnlockDepth: Ember.computed('settings', { + get() { + var depth = this.get('settings.BlockUnlockDepth'); + if (depth) { + return depth; + } + return this.get('config').BlockUnlockDepth; + } + }), + +}); diff --git a/www/app/controllers/blocks/block.js b/www/app/controllers/blocks/block.js new file mode 100644 index 000000000..12b7f9a34 --- /dev/null +++ b/www/app/controllers/blocks/block.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config') +}); diff --git a/www/app/controllers/blocks/immature.js b/www/app/controllers/blocks/immature.js new file mode 100644 index 000000000..12b7f9a34 --- /dev/null +++ b/www/app/controllers/blocks/immature.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config') +}); diff --git a/www/app/controllers/blocks/index.js b/www/app/controllers/blocks/index.js new file mode 100644 index 000000000..12b7f9a34 --- /dev/null +++ b/www/app/controllers/blocks/index.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config') +}); diff --git a/www/app/controllers/blocks/pending.js b/www/app/controllers/blocks/pending.js new file mode 100644 index 000000000..12b7f9a34 --- /dev/null +++ b/www/app/controllers/blocks/pending.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config') +}); diff --git a/www/app/controllers/help-ko.js b/www/app/controllers/help-ko.js new file mode 100644 index 000000000..12b7f9a34 --- /dev/null +++ b/www/app/controllers/help-ko.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config') +}); diff --git a/www/app/controllers/index.js b/www/app/controllers/index.js index 9654c41fd..f3073c620 100644 --- a/www/app/controllers/index.js +++ b/www/app/controllers/index.js @@ -4,6 +4,39 @@ export default Ember.Controller.extend({ applicationController: Ember.inject.controller('application'), stats: Ember.computed.reads('applicationController'), config: Ember.computed.reads('applicationController.config'), + settings: Ember.computed.reads('applicationController.model.settings'), + + // try to read some settings from the model.settings + PayoutThreshold: Ember.computed('settings', { + get() { + var threshold = this.get('settings.PayoutThreshold'); + if (threshold) { + // in shannon (10**9) + return threshold / 1000000000; + } + return this.get('config').PayoutThreshold; + } + }), + + PayoutInterval: Ember.computed('settings', { + get() { + var interval = this.get('settings.PayoutInterval'); + if (interval) { + return interval; + } + return this.get('config').PayoutInterval; + } + }), + + PoolFee: Ember.computed('settings', { + get() { + var poolfee = this.get('settings.PoolFee'); + if (poolfee) { + return poolfee + '%'; + } + return this.get('config').PoolFee; + } + }), cachedLogin: Ember.computed('login', { get() { @@ -14,5 +47,111 @@ export default Ember.Controller.extend({ this.set('model.login', value); return value; } - }) + }), + chartOptions: Ember.computed("model.hashrate", { + get() { + var now = new Date(); + var e = this, + t = e.getWithDefault("stats.model.poolCharts"), + a = { + chart: { + backgroundColor: "rgba(255, 255, 255, 0.1)", + type: "spline", + height: 300, + marginRight: 10, + events: { + load: function() { + var self = this; + setInterval(function() { + if (!self.series) { + return; // FIXME + } + var series = self.series[0]; + var now = new Date(); + var shift = false; + if (series && series.data && now - series.data[0].x > 6*60*60*1000) { + shift = true; + } + var x = now, y = e.getWithDefault("model.hashrate"); + var d = x.toLocaleString(); + series.addPoint({x: x, y: y, d:d}, true, shift); + }, e.get('config.highcharts.main.interval') || 60000); + } + } + }, + title: { + text: "Our pool's hashrate" + }, + xAxis: { + labels: { + style: { + color: "#000" + } + }, + ordinal: false, + type: "datetime" + }, + yAxis: { + title: { + text: "Pool Hashrate", + style: { + color: "#000" + } + }, + min: 0, + labels: { + style: { + color: "#000" + } + } + }, + plotLines: [{ + value: 0, + width: 1, + color: "#000" + }], + legend: { + enabled: false + }, + tooltip: { + formatter: function() { + return this.y > 1000000000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000000000).toFixed(2) + " TH/s
" : this.y > 1000000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000000).toFixed(2) + " GH/s
" : this.y > 1000000 ? "" + this.point.d + "
Hashrate " + (this.y / 1000000).toFixed(2) + " MH/s
" : "" + this.point.d + "
Hashrate " + this.y.toFixed(2) + " H/s"; + }, + useHTML: true + }, + exporting: { + enabled: false + }, + series: [{ + color: "#15BD27", + name: "Hashrate", + data: function() { + var a = []; + if (null != t) { + t.forEach(function(d) { + var x = new Date(1000 * d.x); + var l = x.toLocaleString(); + var y = d.y; + a.push({x: x, y: y, d: l}); + }); + } + var l = now.toLocaleString(); + var y = e.getWithDefault("model.hashrate"); + var last = {x: now, y: y, d: l}; + var interval = e.get('config.highcharts.main.interval') || 60000; + if (a.length > 0 && now - a[a.length - 1].x > interval) { + a.push(last); + } + return a; + }() + }] + }; + a.title.text = this.get('config.highcharts.main.title') || ""; + a.yAxis.title.text = this.get('config.highcharts.main.ytitle') || "Pool Hashrate"; + a.chart.height = this.get('config.highcharts.main.height') || 300; + a.chart.type = this.get('config.highcharts.main.type') || 'spline'; + a.series[0].color = this.get('config.highcharts.main.color') || '#15b7bd'; + return a; + } + }) }); diff --git a/www/app/controllers/payments.js b/www/app/controllers/payments.js new file mode 100644 index 000000000..12b7f9a34 --- /dev/null +++ b/www/app/controllers/payments.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + applicationController: Ember.inject.controller('application'), + config: Ember.computed.reads('applicationController.config') +}); diff --git a/www/app/helpers/worker-earnperday.js b/www/app/helpers/worker-earnperday.js new file mode 100644 index 000000000..898374271 --- /dev/null +++ b/www/app/helpers/worker-earnperday.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; +import config from '../config/environment'; + +export default Ember.Helper.extend({ + compute(hashrates) { + return 24 * 60 * 60 / config.APP.BlockTime * (hashrates[0] / hashrates[1]) * config.APP.BlockReward; + } +}); diff --git a/www/app/index.html b/www/app/index.html index d161fdc8d..5082c92c9 100644 --- a/www/app/index.html +++ b/www/app/index.html @@ -9,6 +9,7 @@ {{content-for "head"}} + {{content-for "head-footer"}} diff --git a/www/app/router.js b/www/app/router.js index 9eac95d62..7b1970c4e 100644 --- a/www/app/router.js +++ b/www/app/router.js @@ -17,9 +17,11 @@ Router.map(function() { }); this.route('help'); + this.route('help-ko'); this.route('payments'); this.route('miners'); this.route('about'); + this.route('about-ko'); }); export default Router; diff --git a/www/app/routes/account.js b/www/app/routes/account.js index b5744f6c4..d35d00532 100644 --- a/www/app/routes/account.js +++ b/www/app/routes/account.js @@ -2,9 +2,27 @@ import Ember from 'ember'; import config from '../config/environment'; export default Ember.Route.extend({ + minerCharts: null, + paymentCharts: null, + chartTimestamp: 0, + model: function(params) { var url = config.APP.ApiUrl + 'api/accounts/' + params.login; + let charts = this.get('minerCharts'); + if (!charts || new Date().getTime() - this.getWithDefault('chartTimestamp', 0) > (config.APP.highcharts.account.chartInterval || 900000 /* 15 min */)) { + url += '/chart'; + charts = null; + } + let self = this; return Ember.$.getJSON(url).then(function(data) { + if (!charts) { + self.set('minerCharts', data.minerCharts); + self.set('paymentCharts', data.paymentCharts); + self.set('chartTimestamp', new Date().getTime()); + } else { + data.minerCharts = self.get('minerCharts'); + data.paymentCharts = self.get('paymentCharts'); + } data.login = params.login; return Ember.Object.create(data); }); @@ -12,7 +30,6 @@ export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); - Ember.run.later(this, this.refresh, 5000); }, actions: { diff --git a/www/app/routes/application.js b/www/app/routes/application.js index 293d5ce61..e4a63144c 100644 --- a/www/app/routes/application.js +++ b/www/app/routes/application.js @@ -1,21 +1,130 @@ import Ember from 'ember'; import config from '../config/environment'; +function selectLocale(selected) { + // FIXME + let supported = ['en', 'ko', 'en-us']; + const language = navigator.languages[0] || navigator.language || navigator.userLanguage; + + let locale = selected; + + if (locale == null) { + // default locale + locale = language; + if (supported.indexOf(locale) < 0) { + locale = locale.replace(/\-[a-zA-Z]*$/, ''); + } + } + if (supported.indexOf(locale) >= 0) { + if (locale === 'en') { + locale = 'en-us'; + } + } else { + locale = 'en-us'; + } + return locale; +} + export default Ember.Route.extend({ intl: Ember.inject.service(), + selectedLanguage: null, + languages: null, + poolSettings: null, + poolCharts: null, + chartTimestamp: 0, beforeModel() { - this.get('intl').setLocale('en-us'); + let locale = this.get('selectedLanguage'); + if (!locale) { + // read cookie + locale = Ember.$.cookie('lang'); + // pick a locale + locale = selectLocale(locale); + + this.get('intl').setLocale(locale); + Ember.$.cookie('lang', locale); + console.log('INFO: locale selected - ' + locale); + this.set('selectedLanguage', locale); + } + + let intl = this.get('intl'); + this.set('languages', [ + { name: intl.t('lang.korean'), value: 'ko'}, + { name: intl.t('lang.english'), value: 'en-us'} + ]); + + let settings = this.get('poolSettings'); + if (!settings) { + let self = this; + let url = config.APP.ApiUrl + 'api/settings'; + Ember.$.ajax({ + url: url, + type: 'GET', + header: { + 'Accept': 'application/json' + }, + success: function(data) { + settings = Ember.Object.create(data); + self.set('poolSettings', settings); + console.log('INFO: pool settings loaded..'); + }, + error: function(request, status, e) { + console.log('ERROR: fail to load pool settings: ' + e); + self.set('poolSettings', {}); + } + }); + } + }, + + actions: { + selectLanguage: function(lang) { + let selected = lang; + if (typeof selected === 'undefined') { + return true; + } + let locale = selectLocale(selected); + this.get('intl').setLocale(locale); + this.set('selectedLanguage', locale); + Ember.$.cookie('lang', locale); + let languages = this.get('languages'); + for (var i = 0; i < languages.length; i++) { + if (languages[i].value == locale) { + Ember.$('#selectedLanguage').html(languages[i].name + ''); + break; + } + } + + return true; + }, + + toggleMenu: function() { + Ember.$('.navbar-collapse.in').attr("aria-expanded", false).removeClass("in"); + } }, model: function() { var url = config.APP.ApiUrl + 'api/stats'; + let charts = this.get('poolCharts'); + if (!charts || new Date().getTime() - this.getWithDefault('chartTimestamp', 0) > (config.APP.highcharts.main.chartInterval || 900000 /* 15 min */)) { + url += '/chart'; + charts = null; + } + let self = this; return Ember.$.getJSON(url).then(function(data) { + if (!charts) { + self.set('poolCharts', data.poolCharts); + self.set('chartTimestamp', new Date().getTime()); + } else { + data.poolCharts = self.get('poolCharts'); + } return Ember.Object.create(data); }); }, setupController: function(controller, model) { + let settings = this.get('poolSettings'); + model.settings = settings; + model.languages = this.get('languages'); this._super(controller, model); Ember.run.later(this, this.refresh, 5000); } diff --git a/www/app/routes/blocks.js b/www/app/routes/blocks.js index 975d6980b..483f96f3d 100644 --- a/www/app/routes/blocks.js +++ b/www/app/routes/blocks.js @@ -27,6 +27,5 @@ export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); - Ember.run.later(this, this.refresh, 5000); } }); diff --git a/www/app/routes/miners.js b/www/app/routes/miners.js index 303c416bf..e89458ba3 100644 --- a/www/app/routes/miners.js +++ b/www/app/routes/miners.js @@ -29,6 +29,5 @@ export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); - Ember.run.later(this, this.refresh, 5000); } }); diff --git a/www/app/routes/payments.js b/www/app/routes/payments.js index aa52f04aa..fdf0b89a9 100644 --- a/www/app/routes/payments.js +++ b/www/app/routes/payments.js @@ -17,6 +17,5 @@ export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); - Ember.run.later(this, this.refresh, 5000); } }); diff --git a/www/app/styles/app.css b/www/app/styles/app.css index ab6b45cc5..4cde4b84b 100644 --- a/www/app/styles/app.css +++ b/www/app/styles/app.css @@ -130,6 +130,14 @@ span.logo-3 { font-weight: 200; } +h1, h2, h3, h4, h4, h6 { + margin-top: 0px; +} + +.container>h2, .container>h3 { + margin-top:2.0rem; +} + .note { margin: 0 0 20px 0; padding: 15px 30px 15px 15px; @@ -142,6 +150,12 @@ span.logo-3 { border-color: #57b5e3; } +.note code { + background-color: transparent; + font-size: 16px; + color: #000; +} + .note-danger { background-color: #ff9999; border-color: #ff0000; @@ -176,3 +190,22 @@ h4.note { .stats > div > span:first-of-type{ font-weight: bold; } + +/* tabs */ +.tab-content>.tab-pane { + background-color: #ffffff; + padding: 1em; + border-radius: 0 0 5px 5px; + border: 1px #eeeeee solid; + border-width: 0 1px 1px 1px; +} + +code { + font-size: 100%; + /* color: #444f67; /* */ + background-color: #f7f7f7; +} + +code em { + color: #3b9218; +} diff --git a/www/app/templates/about-ko.hbs b/www/app/templates/about-ko.hbs new file mode 100644 index 000000000..ef5a4aa0d --- /dev/null +++ b/www/app/templates/about-ko.hbs @@ -0,0 +1,22 @@ +
+ +

이용약관

+

이곳에서 사용하고 있는 자유 소프트웨어 때문에 발생가능할 수 있는 문제에 대해서 보증하지 않습니다.
+ 이 풀을 이용하는 사용자는 이로 인하여 발생할 수 있는 문제에 대하여 인정하는 것으로 간주합니다.
+ 풀 운영자는 되돌릴 수 없는 손실에 대해 보상할 수 없습니다만, 그렇지 않은 경우에 대해서는 최악의 상황을 막기 위해서 최선을 다하고 있습니다. +

+ +

풀의 지원 사항

+

+

    +
  • Go 언어로 작성되어 매우 빠른 동시처리와 낮은 메모리 사용률을 가집니다
  • +
  • 최상의 성능의 프록시
  • +
  • 출금 및 블럭 unlocking 모듈 지원
  • +
  • 100% 분산 환경으로 디자인 됨
  • +
  • Strict policy 정책 모듈 지원
  • +
  • 아름다운 현대적 Ember.js 프론트엔드 사용
  • +
+

+
diff --git a/www/app/templates/account.hbs b/www/app/templates/account.hbs index 2f67af466..237a62a6b 100644 --- a/www/app/templates/account.hbs +++ b/www/app/templates/account.hbs @@ -4,40 +4,41 @@
- Immature Balance: {{format-balance model.stats.immature}}
- Preliminary balance awaiting blocks to mature. + {{t "account.immature.balance"}}: {{format-balance model.stats.immature}} {{config.Unit}}
+ {{t "account.immature.description"}}
- Pending Balance: {{format-balance model.stats.balance}}
- Credited coins awaiting payout. + {{t "account.pending.balance"}}: {{format-balance model.stats.balance}} {{config.Unit}}
+ {{t "account.pending.description"}}
{{#if model.stats.pending}}
- Current Payment: {{format-balance model.stats.pending}}
+ {{t "account.current"}}: {{format-balance model.stats.pending}} {{config.Unit}}
{{/if}} -
Total Paid: {{format-balance model.stats.paid}}
+
{{t "account.total.paid"}}: {{format-balance model.stats.paid}} {{config.Unit}}
{{#if model.stats.lastShare}} -
- Last Share Submitted: {{format-relative (seconds-to-ms (string-to-int model.stats.lastShare))}} +
+ {{t "account.last_share_submitted"}}: {{format-relative (seconds-to-ms (string-to-int model.stats.lastShare))}}
{{/if}} -
Workers Online: {{format-number model.workersOnline}}
-
Hashrate (30m): {{format-hashrate model.currentHashrate}}
-
Hashrate (3h): {{format-hashrate model.hashrate}}
+
{{t "account.online"}}: {{format-number model.workersOnline}}
+
{{t "account.hashrate"}} (30m): {{format-hashrate model.currentHashrate}}
+
{{t "account.hashrate"}} (3h): {{format-hashrate model.hashrate}}
+
{{t "account.earnings.perday"}} (3h avg): {{format-number earnPerDay}} {{config.Unit}}
-
Blocks Found: {{format-number model.stats.blocksFound fallback='0'}}
-
Total Payments: {{format-number model.paymentsTotal}}
+
{{t "account.blocks.found"}}: {{format-number model.stats.blocksFound fallback='0'}}
+
{{t "account.total.payments"}}: {{format-number model.paymentsTotal}}
- Your Round Share: {{format-number roundPercent style='percent' maximumFractionDigits='6'}}
- Percent of your contribution to current round. + {{t "account.round_share"}}: {{format-number roundPercent style='percent' maximumFractionDigits='4'}}
+ {{t "account.round_share_description"}}
- - Epoch Switch: {{format-relative applicationController.nextEpoch units="hour"}} + + {{t "account.epoch_switch"}}: {{format-relative applicationController.nextEpoch units="hour"}}
@@ -45,12 +46,13 @@
+
diff --git a/www/app/templates/account/index.hbs b/www/app/templates/account/index.hbs index 4fb7975f1..34aff3fe3 100644 --- a/www/app/templates/account/index.hbs +++ b/www/app/templates/account/index.hbs @@ -1,14 +1,18 @@
+{{#if config.highcharts.account.enabled}} + {{high-charts mode=chartMode chartOptions=chartOptions content=chartData}} +{{/if}} {{#if model.workers}} -

Your Workers

+

{{t "account.your_workers"}}

- - - + + + + @@ -17,6 +21,7 @@ + {{/each-in}} @@ -24,16 +29,13 @@
IDHashrate (rough, short average)Hashrate (accurate, long average)Last Share{{t "account.hashrate"}} ({{t "account.short_average"}}){{t "account.hashrate"}} ({{t "account.long_average"}}){{t "account.earnings.perday"}} ({{t "account.earnings.short_avg"}}){{t "account.last_share"}}
{{k}} {{format-hashrate v.hr}} {{format-hashrate v.hr2}}{{format-number (worker-earnperday v.hr netstats.hashrate)}} {{format-relative (seconds-to-ms v.lastBeat)}}
{{else}} -

No workers online

+

{{t "account.no_workers_online"}}

{{/if}}
diff --git a/www/app/templates/account/payouts.hbs b/www/app/templates/account/payouts.hbs index 7ef5b7f88..dd1410adf 100644 --- a/www/app/templates/account/payouts.hbs +++ b/www/app/templates/account/payouts.hbs @@ -1,13 +1,16 @@
+{{#if config.highcharts.account.enabled}} + {{high-charts mode=chartMode chartOptions=chartPayment content=chartData}} +{{/if}} {{#if model.payments}} -

Your Latest Payouts

+

{{t "payout.latest_payouts"}}

- - - + + + @@ -15,15 +18,15 @@ - + {{/each}}
TimeTx IDAmount{{t "payout.time"}}{{t "payout.txid"}}{{t "payout.amount"}}
{{format-date-locale tx.timestamp}} - {{tx.tx}} + {{tx.tx}} {{format-balance tx.amount}}{{format-balance tx.amount}} {{config.Unit}}
{{else}} -

No payouts yet

+

{{t "payout.no_payouts_yet"}}

{{/if}}
diff --git a/www/app/templates/application.hbs b/www/app/templates/application.hbs index a85d241ab..3befd021e 100644 --- a/www/app/templates/application.hbs +++ b/www/app/templates/application.hbs @@ -8,23 +8,23 @@ - ΞthereumPool + {{config.PoolName}} Pool diff --git a/www/app/templates/blocks.hbs b/www/app/templates/blocks.hbs index e062465ae..7a0b79fd6 100644 --- a/www/app/templates/blocks.hbs +++ b/www/app/templates/blocks.hbs @@ -1,10 +1,9 @@
-

Pool always pay full block reward including TX fees and uncle rewards.

- - Block maturity requires up to 520 blocks. - Usually it's less indeed. - +

{{t "block.pool_rewards"}}

+ + {{format-html-message "block.pool_notice.html" success=BlockUnlockDepth}} +
@@ -13,13 +12,13 @@ {{/if}} {{outlet}} diff --git a/www/app/templates/blocks/block.hbs b/www/app/templates/blocks/block.hbs index 747b96c89..228fe6e37 100644 --- a/www/app/templates/blocks/block.hbs +++ b/www/app/templates/blocks/block.hbs @@ -1,18 +1,18 @@ {{#if block.uncle}} - {{format-number block.height}} + {{format-number block.height}} {{else}} - {{format-number block.height}} + {{format-number block.height}} {{/if}} {{#if block.uncle}} - {{block.hash}} + {{block.hash}} {{else if block.orphan}} - Orphan + {{t "block.orphan"}} {{else}} - {{block.hash}} + {{block.hash}} {{/if}} {{format-date-locale block.timestamp}} diff --git a/www/app/templates/blocks/immature.hbs b/www/app/templates/blocks/immature.hbs index 5552d4d82..2784b784f 100644 --- a/www/app/templates/blocks/immature.hbs +++ b/www/app/templates/blocks/immature.hbs @@ -1,15 +1,15 @@ {{#if model.immature}} -

Immature Blocks

+

{{t "block.immature_blocks"}}

- - - - - + + + + + @@ -20,5 +20,5 @@
HeightBlock HashTime FoundVarianceReward{{t "block.height"}}{{t "block.hash"}}{{t "block.time_found"}}{{t "block.variance"}}{{t "block.reward"}}
{{else}} -

No immature blocks yet

+

{{t "block.no_immature_blocks_yet"}}

{{/if}} diff --git a/www/app/templates/blocks/index.hbs b/www/app/templates/blocks/index.hbs index e1a833535..6b49fb3db 100644 --- a/www/app/templates/blocks/index.hbs +++ b/www/app/templates/blocks/index.hbs @@ -1,14 +1,14 @@ {{#if model.matured}} -

Matured Blocks

+

{{t "block.matured"}}

- - - - - + + + + + @@ -19,5 +19,5 @@
HeightBlock HashTime FoundVarianceReward{{t "block.height"}}{{t "block.hash"}}{{t "block.time_found"}}{{t "block.variance"}}{{t "block.reward"}}
{{else}} -

No matured blocks yet

+

{{t "block.no_matured_blocks_yet"}}

{{/if}} diff --git a/www/app/templates/blocks/pending.hbs b/www/app/templates/blocks/pending.hbs index 486abc598..967717a4a 100644 --- a/www/app/templates/blocks/pending.hbs +++ b/www/app/templates/blocks/pending.hbs @@ -1,18 +1,18 @@ {{#if model.candidates}} -

Recently Found Blocks

+

{{t "block.recently_found_blocks"}}

- - - + + + {{#each model.candidates as |block|}} - +
HeightTime FoundVariance{{t "block.height"}}{{t "block.time_found"}}{{t "block.variance"}}
{{format-number block.height}}{{format-number block.height}} {{format-date-locale block.timestamp}} {{#if block.isLucky}} @@ -27,5 +27,5 @@
{{else}} -

No new blocks yet

+

{{t "block.no_new_blocks_yet"}}

{{/if}} diff --git a/www/app/templates/help-ko.hbs b/www/app/templates/help-ko.hbs new file mode 100644 index 000000000..559a13fda --- /dev/null +++ b/www/app/templates/help-ko.hbs @@ -0,0 +1,90 @@ +
+ +

GPU 마이너 프로그램

+ +

다음의 GPU 마이너 프로그램중 하나를 다운로드하세요. +

+

+ +

지갑 주소를 준비합니다

+
+

geth 사용하기

+
    +
  • cmd.exe를 사용하는 윈도우 명령행 실행 (커맨드 쉘) > geth account new
  • +
  • 유닉스/리눅스 쉘 $ geth account new
  • +
+ +

온라인 지갑 사용하기

+
    +
  • {{format-html-message "wallet.online_html"}}
  • +
+ +

지갑 Dapp (aka. Mist)

+
    +
  • {{format-html-message "wallet.dapp_html"}}
  • +
+
+ +

사용 예제

+
+
0x0000000000000000000000000000000000000000
+
출금을 위한 지갑 주소
+ 예: 0x8b92c50e1c39466f900a578edb20a49356c4fe24. +
+
your-worker-1
your-worker-2
+
+ PC/마이닝리그를 다른 것과 구별하기 위한 ID. 하나밖에 없다면 생략해도 됩니다.
+ 반드시 알파벳/숫자와 추가적으로 대시와 밑줄을 사용할 수 있습니다.
+ 예: worker-1 +
+
+ +

Claymore 예시

+

Claymore 듀얼 마이너 다운로드 (AMD/NVIDIA): [ANN] Bitcointalk.org

+

+ EthDcrMiner64 -epool {{config.StratumHost}}:{{config.StratumPort}} -esm 0 -ewal 0x0000000000000000000000000000000000000000 -eworker your-worker-1 -allcoins 1 -allpools 1 +

+
    +
  • EthDcrMiner64 - 윈도우상의 마이너 실행파일 이름. 리눅스/우분투의 경우 ./ethdcrminer64.
  • +
  • {{config.StratumHost}} - Stratum Server name
  • +
  • -esm 0{{config.StratumPort}} 포트번호는 Stratum 서버의 경우.
  • +
  • -esm 3{{config.NicehashPort}} 포트번호는 Nicehash 지원 서버의 경우.
  • +
  • {{config.StratumPort}} - Stratum Port number
  • +
  • 0x0000000000000000000000000000000000000000 - Your wallet address
  • +
  • your-worker-1 - Your worker name
  • +
+ +

Ethminer 예시

+

+ Ethminer 다운로드 (AMD/NVIDIA): Ethminer 소스/바이너리 at Github +

+
Stratum 방식
+

+ ethminer -SP 1 -U -S {{config.StratumHost}}:{{config.StratumPort}} -O 0x0000000000000000000000000000000000000000.your-worker-1 --farm-recheck 2000 +

+
    +
  • -SP 1 - Stratum 서버일 경우
  • +
  • -U - NVIDIA GPU 의 경우 사용. AMD GPU의 경우는 -G.
  • +
  • -S {{config.StratumHost}}:{{config.StratumPort}} - stratum_server_name:stratum_port_number
  • +
  • -O 0x0000000000000000000000000000000000000000.your-worker-1 - your_wallet_address.your_worker_name
  • +
  • --farm-recheck 2000 - 작업이 바뀔 때마다 2000ms간격을 두고 체크한다. (기본값 500ms. Stratum의 경우 2000ms으로 해야 안정적. eth-proxy의 경우 적은 값 사용)
  • +
+
레거시 HTTP 방식
+

+ ethminer -U -F {{config.HttpHost}}:{{config.HttpPort}}/0x0000000000000000000000000000000000000000/your-worker-1 --farm-recheck 200 +

+
    +
  • -U - NVIDIA GPU 의 경우 사용. AMD GPU의 경우는 -G.
  • +
  • -F {{config.HttpHost}}:{{config.HttpPort}}/0x0000000000000000000000000000000000000000/your-worker-1 +
    • 레거시 HTTP 방식. http_server_name:http_port_number
    • +
    • 0x0000000000000000000000000000000000000000/your-worker-1 - your_wallet_address.your_worker_name
    • +
    +
  • +
+ +
diff --git a/www/app/templates/help-minimal-ko.hbs b/www/app/templates/help-minimal-ko.hbs new file mode 100644 index 000000000..e9e5471f8 --- /dev/null +++ b/www/app/templates/help-minimal-ko.hbs @@ -0,0 +1,16 @@ +
+

Claymore 듀얼 마이너

+

+ EthDcrMiner64 -epool {{config.StratumHost}}:{{config.StratumPort}} -esm 0 -ewal 0x0000000000000000000000000000000000000000 -eworker your-worker-1 -allcoins 1 -allpools 1 +

+

Hint 리눅스/우분투의 경우 ./ethdcrminer64 명령 사용
+

+
+ +
+

Ethminer

+

+ ethminer -U -F {{config.HttpHost}}:{{config.HttpPort}}/0x0000000000000000000000000000000000000000/your-worker-1 --farm-recheck 200 +

+

Hint -U 옵션은 NVIDIA GPU 사용, -G 옵션은 AMD GPU 사용

+
diff --git a/www/app/templates/help-minimal.hbs b/www/app/templates/help-minimal.hbs new file mode 100644 index 000000000..957390542 --- /dev/null +++ b/www/app/templates/help-minimal.hbs @@ -0,0 +1,16 @@ +
+

Claymore Dual Miner

+

+ EthDcrMiner64 -epool {{config.StratumHost}}:{{config.StratumPort}} -esm 0 -ewal 0x0000000000000000000000000000000000000000 -eworker your-worker-1 -allcoins 1 -allpools 1 +

+

Hint use ./ethdcrminer64 under Linux/ubuntu
+

+
+ +
+

Ethminer

+

+ ethminer -U -F {{config.HttpHost}}:{{config.HttpPort}}/0x0000000000000000000000000000000000000000/your-worker-1 --farm-recheck 200 +

+

Hint use -U option for NVIDIA GPU, -G option for AMD GPU.

+
diff --git a/www/app/templates/help.hbs b/www/app/templates/help.hbs index f2041e852..91fef23b2 100644 --- a/www/app/templates/help.hbs +++ b/www/app/templates/help.hbs @@ -1,43 +1,88 @@
-

In order to mine on this pool you need to have an - ethminer installation - pointed to
{{config.HttpHost}}:{{config.HttpPort}}/YOUR_ETH_ADDRESS/RIG_ID +

GPU Miner Softwares

+ +

Download one of the following GPU miners. +

+ +

Prepare Wallet Address

+
+

Using geth

+
    +
  • Unix/Linux shell $ geth account new
  • +
  • Windows command line using cmd.exe (command shell) > geth account new
  • +
+ +

Using online wallet

+
    +
  • {{format-html-message "wallet.online_html"}}
  • +
+ +

Wallet Dapp (aka. Mist)

+
    +
  • Wallet dapp: {{format-html-message "wallet.dapp_html"}}
  • +
+
+ +

Usage examples

-
YOUR_ETH_ADDRESS
-
This is your address for payouts, generate one with geth, or mine directly to exchange like - Poloniex - or Bittrex.
- Example: 0xb85150eb365e7df0941f0cf08235f987ba91506a. +
0x0000000000000000000000000000000000000000
+
This is your address for payouts
+ Example: 0x8b92c50e1c39466f900a578edb20a49356c4fe24.
-
RIG_ID
+
your-worker-1
your-worker-2
- ID of your farm to distinguish it from your other rig. If you have just one rig, feel free to omit this param. + ID of your PC/mining-rig to distinguish it from your other rigs. If you have just one rig, feel free to omit this param.
This param must be short alphanumeric string with optional dashes and underscores.
- Example: rig-1 + Example: worker-1
-

- Full example: - ethminer -F {{config.HttpHost}}:{{config.HttpPort}}/0xb85150eb365e7df0941f0cf08235f987ba91506a/myfarm -G --farm-recheck 200.
- Hint: If you are compiling ethminer from latest source, please also use - extra --disable-submit-hashrate option. -

-

Stratum Mining with Stratum Proxy for Ethereum

-

Grab proxy from eth-proxy GitHub repo.

-

Edit eth-proxy.conf and specify our pool's HOST: {{config.StratumHost}}, PORT: {{config.StratumPort}} and your WALLET.

+

Claymore Example

+

Download Claymore Dual Miner (AMD/NVIDIA): [ANN] Bitcointalk.org

+

+ EthDcrMiner64 -epool {{config.StratumHost}}:{{config.StratumPort}} -esm 0 -ewal 0x0000000000000000000000000000000000000000 -eworker your-worker-1 -allcoins 1 -allpools 1 +

+
    +
  • EthDcrMiner64 - executable name under Windows. use ./ethdcrminer64 under Linux/Ubuntu
  • +
  • {{config.StratumHost}} - Stratum Server name
  • +
  • use -esm 0 and {{config.StratumPort}} port number for Stratum Server.
  • +
  • use -esm 3 and {{config.NicehashPort}} port number for Nicehash Server.
  • +
  • 0x0000000000000000000000000000000000000000 - Your wallet address
  • +
  • your-worker-1 - Your worker name
  • +
-

Mining with Ether-Proxy

-

Use stable release of Ethereum Solo/Pool Mining Proxy.

- -

Advice

-

CPU mining is not recommended.

-

Terms of Service

-

By using the pool you accept all possible risks related to experimental software usage.
- Pool owner can't compensate any irreversible losses, but will do his best to prevent worst case. -

+

Ethminer Examples

+

+ Download Ethminer (AMD/NVIDIA): Ethminer source/binary at Github +

+
Stratum method
+

+ ethminer -SP 1 -U -S {{config.StratumHost}}:{{config.StratumPort}} -O 0x0000000000000000000000000000000000000000.your-worker-1 --farm-recheck 2000 +

+
    +
  • -SP 1 - option for Stratum server
  • +
  • -U - NVIDIA GPU or -G for AMD GPU
  • +
  • -S {{config.StratumHost}}:{{config.StratumPort}} - stratum_server_name:stratum_port_number
  • +
  • -O 0x0000000000000000000000000000000000000000.your-worker-1 - your_wallet_address.your_worker_name
  • +
  • --farm-recheck 2000 - Leave 2000 ms between checks for changed work (default 500ms. use higher value to use stratum for stability)
  • +
+
Legacy HTTP method
+

+ ethminer -U -F {{config.HttpHost}}:{{config.HttpPort}}/0x0000000000000000000000000000000000000000/your-worker-1 --farm-recheck 200 +

+
    +
  • -U - NVIDIA GPU or -G for AMD GPU
  • +
  • -F {{config.HttpHost}}:{{config.HttpPort}}/0x0000000000000000000000000000000000000000/your-worker-1 +
    • Legacy HTTP method. http_server_name:http_port_number
    • +
    • 0x0000000000000000000000000000000000000000/your-worker-1 - your_wallet_address.your_worker_name
    • +
    +
  • +
diff --git a/www/app/templates/index.hbs b/www/app/templates/index.hbs index 4490ea1aa..7f3a31871 100644 --- a/www/app/templates/index.hbs +++ b/www/app/templates/index.hbs @@ -3,45 +3,47 @@

- Open Ethereum Pool + {{config.PoolTitle}}

- Min. payout threshold: {{config.PayoutThreshold}}, Payouts run twice per day.
- PROP Stable and profitable pool with regular payouts. + {{t "home.min_payout_threshold"}}: {{PayoutThreshold}} {{config.Unit}} / {{t "home.payouts_run" interval=PayoutInterval}}
+ PROP {{t "home.payout_scheme_detail"}}
-
Miners Online: {{format-number stats.model.minersTotal}}
-
Pool Hash Rate: {{format-hashrate stats.model.hashrate}}
-
Pool Fee: {{config.PoolFee}}
+
{{t "home.miners_online"}}: {{format-number stats.model.minersTotal}}
+
{{t "home.pool_hashrate"}}: {{format-hashrate stats.model.hashrate}}
+
{{t "home.pool_fee"}}: {{PoolFee}}
{{#if stats.model.stats.lastBlockFound}} -
Last Block Found: {{format-relative (seconds-to-ms stats.model.stats.lastBlockFound)}}
+
{{t "home.last_block_found"}}: {{format-relative (seconds-to-ms stats.model.stats.lastBlockFound)}}
{{/if}}
-
Network Difficulty: {{with-metric-prefix stats.difficulty}}
-
Network Hash Rate: {{format-hashrate stats.hashrate}}
-
Blockchain Height: {{format-number stats.height}}
-
Current Round Variance: {{format-number stats.roundVariance style='percent'}}
+
{{t "home.network_difficulty"}}: {{with-metric-prefix stats.difficulty}}
+
{{t "home.network_hashrate"}}: {{format-hashrate stats.hashrate}}
+
{{t "home.blockchain_height"}}: {{format-number stats.height}}
+
{{t "home.current_round_variance"}}: {{format-number stats.roundVariance style='percent'}}
+{{#if config.highcharts.main.enabled}} +
+ {{high-charts mode=chartMode chartOptions=chartOptions content=chartData}} +
+{{/if}} +
-

Your Stats & Payment History

+

{{t "home.query_history"}}

- {{input value=cachedLogin class="form-control" placeholder="Enter Your Ethereum Address"}} + {{input value=cachedLogin class="form-control" placeholder=(t "home.input.enter_your_wallet_address")}}
-
-
-

- ethminer.exe -F {{config.HttpHost}}:{{config.HttpPort}}/<address>/<worker> -G -

-
+ + {{partial (t "home.help.minimal")}}
diff --git a/www/app/templates/luck.hbs b/www/app/templates/luck.hbs index 56a1c8220..14e5ad029 100644 --- a/www/app/templates/luck.hbs +++ b/www/app/templates/luck.hbs @@ -2,10 +2,10 @@ - - - - + + + + diff --git a/www/app/templates/miners.hbs b/www/app/templates/miners.hbs index 1017826b7..1b2f52490 100644 --- a/www/app/templates/miners.hbs +++ b/www/app/templates/miners.hbs @@ -1,19 +1,19 @@
-

Total hashrate: {{format-hashrate model.hashrate}}.

- Total miners: {{model.minersTotal}} +

{{t "miners.total_hashrate"}}: {{format-hashrate model.hashrate}}.

+ {{t "miners.total_miners"}}: {{model.minersTotal}}
{{#if model.miners}} -

Miners

+

{{t "miners.miners"}}

BlocksShares/DiffUncle RateOrphan Rate{{t "luck.blocks"}}{{t "luck.shares_diff"}}{{t "luck.uncle_rate"}}{{t "luck.orphan_rate"}}
- - - + + + @@ -28,6 +28,6 @@
LoginHashrateLast Beat{{t "miners.login"}}{{t "miners.hashrate"}}{{t "miners.last_beat"}}
{{else}} -

No miners

+

{{t "miners.no_miners"}}

{{/if}} diff --git a/www/app/templates/payments.hbs b/www/app/templates/payments.hbs index a34d558cd..58d10cba7 100644 --- a/www/app/templates/payments.hbs +++ b/www/app/templates/payments.hbs @@ -1,20 +1,20 @@
-

Pool always pay tx fees from it's own pocket for now.

- Total payments sent: {{model.paymentsTotal}} +

{{t "payments.pay_tx"}}

+ {{t "payments.total_payments_sent"}}: {{model.paymentsTotal}}
{{#if model.payments}} -

Latest Payouts

+

{{t "payments.latest_payouts"}}

- - - - + + + + @@ -23,10 +23,10 @@ {{/each}} @@ -34,6 +34,6 @@
TimeAmountAddressTx ID{{t "payments.time"}}{{t "payments.amount"}}{{t "payments.address"}}{{t "payments.txid"}}
{{format-date-locale tx.timestamp}} {{format-number tx.formatAmount}} - {{tx.address}} + {{tx.address}} - {{format-tx tx.tx}} + {{format-tx tx.tx}}
{{else}} -

No payouts yet

+

{{t "payments.no_payouts_yet"}}

{{/if}}
diff --git a/www/config/environment.js b/www/config/environment.js index c3fcf8a9a..d67fd6489 100644 --- a/www/config/environment.js +++ b/www/config/environment.js @@ -14,6 +14,10 @@ module.exports = function(environment) { }, APP: { + // PoolName + PoolName: 'Ethereum', + // PoolTitle + PoolTitle: 'Open Ethereum Pool', // API host and port ApiUrl: '//example.net/', @@ -27,10 +31,42 @@ module.exports = function(environment) { // Fee and payout details PoolFee: '1%', - PayoutThreshold: '0.5 Ether', + PayoutThreshold: '1', + PayoutInterval: '2m', + Unit: 'ETH', + EtherUnit: 'ETH', // For network hashrate (change for your favourite fork) - BlockTime: 14.4 + BlockExplorerLink: 'https://myexplorer.net', + BlockExplorerAddrLink: 'https://myexplorer.net/addr', + DonationLink: false, + DonationAddress: '', + BlockReward: 5, + BlockUnlockDepth: 120, + BlockTime: 14.4, + highcharts: { + main: { + enabled: true, + height: 200, + type: 'spline', + color: '', + title: '', + ytitle: '', + interval: 180000, + chartInterval: 900000 + }, + account: { + enabled: true, + height: 200, + type: 'spline', + color: [ '', '' ], + title: '', + ytitle: '', + interval: 180000, + chartInterval: 900000, + paymentInterval: 300000 + } + } } }; diff --git a/www/package.json b/www/package.json index 9aebf089f..910d2a756 100644 --- a/www/package.json +++ b/www/package.json @@ -36,9 +36,11 @@ "ember-cli-test-loader": "^1.1.0", "ember-cli-uglify": "^1.2.0", "ember-export-application-global": "^1.0.5", + "ember-highcharts": "0.6.0", "ember-load-initializers": "^0.5.1", "ember-resolver": "^2.0.3", "ember-welcome-page": "^1.0.3", + "highcharts": "^6.0.7", "loader.js": "^4.0.10", "ember-intl": "2.15.1", "ember-cli-cookie": "^0.2.0" diff --git a/www/translations/en-us.yaml b/www/translations/en-us.yaml index 5c7ef82c0..ae4e30c92 100644 --- a/www/translations/en-us.yaml +++ b/www/translations/en-us.yaml @@ -3,3 +3,132 @@ product: title: 'Hello world!' html: info: '{product} will cost {price, number, USD} if ordered by {deadline, date, time}' + +menu: + home: Home + help: Help + pool_blocks: Pool Blocks + payments: Payments + miners: Miners + about: About + i18n: + about: about + help: help + language: language + +lang: + korean: Korean + english: English + +home: + min_payout_threshold: Min. payout threshold + payouts_run: Payouts run every {interval}. + payout_scheme_detail: Stable and profitable pool with regular payouts. + miners_online: Miners Online + pool_hashrate: Pool Hash Rate + pool_fee: Pool Fee + last_block_found: Last Block Found + network_difficulty: Network Difficulty + network_hashrate: Network Hash Rate + blockchain_height: Blockchain Height + current_round_variance: Current Round Variance + + query_history: Your Stats and Payment History + input: + enter_your_wallet_address: Enter Your Wallet Address + + button: + lookup: Lookup + + help: + minimal: help-minimal + +account: + immature: + balance: Immature Balance + description: Preliminary balance awaiting blocks to mature. + pending: + balance: Pending Balance + description: Credited coins awaiting payout. + current: Current Payment + last_share_submitted: Last Share Submitted + online: Workers Online + hashrate: Hashrate + earnings: + perday: Earnings per day + short_avg: short avg. + blocks: + found: Blocks Found + total: + payments: Total Payments + paid: Total Paid + round_share: Your Round Share + round_share_description: Percent of your contribution to current round. + epoch_switch: Epoch Switch + workers: Workers + payouts: Payouts + + your_workers: Workers + last_share: Last Share + short_average: rough, short average + long_average: accurate, long average + no_workers_online: No workers online + notice: Notice + notice_html: Your average hashrate will be smoothly adjusted until you have shares to fullfill estimation window.
There are two windows, long and short, first is equal to about 30 minutes and long window is usually equal to 3 hours.
Dead (sick) workers will be highlighted in a table of workers if they didn't submit a share for 1/2 of short window, so you can perform maintenance of your rigs. + json_api_url: Your bulk stats JSON API URL + +payout: + latest_payouts: Your Latest Payouts + time: Time + txid: Tx ID + amount: Amount + no_payouts_yet: No payouts yet + +block: + pool_rewards: Pool always pay full block reward including TX fees and uncle rewards. + pool_notice: + html: Block maturity requires up to {success} blocks. Usually it's less indeed. + blocks: Blocks + immature: Immature + new: New Blocks + orphan: Orphan + no_matured_blocks_yet: No matured blocks yet + reward: Reward + height: Height + hash: Hash + time_found: Time Found + variance: Share Variance (Shares/Diff) + matured: Matured Blocks + immature_blocks: Immature Blocks + no_immature_blocks_yet: No immature blocks yet + no_new_blocks_yet: No new blocks yet + recently_found_blocks: Recently Found Blocks + +luck: + blocks: Blocks + shares_diff: Shares/Diff + uncle_rate: Uncle Rate + orphan_rate: Orphan Rate + +payments: + pay_tx: Pool always pay tx fees from it's own pocket for now. + total_payments_sent: Total payments sent + latest_payouts: Latest Payouts + time: Time + amount: Amount + address: Address + txid: Tx ID + no_payouts_yet: No payouts yet + +miners: + total_hashrate: Total pool hashrate + total_miners: Total miners + login: Login + hashrate: Hashrate + last_beat: Last beat + no_miners: No miners + miners: Miners + +wallet: + dapp_html: https://ethereum.org/ + online_html: https://www.myetherwallet.com Online wallet at myetherwallet.com diff --git a/www/translations/ko.yaml b/www/translations/ko.yaml new file mode 100644 index 000000000..880d7f84e --- /dev/null +++ b/www/translations/ko.yaml @@ -0,0 +1,133 @@ +product: + info: '{product} will cost {price, number, USD} if ordered by {deadline, date, time}' + title: 'Hello world!' + html: + info: '{product} will cost {price, number, USD} if ordered by {deadline, date, time}' + +menu: + home: 홈 + help: 도움말 + pool_blocks: 블록 정보 + payments: 지급현황 + miners: 채굴현황 + about: 이곳은.. + i18n: + about: about-ko + help: help-ko + language: 언어 + +lang: + korean: 한국어 + english: English + +home: + min_payout_threshold: 최소 지급량 + payouts_run: 지급은 매 {interval} 간격으로 이루어집니다. + payout_scheme_detail: 정기적인 지급으로 안정적이고 수익성있는 풀입니다. + miners_online: 접속된 마이너 + pool_hashrate: 풀 해시 + pool_fee: 수수료 + last_block_found: 마지막 발견 블록 + network_difficulty: 네트워크 난이도 + network_hashrate: 네트워크 총해시 + blockchain_height: 블록체인 높이 + current_round_variance: 라운드 쉐어 분산도 + + query_history: 채굴현황 및 지급내역 조회하기 + input: + enter_your_wallet_address: 조회할 지갑 주소를 넣으세요 + + button: + lookup: 보기 + help: + minimal: help-minimal-ko + +account: + immature: + balance: 미승인 잔고 + description: 블록이 성숙되기를 기다리는 미승인 잔고입니다. + pending: + balance: 지급대기 잔고 + description: 지급 대기중인 잔고입니다. + current: 현 출금량 + last_share_submitted: 마지막 쉐어 제출 + online: 작동중인 워커 + hashrate: 해시량 + earnings: + perday: 하루 채굴 예상량 + short_avg: 단기 평균 + blocks: + found: 발견한 블록 + total: + payments: 총 출금회수 + paid: 총 출금량 + round_share: 라운드 쉐어율 + round_share_description: 현재 라운드에 대한 쉐어 비율. + epoch_switch: 에폭 전환 + workers: 워커현황 + payouts: 출금내역 + + your_workers: 워커 목록 + last_share: 마지막 쉐어 + short_average: 짧은 구간 평균 + long_average: 긴구간 평균 + no_workers_online: 작동중인 워커가 없습니다 + notice: 주의사항 + notice_html: 해시 평균값은 평가 구간안에서 쉐어가 충분히 제출되면 매끄럽게 조정됩니다.
여기서 두가지의 평가구간, 긴 구간과 짧은 구간이 있는데, 짧은 구간은 30분, 긴 구간은 3시간 구간입니다.
문제있는 워커는 짧은 구간에서 1/2의 쉐어가 제출되지 않는 경우이며 강조되어 표시되고, 해당 워커를 확인하셔야 할 필요가 있습니다. + json_api_url: 워커 상태에 대한 JSON API 링크 + +payout: + latest_payouts: 최근 출금 기록 + time: 시간 + txid: 트랜젝션 ID + amount: 출금량 + no_payouts_yet: 출금기록이 아직 없습니다 + +block: + pool_rewards: 이 풀은 TX 수수료 및 엉클 블록에 대한 모든 블록 보상을 하고 있습니다. + pool_notice: + html: 블록 확정은 최대 {success} 블록이 발견되어야 하며, 보통은 그 이전에 확정됩니다. + blocks: 블록 + immature: 미성숙 + new: 새 블록 + orphan: 외토리블록 + no_matured_blocks_yet: 완성된 블록이 없습니다 + reward: 보상 + height: 높이 + hash: 해시 + time_found: 발견된 시간 + variance: 쉐어 분산(쉐어/난이도) + matured: 확정된 블록 + immature_blocks: 미성숙 블록 + no_immature_blocks_yet: 미성숙 블록이 아직 없습니다 + no_new_blocks_yet: 새로운 블록이 없습니다 + recently_found_blocks: 최근에 발견된 블록들 + +luck: + blocks: 블록수 + shares_diff: 쉐어/난이도 + uncle_rate: 엉클 비율 + orphan_rate: 외토리 비율 + +payments: + pay_tx: 현재 전송 수수료를 이 풀에서 지급하고 있습니다. + total_payments_sent: 전체 출금건수 + latest_payouts: 최근 출금 내역 + time: 시간 + amount: 수량 + address: 주소 + txid: 전송 ID + no_payouts_yet: 출금이 아직 없습니다 + +miners: + total_hashrate: 전체 풀 해시 + total_miners: 접속중인 마이너 + login: 계정/지갑주소 + hashrate: 해시량 + last_beat: 최근 작동시간 + no_miners: 마이너가 없습니다 + miners: 마이너 현황 + +wallet: + dapp_html: https://ethereum.org/ + online_html: https://www.myetherwallet.com myetherwallet.com에서 제공하는 온라인 지갑