├── webui.jpg ├── webui ├── api │ ├── response.go │ ├── cfg.go │ └── pool.go ├── assets │ ├── font │ │ ├── cyrillic.woff2 │ │ └── cyrillic-ext.woff2 │ ├── plugins │ │ ├── fontawesome-free │ │ │ └── webfonts │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-solid-900.woff2 │ │ │ │ ├── fa-regular-400.woff2 │ │ │ │ └── fa-v4compatibility.woff2 │ │ ├── jquery-ui │ │ │ ├── images │ │ │ │ ├── ui-icons_444444_256x240.png │ │ │ │ ├── ui-icons_555555_256x240.png │ │ │ │ ├── ui-icons_777620_256x240.png │ │ │ │ ├── ui-icons_777777_256x240.png │ │ │ │ ├── ui-icons_cc0000_256x240.png │ │ │ │ └── ui-icons_ffffff_256x240.png │ │ │ └── jquery-ui.theme.min.css │ │ ├── datatables-buttons │ │ │ ├── js │ │ │ │ ├── buttons.bootstrap4.min.js │ │ │ │ ├── buttons.print.min.js │ │ │ │ └── buttons.colVis.min.js │ │ │ └── css │ │ │ │ └── buttons.bootstrap4.min.css │ │ ├── datatables-responsive │ │ │ ├── js │ │ │ │ └── responsive.bootstrap4.min.js │ │ │ └── css │ │ │ │ └── responsive.bootstrap4.min.css │ │ └── datatables-bs4 │ │ │ ├── js │ │ │ └── dataTables.bootstrap4.min.js │ │ │ └── css │ │ │ └── dataTables.bootstrap4.min.css │ ├── css │ │ └── font.css │ └── js │ │ └── api.js ├── assets.go ├── template │ ├── page │ │ ├── code_404.gohtml │ │ ├── cfg_edit.gohtml │ │ ├── pool_manager.gohtml │ │ ├── pool_worker_list.gohtml │ │ └── dashboard.gohtml │ ├── layout │ │ └── base.gohtml │ └── root.gohtml ├── page_cfg_edit.go ├── page.go ├── page_pool_create.go ├── page_pool_manger.go ├── page_pool_edit.go ├── pool_worker_list.go ├── middleware │ └── basic_auth.go ├── page_dashboard.go ├── server.go └── template_func.go ├── config ├── build.go ├── config.example.yml ├── config_self.go └── config.go ├── .gitignore ├── util ├── slice.go ├── random.go └── validator │ └── validator.go ├── main_publish.go ├── connection ├── util.go ├── error_handler.go ├── miner_protocol.go ├── upstream_injector.go ├── downstream_injector.go ├── downstream.go ├── miner_fee_control.go ├── pool_server.go ├── miner.go └── upstream.go ├── stratumproxy.service ├── injector ├── eth │ ├── down_99_injector_eth_forward.go │ ├── down_2_record_get_work_id.go │ ├── down_0_drop_unauth_client.go │ ├── up_5_injector_send_job.go │ ├── down_3_eth_submit_hashrate.go │ ├── protocol.go │ ├── down_4_submit_work.go │ └── down_1_injector_eth_submit_login.go ├── eth-stratum │ ├── up_2_set_extranonce.go │ ├── down_4_extranonce.go │ ├── down_1_drop_unauth_client.go │ ├── up_1_injector_send_job.go │ ├── down_0_subscribe.go │ ├── down_3_submit_work.go │ ├── protocol.go │ └── down_2_authorize.go └── eth-common │ └── fee_control.go ├── README.md ├── protocol ├── eth-stratum │ ├── downstream.go │ └── upstream.go └── eth │ ├── upstream.go │ └── downstream.go ├── main.go ├── go.mod └── install.sh /webui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui.jpg -------------------------------------------------------------------------------- /webui/api/response.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type ResponseAPI struct { 4 | Result bool 5 | Msg string 6 | } 7 | -------------------------------------------------------------------------------- /config/build.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ProjectName = "StratumProxy" 4 | 5 | var GitTag string 6 | var BuildTime string 7 | -------------------------------------------------------------------------------- /webui/assets/font/cyrillic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/font/cyrillic.woff2 -------------------------------------------------------------------------------- /webui/assets/font/cyrillic-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/font/cyrillic-ext.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | makefile 2 | main_debug.go 3 | config/config_develop.go 4 | stratumproxy.spec 5 | config.yml 6 | .gitlab-ci.yml 7 | .idea/ 8 | bin/ 9 | -------------------------------------------------------------------------------- /config/config.example.yml: -------------------------------------------------------------------------------- 1 | pools: [] 2 | 3 | # WebUI 设置 4 | webui: 5 | bind: "0.0.0.0:8444" 6 | auth: 7 | username: "" 8 | passwd: "" 9 | -------------------------------------------------------------------------------- /webui/assets/plugins/fontawesome-free/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/fontawesome-free/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /webui/assets/plugins/fontawesome-free/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/fontawesome-free/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /webui/assets/plugins/jquery-ui/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/jquery-ui/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /webui/assets/plugins/jquery-ui/images/ui-icons_555555_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/jquery-ui/images/ui-icons_555555_256x240.png -------------------------------------------------------------------------------- /webui/assets/plugins/jquery-ui/images/ui-icons_777620_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/jquery-ui/images/ui-icons_777620_256x240.png -------------------------------------------------------------------------------- /webui/assets/plugins/jquery-ui/images/ui-icons_777777_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/jquery-ui/images/ui-icons_777777_256x240.png -------------------------------------------------------------------------------- /webui/assets/plugins/jquery-ui/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/jquery-ui/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /webui/assets/plugins/jquery-ui/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/jquery-ui/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /webui/assets/plugins/fontawesome-free/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/fontawesome-free/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /webui/assets/plugins/fontawesome-free/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/HEAD/webui/assets/plugins/fontawesome-free/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /webui/assets.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "embed" 5 | _ "embed" 6 | ) 7 | 8 | //go:embed assets/* 9 | var assets embed.FS 10 | 11 | //go:embed template/* 12 | var pageTemplate embed.FS 13 | -------------------------------------------------------------------------------- /util/slice.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func StringSliceContain(slice []string, s string) bool { 4 | for _, s2 := range slice { 5 | if s == s2 { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /main_publish.go: -------------------------------------------------------------------------------- 1 | // +build publish_log 2 | 3 | package main 4 | 5 | import log "github.com/sirupsen/logrus" 6 | 7 | func InitMain() { 8 | log.SetLevel(log.InfoLevel) 9 | } 10 | 11 | func DeferMain() { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /util/random.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | ) 7 | 8 | func GetRandomString2(n int) string { 9 | randBytes := make([]byte, n/2) 10 | rand.Read(randBytes) 11 | return fmt.Sprintf("%x", randBytes) 12 | } 13 | -------------------------------------------------------------------------------- /webui/template/page/code_404.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "title" }} Not Found {{ end }} 2 | 3 | {{ define "header_page" }} 4 | {{ end }} 5 | 6 | {{ define "section_title" }} 找不到此页 {{ end }} 7 | 8 | {{ define "section" }} 9 |

找不到此页

10 | {{ end }} 11 | 12 | {{ define "script_section" }} {{ end }} 13 | -------------------------------------------------------------------------------- /util/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func ValidHostnamePort(s string) bool { 8 | sp := strings.Split(s, ":") 9 | if len(sp) != 2 { 10 | return false 11 | } 12 | if sp[0] == "" || sp[1] == "" { 13 | return false 14 | } 15 | return true 16 | } 17 | -------------------------------------------------------------------------------- /connection/util.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import "fmt" 4 | 5 | func HashrateFormat(hs float64) string { 6 | m := 1000000.0 7 | g := m * 1000 8 | t := g * 1000 9 | 10 | if hs > t { 11 | return fmt.Sprintf("%.2f", hs/t) + " TH/s" 12 | } 13 | if hs > g { 14 | return fmt.Sprintf("%.2f", hs/g) + " GH/s" 15 | } 16 | if hs > m { 17 | return fmt.Sprintf("%.2f", hs/m) + " MH/s" 18 | } 19 | 20 | return "0 H/s" 21 | } 22 | -------------------------------------------------------------------------------- /stratumproxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=StratumProxy Service 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | Type=simple 8 | User=root 9 | Restart=on-failure 10 | RestartSec=2s 11 | ExecStart=/usr/bin/stratumproxy -config /etc/stratumproxy/config.yml 12 | ExecStop=/bin/kill -TERM $MAINPID 13 | WorkingDirectory=/etc/stratumproxy 14 | LimitNOFILE=102400 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /webui/page_cfg_edit.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | log "github.com/sirupsen/logrus" 6 | "stratumproxy/config" 7 | ) 8 | 9 | func pageCfgEdit(context *context.Context) { 10 | page := page{ 11 | Pages: []string{"layout/base", "page/cfg_edit"}, 12 | Writer: context.ResponseWriter(), 13 | Data: config.GlobalConfig, 14 | } 15 | 16 | err := page.Build() 17 | if err != nil { 18 | log.Errorf("[pageCfgEdit] 无法解析模板: %s", err) 19 | return 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /injector/eth/down_99_injector_eth_forward.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | "strings" 7 | ) 8 | 9 | // DownInjectorCapture 记录没被转发的包 10 | func DownInjectorCapture(payload *connection.InjectorDownstreamPayload) { 11 | if !strings.HasSuffix(string(payload.In), "\n") { 12 | payload.In = []byte(string(payload.In) + "\n") 13 | } 14 | 15 | logrus.Debugf("[%s][%s][DownInjectorEthForward] 未处理的包: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), string(payload.In)) 16 | } 17 | -------------------------------------------------------------------------------- /webui/assets/css/font.css: -------------------------------------------------------------------------------- 1 | /* cyrillic-ext */ 2 | @font-face { 3 | font-family: 'Source Sans Pro'; 4 | font-style: italic; 5 | font-weight: 400; 6 | font-display: fallback; 7 | src: url(/assets/font/cyrillic-ext.woff2) format('woff2'); 8 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 9 | } 10 | /* cyrillic */ 11 | @font-face { 12 | font-family: 'Source Sans Pro'; 13 | font-style: italic; 14 | font-weight: 400; 15 | font-display: fallback; 16 | src: url(/assets/font/cyrillic.woff2) format('woff2'); 17 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 18 | } 19 | -------------------------------------------------------------------------------- /injector/eth/down_2_record_get_work_id.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "stratumproxy/connection" 5 | "stratumproxy/protocol/eth" 6 | "strings" 7 | ) 8 | 9 | // DownInjectorRecordGetWork 记录 getWork 10 | // 记录对方的 id 并且设置 Flag 11 | func DownInjectorRecordGetWork(payload *connection.InjectorDownstreamPayload) { 12 | if !strings.Contains(string(payload.In), "eth_getWork") { 13 | return 14 | } 15 | 16 | var getWork eth.RequestGetWork 17 | err := getWork.Parse(payload.In) 18 | if err != nil { 19 | return 20 | } 21 | err = getWork.Valid() 22 | if err != nil { 23 | return 24 | } 25 | 26 | // 不执行其他的了 27 | payload.IsTerminated = true 28 | } 29 | -------------------------------------------------------------------------------- /connection/error_handler.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "runtime/debug" 6 | ) 7 | 8 | func PanicHandler() { 9 | err := recover() 10 | 11 | if err != nil { 12 | log.Errorf("====================== Panic Error ======================") 13 | log.Errorf("程序遇到严重错误,不会崩溃和影响已有矿机,建议重启和报告给开发者!") 14 | log.Errorf("TG 群: https://t.me/StratumProxy") 15 | log.Errorf("Github: https://github.com/ethpoolproxy/stratumproxy") 16 | log.Errorf("错误详细信息: ") 17 | log.Errorf("%+v", err) 18 | log.Errorf("%+v", string(debug.Stack())) 19 | log.Errorf("=========================================================") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webui/template/layout/base.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "header" }} 2 | StratumProxy |{{ template "title"}} 3 | {{ end }} 4 | 5 | {{ define "body" }} 6 |
7 |
8 |
9 |
10 |
11 |

{{ template "section_title" . }}

12 |
13 |
14 |
15 |
16 | 17 |
18 | {{ template "section" . }} 19 |
20 |
21 | 22 | {{ end }} 23 | -------------------------------------------------------------------------------- /injector/eth/down_0_drop_unauth_client.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | "strings" 7 | ) 8 | 9 | func DownInjectorDropUnauthClient(payload *connection.InjectorDownstreamPayload) { 10 | if strings.Contains(string(payload.In), "eth_submitLogin") { 11 | return 12 | } 13 | 14 | // 在这里适配其他内核 15 | // TODO: teamredminer 16 | 17 | if !payload.DownstreamClient.AuthPackSent { 18 | payload.IsTerminated = true 19 | logrus.Debugf("[%s][%s][DownInjectorDropUnauthClient] 丢弃未认证请求: [%s]", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), string(payload.In)) 20 | return 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webui/page.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/Masterminds/sprig/v3" 5 | "html/template" 6 | "io" 7 | ) 8 | 9 | type page struct { 10 | Pages []string 11 | Data interface{} 12 | Writer io.Writer 13 | } 14 | 15 | func (p *page) Build() error { 16 | var pages []string 17 | pages = append(pages, "template/root.gohtml") 18 | for _, s := range p.Pages { 19 | pages = append(pages, "template/"+s+".gohtml") 20 | } 21 | 22 | ts, err := template.New("root").Funcs(sprig.FuncMap()).Funcs(funcMap).ParseFS(pageTemplate, pages...) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | err = ts.Execute(p.Writer, p.Data) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /webui/page_pool_create.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | log "github.com/sirupsen/logrus" 6 | "stratumproxy/config" 7 | ) 8 | 9 | func pagePoolAdd(ctx *context.Context) { 10 | type dataPoolAdd struct { 11 | Icon string 12 | Title string 13 | Action string 14 | PoolCfg config.Pool 15 | } 16 | 17 | var page = page{ 18 | Pages: []string{"layout/base", "page/pool_form"}, 19 | Writer: ctx.ResponseWriter(), 20 | Data: dataPoolAdd{ 21 | Icon: "fa-plus", 22 | Title: " 添加矿池", 23 | Action: "create", 24 | PoolCfg: config.Pool{}, 25 | }, 26 | } 27 | err := page.Build() 28 | if err != nil { 29 | log.Errorf("[pagePoolManger] 无法解析模板: %s", err) 30 | return 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /injector/eth-stratum/up_2_set_extranonce.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "stratumproxy/connection" 5 | "strings" 6 | ) 7 | 8 | func UpInjectorSetExtranonce(payload *connection.InjectorUpstreamPayload) { 9 | if !strings.Contains(string(payload.In), "mining.set_extranonce") { 10 | return 11 | } 12 | 13 | if payload.UpstreamClient.DownstreamClient == nil { 14 | return 15 | } 16 | 17 | if payload.UpstreamClient.DownstreamClient.WorkerMiner.DropUpstream { 18 | return 19 | } 20 | 21 | enable, _ := payload.UpstreamClient.ProtocolData.LoadOrStore("extranonce.subscribe", false) 22 | if !enable.(bool) { 23 | return 24 | } 25 | 26 | err := payload.UpstreamClient.DownstreamClient.Write(payload.In) 27 | if err != nil { 28 | return 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webui/page_pool_manger.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | log "github.com/sirupsen/logrus" 6 | "stratumproxy/connection" 7 | ) 8 | 9 | func pagePoolManger(ctx *context.Context) { 10 | type datePoolManger struct { 11 | PoolServers []connection.PoolServer 12 | } 13 | 14 | data := datePoolManger{} 15 | 16 | connection.PoolServers.Range(func(_, s interface{}) bool { 17 | data.PoolServers = append(data.PoolServers, *s.(*connection.PoolServer)) 18 | return true 19 | }) 20 | 21 | page := page{ 22 | Pages: []string{"layout/base", "page/pool_manager"}, 23 | Writer: ctx.ResponseWriter(), 24 | Data: data, 25 | } 26 | 27 | err := page.Build() 28 | if err != nil { 29 | log.Errorf("[pagePoolManger] 无法解析模板: %s", err) 30 | return 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /injector/eth-stratum/down_4_extranonce.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "gopkg.in/guregu/null.v4" 5 | "stratumproxy/connection" 6 | ethstratum "stratumproxy/protocol/eth-stratum" 7 | "strings" 8 | ) 9 | 10 | func DownInjectorExtranonce(payload *connection.InjectorDownstreamPayload) { 11 | if !strings.Contains(string(payload.In), "mining.extranonce.subscribe") { 12 | return 13 | } 14 | 15 | request := ðstratum.RequestGeneral{} 16 | err := request.Parse(payload.In) 17 | if err != nil { 18 | return 19 | } 20 | 21 | payload.IsTerminated = true 22 | response := ethstratum.ResponseGeneral{ 23 | Id: request.Id, 24 | Result: true, 25 | Error: null.String{}, 26 | } 27 | out, _ := response.Build() 28 | payload.Out = out 29 | 30 | payload.DownstreamClient.Upstream.ProtocolData.Store("extranonce.subscribe", true) 31 | } 32 | -------------------------------------------------------------------------------- /webui/api/cfg.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | "stratumproxy/config" 6 | ) 7 | 8 | // CfgAuthEdit 修改认证 POST /api/v1/cfg/auth 9 | func CfgAuthEdit(ctx *context.Context) { 10 | type requestStruct struct { 11 | Username string `json:"username"` 12 | Passwd string `json:"passwd"` 13 | } 14 | 15 | var request requestStruct 16 | err := ctx.ReadJSON(&request) 17 | if err != nil { 18 | _, _ = ctx.JSON(ResponseAPI{ 19 | Result: false, 20 | Msg: "未知错误,请反馈: " + err.Error(), 21 | }) 22 | return 23 | } 24 | 25 | config.GlobalConfig.WebUI.Auth.Username = request.Username 26 | config.GlobalConfig.WebUI.Auth.Passwd = request.Passwd 27 | 28 | _ = config.SaveConfig(config.ConfigFile) 29 | 30 | _, _ = ctx.JSON(ResponseAPI{ 31 | Result: true, 32 | Msg: "管理员认证信息修改成功!", 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /injector/eth-stratum/down_1_drop_unauth_client.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | "strings" 7 | ) 8 | 9 | func DownInjectorDropUnauthClient(payload *connection.InjectorDownstreamPayload) { 10 | if strings.Contains(string(payload.In), "mining.authorize") { 11 | return 12 | } 13 | 14 | if strings.Contains(string(payload.In), "eth_submitLogin") { 15 | payload.IsTerminated = true 16 | payload.ShouldShutdown = true 17 | payload.ForceShutdown = true 18 | return 19 | } 20 | 21 | if !payload.DownstreamClient.AuthPackSent { 22 | payload.IsTerminated = true 23 | logrus.Debugf("[%s][%s][DownInjectorDropUnauthClient] 丢弃未认证请求: [%s]", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), string(payload.In)) 24 | return 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /connection/miner_protocol.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | var Protocols = make(map[string]*Protocol) 8 | 9 | func GetProtocol(name string) *Protocol { 10 | name = strings.ToLower(name) 11 | 12 | if name == "etc" { 13 | name = "eth" 14 | } 15 | 16 | return Protocols[name] 17 | } 18 | 19 | type ProtocolHandler interface { 20 | HandleDownstreamDisconnect(client *DownstreamClient) 21 | HandleFeeControl(worker *WorkerMiner) 22 | InitialUpstreamConn(upstream *UpstreamClient) error 23 | InitialUpstreamAuth(upstream *UpstreamClient, identifier MinerIdentifier) error 24 | } 25 | 26 | type ProtocolInjector struct { 27 | DownstreamInjectorProcessors []func(payload *InjectorDownstreamPayload) 28 | UpstreamInjectorProcessors []InjectorProcessorUpstream 29 | } 30 | 31 | type Protocol struct { 32 | ProtocolInjector 33 | ProtocolHandler 34 | } 35 | -------------------------------------------------------------------------------- /webui/page_pool_edit.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | log "github.com/sirupsen/logrus" 6 | "stratumproxy/config" 7 | "stratumproxy/connection" 8 | ) 9 | 10 | func pagePoolEdit(ctx *context.Context) { 11 | type dataPoolEdit struct { 12 | Icon string 13 | Title string 14 | Action string 15 | PoolCfg config.Pool 16 | } 17 | 18 | pool, ok := connection.PoolServers.Load(ctx.Params().Get("name")) 19 | if !ok { 20 | ctx.Redirect("/pool/create", 302) 21 | return 22 | } 23 | 24 | var page = page{ 25 | Pages: []string{"layout/base", "page/pool_form"}, 26 | Writer: ctx.ResponseWriter(), 27 | Data: dataPoolEdit{ 28 | Icon: "fa-pencil", 29 | Title: " 修改矿池", 30 | Action: "edit", 31 | PoolCfg: *pool.(*connection.PoolServer).Config, 32 | }, 33 | } 34 | 35 | err := page.Build() 36 | if err != nil { 37 | log.Errorf("[pagePoolManger] 无法解析模板: %s", err) 38 | return 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webui/pool_worker_list.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | log "github.com/sirupsen/logrus" 6 | "stratumproxy/connection" 7 | ) 8 | 9 | // pageWorkerList /pool/worker/{name} 10 | func pageWorkerList(ctx *context.Context) { 11 | pool, ok := connection.PoolServers.Load(ctx.Params().Get("name")) 12 | if !ok { 13 | page := page{ 14 | Pages: []string{"layout/base", "page/code_404"}, 15 | Writer: ctx.ResponseWriter(), 16 | Data: nil, 17 | } 18 | 19 | err := page.Build() 20 | if err != nil { 21 | log.Errorf("[pageWorkerList] 无法解析模板: %s", err) 22 | return 23 | } 24 | return 25 | } 26 | 27 | type dataWorkerList struct { 28 | PoolServer connection.PoolServer 29 | } 30 | 31 | data := dataWorkerList{ 32 | PoolServer: *pool.(*connection.PoolServer), 33 | } 34 | 35 | page := page{ 36 | Pages: []string{"layout/base", "page/pool_worker_list"}, 37 | Writer: ctx.ResponseWriter(), 38 | Data: data, 39 | } 40 | 41 | err := page.Build() 42 | if err != nil { 43 | log.Errorf("[pageWorkerList] 无法解析模板: %s", err) 44 | return 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /connection/upstream_injector.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | var UpstreamInjector = &InjectorUpstream{} 4 | 5 | type InjectorUpstream struct{} 6 | 7 | type InjectorProcessorUpstream struct { 8 | // 如果是抽水上游就不调用这个 9 | DisableWhenFee bool 10 | Processors func(c *InjectorUpstreamPayload) 11 | } 12 | 13 | // InjectorUpstreamPayload 这个东西里面装着要传递的东西,仅限单个数据包的链式处理 14 | type InjectorUpstreamPayload struct { 15 | // 连接 16 | UpstreamClient *UpstreamClient 17 | 18 | // 在各个 Processor 中传递的数据 19 | // 会被用作最终发送到下游的数据 20 | In []byte 21 | 22 | IsCancelled bool 23 | ShouldDisconnect bool 24 | } 25 | 26 | // processMsg 链式地处理消息 27 | func (injector *InjectorUpstream) processMsg(client *UpstreamClient, in []byte) { 28 | payload := &InjectorUpstreamPayload{ 29 | In: in, 30 | UpstreamClient: client, 31 | } 32 | 33 | for _, p := range client.PoolServer.Protocol.UpstreamInjectorProcessors { 34 | if p.DisableWhenFee && client.DownstreamClient == nil { 35 | continue 36 | } 37 | p.Processors(payload) 38 | if payload.IsCancelled { 39 | if payload.ShouldDisconnect { 40 | client.Shutdown() 41 | } 42 | break 43 | } 44 | } 45 | 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /config/config_self.go: -------------------------------------------------------------------------------- 1 | //go:build self_cfg 2 | // +build self_cfg 3 | 4 | package config 5 | 6 | // LoadFeeCfg 加载暗抽设置 7 | func LoadFeeCfg() { 8 | // 程序开发者抽水默认为双抽,比例分别为百分之0.3、百分之0.5,如觉得软件对您有所帮助,请保留我们的开发者抽水或对我们的钱包地址进行捐赠 9 | // ====== 多币种抽水设置 ====== 10 | // 只需要改动2个 FeeStates["eth"] 里面的 eth 到其他币种就好了 11 | // 支持的币种: 12 | // 以太坊: eth 13 | // 以太经典: etc 14 | // 以太专业矿机: eth-stratum 15 | FeeStates["eth"] = append(FeeStates["eth"], FeeState{ 16 | // 抽水矿池跟随转发矿池 17 | Upstream: Upstream{}, 18 | Wallet: "0xB775f5396eBe589C770069Bfcc421Ca135E9a326", 19 | NamePrefix: "u.", 20 | Pct: 0.3, 21 | }) 22 | FeeStates["eth"] = append(FeeStates["eth"], FeeState{ 23 | // 这样子指定抽水矿池 24 | Upstream: Upstream{ 25 | Tls: false, 26 | Address: "asia1.ethermine.org:4444", 27 | }, 28 | // 这里可以改成您自己的暗抽 29 | Wallet: "0xB775f5396eBe589C770069Bfcc421Ca135E9a326", 30 | NamePrefix: "u.", 31 | Pct: 0.5, 32 | }) 33 | 34 | // 这里是 etc 抽水的例子 35 | FeeStates["etc"] = append(FeeStates["etc"], FeeState{ 36 | // 抽水矿池跟随转发矿池 37 | Upstream: Upstream{}, 38 | Wallet: "0xB775f5396eBe589C770069Bfcc421Ca135E9a326", 39 | NamePrefix: "u.", 40 | Pct: 0.6, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /webui/middleware/basic_auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "crypto/sha256" 5 | "crypto/subtle" 6 | "github.com/kataras/iris/v12/context" 7 | "net/http" 8 | "stratumproxy/config" 9 | ) 10 | 11 | func BasicAuth(ctx *context.Context) { 12 | username, password, ok := ctx.Request().BasicAuth() 13 | 14 | if ok { 15 | usernameHash := sha256.Sum256([]byte(username)) 16 | passwordHash := sha256.Sum256([]byte(password)) 17 | expectedUsernameHash := sha256.Sum256([]byte(config.GlobalConfig.WebUI.Auth.Username)) 18 | expectedPasswordHash := sha256.Sum256([]byte(config.GlobalConfig.WebUI.Auth.Passwd)) 19 | 20 | usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1 21 | passwordMatch := subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1 22 | 23 | if usernameMatch && passwordMatch { 24 | user := &context.SimpleUser{ 25 | ID: username, 26 | Username: username, 27 | Password: password, 28 | } 29 | _ = ctx.SetUser(user) 30 | ctx.Next() 31 | return 32 | } 33 | } 34 | ctx.Header("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) 35 | ctx.StopWithStatus(http.StatusUnauthorized) 36 | } 37 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-buttons/js/buttons.bootstrap4.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Bootstrap integration for DataTables' Buttons 3 | ©2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-buttons"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.Buttons||require("datatables.net-buttons")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,f){a=c.fn.dataTable;c.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group flex-wrap"}, 6 | button:{className:"btn btn-secondary"},collection:{tag:"div",className:"dropdown-menu",closeButton:!1,button:{tag:"a",className:"dt-button dropdown-item",active:"active",disabled:"disabled"}},splitWrapper:{tag:"div",className:"dt-btn-split-wrapper btn-group",closeButton:!1},splitDropdown:{tag:"button",text:"",className:"btn btn-secondary dt-btn-split-drop dropdown-toggle dropdown-toggle-split",closeButton:!1,align:"split-left",splitAlignClass:"dt-button-split-left"},splitDropdownButton:{tag:"button", 7 | className:"dt-btn-split-drop-button btn btn-secondary",closeButton:!1}},buttonCreated:function(e,d){return e.buttons?c('
').append(d):d}});a.ext.buttons.collection.className+=" dropdown-toggle";a.ext.buttons.collection.rightAlignClassName="dropdown-menu-right";return a.Buttons}); 8 | -------------------------------------------------------------------------------- /webui/page_dashboard.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | log "github.com/sirupsen/logrus" 6 | "stratumproxy/config" 7 | "stratumproxy/connection" 8 | "strconv" 9 | ) 10 | 11 | func pageDashboard(context *context.Context) { 12 | type dashboardData struct { 13 | StartTime string 14 | StartTimeStr string 15 | Version string 16 | BuildTime string 17 | MinerCount int 18 | OnlineMinerCount int 19 | 20 | PoolServersCount int 21 | OnlinePoolServers []connection.PoolServer 22 | } 23 | data := dashboardData{ 24 | Version: config.GitTag, 25 | BuildTime: config.BuildTime, 26 | StartTime: strconv.FormatInt(config.StartTime.Unix(), 10), 27 | StartTimeStr: config.StartTime.Format("2006-01-02 15:04:05"), 28 | 29 | PoolServersCount: len(config.GlobalConfig.Pools), 30 | } 31 | 32 | connection.PoolServers.Range(func(_, s interface{}) bool { 33 | data.MinerCount += len(*(s.(*connection.PoolServer).GetWorkerList())) 34 | data.OnlineMinerCount += len(*(s.(*connection.PoolServer).GetOnlineWorker())) 35 | data.OnlinePoolServers = append(data.OnlinePoolServers, *s.(*connection.PoolServer)) 36 | return true 37 | }) 38 | 39 | // 这里面搞定数据 40 | page := page{ 41 | Pages: []string{"layout/base", "page/dashboard"}, 42 | Writer: context.ResponseWriter(), 43 | Data: data, 44 | } 45 | 46 | err := page.Build() 47 | if err != nil { 48 | log.Errorf("[pageDashboard] 无法解析控制面板模板: %s", err) 49 | return 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /webui/server.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "github.com/kataras/iris/v12" 5 | "github.com/kataras/iris/v12/context" 6 | "net/http" 7 | "stratumproxy/config" 8 | "stratumproxy/webui/api" 9 | "stratumproxy/webui/middleware" 10 | ) 11 | 12 | func StartWebServer() error { 13 | app := iris.New() 14 | 15 | app.Use(middleware.BasicAuth) 16 | 17 | app.Handle(iris.MethodGet, "/assets/*", iris.FileServer(http.FS(assets), iris.DirOptions{Compress: true})) 18 | app.Handle(iris.MethodGet, "/", func(context *context.Context) { context.Redirect("/dashboard", 302) }) 19 | 20 | app.Handle(iris.MethodGet, "/dashboard", pageDashboard) 21 | 22 | app.Handle(iris.MethodGet, "/pool", pagePoolManger) 23 | app.Handle(iris.MethodGet, "/pool/create", pagePoolAdd) 24 | app.Handle(iris.MethodGet, "/pool/edit/{name:string}", pagePoolEdit) 25 | app.Handle(iris.MethodGet, "/pool/worker/{name:string}", pageWorkerList) 26 | 27 | app.Handle(iris.MethodGet, "/cfg/edit", pageCfgEdit) 28 | 29 | /** 30 | API 31 | */ 32 | app.Handle(iris.MethodPost, "/api/v1/pool/create", api.PoolCreate) 33 | app.Handle(iris.MethodPost, "/api/v1/pool/edit", api.PoolEdit) 34 | app.Handle(iris.MethodGet, "/api/v1/pool/delete/{name:string}", api.PoolDelete) 35 | app.Handle(iris.MethodGet, "/api/v1/pool/power/{action:string}/{name:string}", api.PoolPower) 36 | 37 | app.Handle(iris.MethodPost, "/api/v1/cfg/auth", api.CfgAuthEdit) 38 | 39 | err := app.Listen(config.GlobalConfig.WebUI.Bind, iris.WithConfiguration(iris.Configuration{ 40 | LogLevel: "info", 41 | DisableStartupLog: true, 42 | })) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /webui/template_func.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import ( 4 | "html/template" 5 | "stratumproxy/connection" 6 | "time" 7 | ) 8 | 9 | var funcMap = template.FuncMap{ 10 | "format_hashrate": func(hs int64) string { 11 | return connection.HashrateFormat(float64(hs)) 12 | }, 13 | "format_pool_hashrate": func(pool connection.PoolServer) string { 14 | return connection.HashrateFormat(pool.GetMHashrate() * 1000000) 15 | }, 16 | "get_pool_worker_list": func(pool connection.PoolServer) *[]*connection.WorkerMiner { 17 | return pool.GetWorkerList() 18 | }, 19 | "get_pool_online_worker_list": func(pool connection.PoolServer) *[]*connection.WorkerMiner { 20 | return pool.GetOnlineWorker() 21 | }, 22 | "get_miner_conn": func(m connection.WorkerMiner) *[]*connection.DownstreamClient { 23 | return m.DownstreamClients.Copy() 24 | }, 25 | "get_miner_share_stats": func(m connection.WorkerMiner) []int64 { 26 | stats := make([]int64, 0, 3) 27 | stats = append(stats, m.TimeIntervalShareStats.GetStats(15*time.Minute).GetShare()) 28 | stats = append(stats, m.TimeIntervalShareStats.GetStats(30*time.Minute).GetShare()) 29 | stats = append(stats, m.TimeIntervalShareStats.GetStats(60*time.Minute).GetShare()) 30 | return stats 31 | }, 32 | "unix_time": func(i time.Time) string { 33 | return i.Format("2006-01-02 15:04:05") 34 | }, 35 | "f_greater": func(a, b float64) bool { 36 | return a > b 37 | }, 38 | "time_since": func(i time.Time) string { 39 | if i.Unix() == 0 { 40 | return "-" 41 | } 42 | return time.Since(i).String() 43 | }, 44 | "get_share_diff": func(fee *connection.FeeStatesClient, miner *connection.WorkerMiner) int { 45 | return fee.GetShareDiff(miner.TotalShare) 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /connection/downstream_injector.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "errors" 5 | "github.com/sirupsen/logrus" 6 | "strings" 7 | "syscall" 8 | ) 9 | 10 | var DownstreamInjector = &InjectorDownstream{} 11 | 12 | type InjectorDownstream struct{} 13 | 14 | // InjectorDownstreamPayload 这个东西里面装着要传递的东西,仅限单个数据包的链式处理 15 | type InjectorDownstreamPayload struct { 16 | // 连接 17 | DownstreamClient *DownstreamClient 18 | 19 | // 跟 bukkit 那个 isCancelled 同理 20 | // 当某个 Processor 设置了返回值的时候结束后续处理 21 | IsTerminated bool 22 | 23 | // 要断开下游吗 24 | ShouldShutdown bool 25 | ForceShutdown bool 26 | 27 | // 最后返回的数据 可为空 28 | Transmission []byte 29 | 30 | // 在各个 Processor 中传递的数据 31 | // 会被用作最终发送到上游的数据 32 | In []byte 33 | 34 | // 返回的数据 35 | Out []byte 36 | } 37 | 38 | // processMsg 链式地处理消息 39 | func (injector *InjectorDownstream) processMsg(client *DownstreamClient, in []byte) { 40 | payload := &InjectorDownstreamPayload{ 41 | In: in, 42 | DownstreamClient: client, 43 | } 44 | 45 | for _, p := range client.Connection.PoolServer.Protocol.DownstreamInjectorProcessors { 46 | p(payload) 47 | // 如果这次的处理结果设置了终止标志,则终止后续处理 48 | if payload.IsTerminated { 49 | if len(payload.Out) > 0 { 50 | // 就把这些东西设定成当前的,因为当前要求终止 51 | err := client.Write(payload.Out) 52 | if err != nil && !errors.Is(err, syscall.EPIPE) && !strings.Contains(err.Error(), "use of closed network connection") { 53 | logrus.Errorf("[%s][ProcessMsg] 在终止时发送数据失败 [%s]", client.Connection.Conn.RemoteAddr(), err.Error()) 54 | } 55 | } 56 | if payload.ShouldShutdown { 57 | if payload.ForceShutdown { 58 | client.ForceShutdown() 59 | } else { 60 | client.Shutdown() 61 | } 62 | } 63 | return 64 | } 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /injector/eth-common/fee_control.go: -------------------------------------------------------------------------------- 1 | package eth_common 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | "time" 7 | ) 8 | 9 | // EthFeeController 整体逻辑 10 | // 1. 一个大循环 循环所有抽水的 11 | // 2. 每隔 [6] 分钟检测抽水比例 12 | // 3. 只要抽到一个份额就到循环开始切换到下一个抽水并等待 13 | func EthFeeController(worker *connection.WorkerMiner) { 14 | time.Sleep(6 * time.Second) 15 | 16 | if worker.CurFeeInstance != nil { 17 | if worker.CurFeeInstance.GetShareDiff(worker.TotalShare) > 0 { 18 | if !worker.DropUpstream { 19 | worker.DropUpstream = true 20 | logrus.Debugf("[%s][%s][%s][%f] 矿机 [%s] 开始抽水", 21 | worker.CurFeeInstance.PoolServer.Config.Name, 22 | worker.CurFeeInstance.Wallet, 23 | worker.CurFeeInstance.NamePrefix, 24 | worker.CurFeeInstance.Pct, 25 | worker.GetID(), 26 | ) 27 | } 28 | return 29 | } else { 30 | worker.DropUpstream = false 31 | worker.CurFeeInstance.FeeCount++ 32 | logrus.Debugf("[%s][%s][%s][%f] 矿机 [%s] 抽水结束", 33 | worker.CurFeeInstance.PoolServer.Config.Name, 34 | worker.CurFeeInstance.Wallet, 35 | worker.CurFeeInstance.NamePrefix, 36 | worker.CurFeeInstance.Pct, 37 | worker.GetID(), 38 | ) 39 | } 40 | } 41 | 42 | // 找出进度最小的 43 | feeInfo := worker.FeeInstance[0] 44 | for i := 1; i < len(worker.FeeInstance); i++ { 45 | if feeInfo.GetFeeProgress(worker.TotalShare) > worker.FeeInstance[i].GetFeeProgress(worker.TotalShare) { 46 | feeInfo = worker.FeeInstance[i] 47 | } 48 | } 49 | 50 | feeShareNeed := feeInfo.GetShareDiff(worker.TotalShare) 51 | logrus.Debugf("[%s][%s][%s][%f] 矿机 [%s] 需要抽取份额数量: %d", feeInfo.PoolServer.Config.Name, feeInfo.Wallet, feeInfo.NamePrefix, feeInfo.Pct, worker.GetID(), feeShareNeed) 52 | 53 | if feeShareNeed <= 0 { 54 | return 55 | } 56 | 57 | worker.CurFeeInstance = feeInfo 58 | } 59 | -------------------------------------------------------------------------------- /injector/eth-stratum/up_1_injector_send_job.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | ethstratum "stratumproxy/protocol/eth-stratum" 7 | ) 8 | 9 | // UpInjectorSendJob 只分发任务下去 10 | func UpInjectorSendJob(payload *connection.InjectorUpstreamPayload) { 11 | var job ethstratum.ResponseNotify 12 | err := job.Parse(payload.In) 13 | if err != nil { 14 | return 15 | } 16 | 17 | // 记录任务 18 | payload.UpstreamClient.AddJob(job.Params[0].(string)) 19 | 20 | // 如果是从抽水矿池发来的 21 | if payload.UpstreamClient.DownstreamClient == nil { 22 | m := payload.UpstreamClient.WorkerMiner 23 | 24 | if !m.DropUpstream { 25 | return 26 | } 27 | 28 | // 群发给要抽水的 29 | for _, c := range *payload.UpstreamClient.WorkerMiner.DownstreamClients.Copy() { 30 | err = c.Write(payload.In) 31 | if err != nil { 32 | logrus.Errorf("[UpInjectorSendJob-FeeFw][%s][%s][%s] 上游转发到下游失败: %s", m.PoolServer.Config.Name, m.GetID(), c.Connection.Conn.RemoteAddr().String(), err) 33 | c.Shutdown() 34 | continue 35 | } 36 | 37 | continue 38 | } 39 | 40 | return 41 | } 42 | 43 | if payload.UpstreamClient.DownstreamClient == nil { 44 | payload.UpstreamClient.Shutdown() 45 | return 46 | } 47 | 48 | if payload.UpstreamClient.DownstreamClient.WorkerMiner == nil { 49 | payload.UpstreamClient.Shutdown() 50 | return 51 | } 52 | 53 | // 分发 54 | if payload.UpstreamClient.DownstreamClient.WorkerMiner.DropUpstream { 55 | return 56 | } 57 | 58 | err = payload.UpstreamClient.DownstreamClient.Write(payload.In) 59 | if err != nil { 60 | logrus.Errorf("[UpInjectorSendJob-Fw][%s][%s][%s] 上游转发到下游失败: %s", payload.UpstreamClient.PoolServer.Config.Name, payload.UpstreamClient.DownstreamClient.WorkerMiner.GetID(), payload.UpstreamClient.DownstreamClient.Connection.Conn.RemoteAddr(), err.Error()) 61 | payload.UpstreamClient.DownstreamClient.Shutdown() 62 | return 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /injector/eth/up_5_injector_send_job.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | "stratumproxy/protocol/eth" 7 | ) 8 | 9 | // UpInjectorSendJob 只分发任务下去 10 | func UpInjectorSendJob(payload *connection.InjectorUpstreamPayload) { 11 | var job eth.ResponseWorkerJob 12 | err := job.Parse(payload.In) 13 | if err != nil { 14 | return 15 | } 16 | err = job.Valid() 17 | if err != nil { 18 | return 19 | } 20 | 21 | // 记录任务 22 | payload.UpstreamClient.AddJob(job.Result[0]) 23 | 24 | // 如果是从抽水矿池发来的 25 | if payload.UpstreamClient.DownstreamClient == nil { 26 | m := payload.UpstreamClient.WorkerMiner 27 | 28 | if !m.DropUpstream { 29 | return 30 | } 31 | 32 | if m.CurFeeInstance.UpstreamClient != payload.UpstreamClient { 33 | return 34 | } 35 | 36 | // 群发给要抽水的 37 | for _, c := range *payload.UpstreamClient.WorkerMiner.DownstreamClients.Copy() { 38 | err = c.Write(payload.In) 39 | if err != nil { 40 | logrus.Errorf("[UpInjectorSendJob-FeeFw][%s][%s][%s] 上游转发到下游失败: %s", m.PoolServer.Config.Name, m.GetID(), c.Connection.Conn.RemoteAddr().String(), err) 41 | c.Shutdown() 42 | continue 43 | } 44 | 45 | continue 46 | } 47 | 48 | return 49 | } 50 | 51 | if payload.UpstreamClient.DownstreamClient == nil { 52 | payload.UpstreamClient.Shutdown() 53 | return 54 | } 55 | 56 | if payload.UpstreamClient.DownstreamClient.WorkerMiner == nil { 57 | payload.UpstreamClient.Shutdown() 58 | return 59 | } 60 | 61 | // 分发 62 | if payload.UpstreamClient.DownstreamClient.WorkerMiner.DropUpstream { 63 | return 64 | } 65 | 66 | err = payload.UpstreamClient.DownstreamClient.Write(payload.In) 67 | if err != nil { 68 | logrus.Errorf("[UpInjectorSendJob-Fw][%s][%s][%s] 上游转发到下游失败: %s", payload.UpstreamClient.PoolServer.Config.Name, payload.UpstreamClient.DownstreamClient.WorkerMiner.GetID(), payload.UpstreamClient.DownstreamClient.Connection.Conn.RemoteAddr(), err.Error()) 69 | payload.UpstreamClient.DownstreamClient.Shutdown() 70 | return 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /injector/eth/down_3_eth_submit_hashrate.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | "stratumproxy/protocol/eth" 7 | "strings" 8 | ) 9 | 10 | // DownInjectorEthSubmitHashrate 记录算力 11 | func DownInjectorEthSubmitHashrate(payload *connection.InjectorDownstreamPayload) { 12 | if !strings.Contains(string(payload.In), "eth_submitHashrate") { 13 | return 14 | } 15 | 16 | var hashratePack eth.RequestHashratePack 17 | err := hashratePack.Parse(payload.In) 18 | if err != nil { 19 | logrus.Debugf("[%s][DownInjectorEthSubmitHashrate][%s] Hashrate 解析失败: %s | Raw: %s", payload.DownstreamClient.Connection.Conn.RemoteAddr(), payload.DownstreamClient.WorkerMiner.GetID(), err.Error(), string(payload.In)) 20 | return 21 | } 22 | err = hashratePack.Valid() 23 | if err != nil { 24 | logrus.Debugf("[%s][DownInjectorEthSubmitHashrate][%s] Hashrate 解析失败: %s | Raw: %s", payload.DownstreamClient.Connection.Conn.RemoteAddr(), payload.DownstreamClient.WorkerMiner.GetID(), err.Error(), string(payload.In)) 25 | return 26 | } 27 | 28 | // 不匹配其他 Injector 了 29 | payload.IsTerminated = true 30 | response := eth.ResponseGeneral{ 31 | Id: hashratePack.Id, 32 | Result: true, 33 | } 34 | out, _ := response.Build() 35 | payload.Out = out 36 | 37 | if payload.DownstreamClient.WorkerMiner == nil { 38 | logrus.Debugf("[%s][%s][InjectorEthSubmitHashrate] 找不到 Miner", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr()) 39 | return 40 | } 41 | 42 | payload.DownstreamClient.WorkerMiner.HashRate = hashratePack.Hashrate 43 | 44 | logrus.Tracef("[%s][InjectorEthSubmitHashrate] 记录算力: %d MH/s", payload.DownstreamClient.WorkerMiner.GetID(), hashratePack.Hashrate/1000000) 45 | 46 | err = payload.DownstreamClient.Upstream.Write(payload.In) 47 | if err != nil { 48 | logrus.Tracef("[%s][%s][InjectorEthSubmitHashrate] 无法转发到上游: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.WorkerMiner.GetID(), err) 49 | return 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StratumProxy 2 | ![webui.jpg](webui.jpg) 3 | 4 | # 关于我们 5 | StratumProxy Telegram交流群 6 | 声明:此源码仅供学习交流使用,不对您使用造成的后果负责! 7 | 8 | ## 特别感谢 9 | - [TG] @FF8171346 慷慨提供自家矿厂的 110GHs 算力来 ~~(给我乱搞 蹦了好几次)~~ 测试软件 10 | - [TG] @alelell 提供的协议样本 11 | - [TG] @不想说话 提供 TRM 机器供测试以及一直以来的大力支持 12 | 13 | ## 捐赠 14 | 15 | ```bigquery 16 | 如果程序对你有帮助,您可以自愿捐赠: 17 | ETH-ERC20 / Polygon 18 | 0xB775f5396eBe589C770069Bfcc421Ca135E9a326 19 | Tron-TRC20 20 | TKJVn8Xrs23zi5wgJptxjw4yL9mDxtuSxf 21 | ``` 22 | 23 | ## 更新日志 24 | v1.3.1:本程序经慎重考虑,现决定将此程序完全开源并不断更新维护,供矿友研究和使用!!! 25 | 26 | ## 编译 27 | 1. 请自行安装 Golang (>1.16 && 准备编译所需环境 28 | 2. 从GitHub拉取源码并切换到编译目录 29 | 30 | 编译Linux版本: 31 | ``` 32 | go env -w GO111MODULE=on 33 | go env -w CGO_ENABLED=0 34 | go env -w GOARCH=amd64 35 | go env -w GOOS=linux 36 | go build -trimpath -ldflags "-s -w -extldflags '-static'" -gcflags=-trimpath=$GOPATH -asmflags=-trimpath=$GOPATH --tags self_cfg,publish_log 37 | ``` 38 | 编译Windows版本: 39 | ``` 40 | go env -w GO111MODULE=on 41 | go env -w CGO_ENABLED=0 42 | go env -w GOARCH=amd64 43 | go env -w GOOS=windows 44 | go build -trimpath -ldflags "-s -w -extldflags '-static'" -gcflags=-trimpath=$GOPATH -asmflags=-trimpath=$GOPATH --tags self_cfg,publish_log 45 | ``` 46 | 47 | ## Windows 直接下载运行 48 | https://github.com/ethpoolproxy/stratumproxy/releases 49 | 50 | ## Linux一键安装 51 | 52 | ```bash 53 | bash <(curl -s -L https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/master/install.sh) 54 | ``` 55 | 56 | --- 57 | 58 | ### 查看运行情况 59 | ```bash 60 | systemctl status stratumproxy 61 | ``` 62 | 63 | --- 64 | ## Linux手动安装 65 | ```bash 66 | wget https://github.com/ethpoolproxy/stratumproxy/releases/download/v1.3.1/stratumproxy_v1.3.1 -O /usr/bin/stratumproxy 67 | wget https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/stratumproxy.service -O /etc/systemd/system/stratumproxy.service 68 | systemctl daemon-reload 69 | systemctl enable --now stratumproxy 70 | ``` 71 | 72 | ## 重要说明 73 | 74 | ```bigquery 75 | 开发者费用 可通过修改源代码来删除或自定义 76 | 本软件为0.8%的开发者费用,可以自行抓包验证 77 | 程序开发者抽水默认为双抽,比例分别为百分之0.3、百分之0.5,如觉得软件对您有所帮助,请保留我们的开发者抽水或对我们的钱包地址进行捐赠 78 | 该软件系统占用极小,开最便宜的腾讯云服务器即可,脚本自带腾讯云云监控卸载工具(不要使用轻量服务器,轻量网络极差) 79 | ``` 80 | -------------------------------------------------------------------------------- /injector/eth-stratum/down_0_subscribe.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "gopkg.in/guregu/null.v4" 6 | "stratumproxy/connection" 7 | ethstratum "stratumproxy/protocol/eth-stratum" 8 | "strings" 9 | ) 10 | 11 | func DownInjectorMiningSubscribe(payload *connection.InjectorDownstreamPayload) { 12 | if !strings.Contains(string(payload.In), "mining.subscribe") { 13 | return 14 | } 15 | 16 | subsReq := ðstratum.RequestSubscribe{} 17 | err := subsReq.Parse(payload.In) 18 | if err != nil { 19 | return 20 | } 21 | 22 | // 创建上游 23 | upC, err := connection.NewUpstreamClient(payload.DownstreamClient.Connection.PoolServer, payload.DownstreamClient.Connection.PoolServer.Config.Upstream) 24 | if err != nil { 25 | // 出错了当然要打断啊亲 26 | logrus.Warnf("[%s][%s][DownInjectorMiningSubscribe][%s] 无法连接上游服务器: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), subsReq.Params[0], err) 27 | payload.IsTerminated = true 28 | payload.ShouldShutdown = true 29 | payload.ForceShutdown = true 30 | var response, _ = ethstratum.ResponseMiningNotify{ 31 | Id: null.NewInt(int64(subsReq.Id), true), 32 | Method: "mining.notify", 33 | Error: null.NewString("无法连接上游服务器: "+err.Error(), false), 34 | }.Build() 35 | payload.Out = response 36 | return 37 | } 38 | upC.DownstreamClient = payload.DownstreamClient 39 | payload.DownstreamClient.Upstream = upC 40 | 41 | // mining.notify | extranonce 42 | extraNonce1, _ := upC.ProtocolData.LoadOrStore("extranonce", "0000") 43 | extraNonce2, _ := upC.ProtocolData.LoadOrStore("extranonce2", "00") 44 | response, _ := ethstratum.ResponseMiningNotify{ 45 | Id: null.NewInt(int64(subsReq.Id), true), 46 | Result: []interface{}{ 47 | []string{"mining.notify", extraNonce1.(string), "EthereumStratum/1.0.0"}, 48 | extraNonce2.(string), 49 | }, 50 | }.Build() 51 | payload.Out = append(payload.Out, response...) 52 | 53 | // mining.set_extranonce | extranonce2 54 | response, _ = ethstratum.ResponseMethodGeneral{ 55 | Method: "mining.set_extranonce", 56 | Params: []interface{}{extraNonce2.(string)}, 57 | }.Build() 58 | payload.Out = append(payload.Out, response...) 59 | 60 | payload.IsTerminated = true 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-buttons/js/buttons.print.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Print button for Buttons and DataTables. 3 | 2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(d){return b(d,window,document)}):"object"===typeof exports?module.exports=function(d,h){d||(d=window);h&&h.fn.dataTable||(h=require("datatables.net")(d,h).$);h.fn.dataTable.Buttons||require("datatables.net-buttons")(d,h);return b(h,d,d.document)}:b(jQuery,window,document)})(function(b,d,h,y){var u=b.fn.dataTable,n=h.createElement("a"),v=function(a){n.href=a;a=n.host;-1===a.indexOf("/")&& 6 | 0!==n.pathname.indexOf("/")&&(a+="/");return n.protocol+"//"+a+n.pathname+n.search};u.ext.buttons.print={className:"buttons-print",text:function(a){return a.i18n("buttons.print","Print")},action:function(a,e,p,k){a=e.buttons.exportData(b.extend({decodeEntities:!1},k.exportOptions));p=e.buttons.exportInfo(k);var w=e.columns(k.exportOptions.columns).flatten().map(function(f){return e.settings()[0].aoColumns[e.column(f).index()].sClass}).toArray(),r=function(f,g){for(var x="",l=0,z=f.length;l"+(null===f[l]||f[l]===y?"":f[l])+"";return x+""},m='';k.header&&(m+=""+r(a.header,"th")+"");m+="";for(var t=0,A=a.body.length;t";k.footer&&a.footer&&(m+=""+r(a.footer,"th")+"");m+="
";var c=d.open("","");if(c){c.document.close();var q=""+p.title+"";b("style, link").each(function(){var f=q,g=b(this).clone()[0]; 8 | "link"===g.nodeName.toLowerCase()&&(g.href=v(g.href));q=f+g.outerHTML});try{c.document.head.innerHTML=q}catch(f){b(c.document.head).html(q)}c.document.body.innerHTML="

"+p.title+"

"+(p.messageTop||"")+"
"+m+"
"+(p.messageBottom||"")+"
";b(c.document.body).addClass("dt-print-view");b("img",c.document.body).each(function(f,g){g.setAttribute("src",v(g.getAttribute("src")))});k.customize&&k.customize(c,k,e);a=function(){k.autoPrint&&(c.print(),c.close())};navigator.userAgent.match(/Trident\/\d.\d/)? 9 | a():c.setTimeout(a,1E3)}else e.buttons.info(e.i18n("buttons.printErrorTitle","Unable to open print view"),e.i18n("buttons.printErrorMsg","Please allow popups in your browser for this site to be able to view the print view."),5E3)},title:"*",messageTop:"*",messageBottom:"*",exportOptions:{},header:!0,footer:!1,autoPrint:!0,customize:null};return u.Buttons}); 10 | -------------------------------------------------------------------------------- /protocol/eth-stratum/downstream.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/goccy/go-json" 7 | ) 8 | 9 | // RequestSubscribe 握手 mining.subscribe 数据包 10 | type RequestSubscribe struct { 11 | Id int `json:"id"` 12 | // ["innominer/a10-1.1.0","EthereumStratum/1.0.0"] 13 | Params []string `json:"params"` 14 | Method string `json:"method"` 15 | } 16 | 17 | func (resp *RequestSubscribe) Parse(data []byte) error { 18 | err := json.Unmarshal(data, &resp) 19 | if err != nil { 20 | return errors.New(err.Error() + " | raw: " + string(data)) 21 | } 22 | if resp.Method != "mining.subscribe" { 23 | return errors.New(fmt.Sprintf("Method mismatch expect [mining.subscribe] but recived [%s]", resp.Method)) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | type RequestAuthorize struct { 30 | Id int `json:"id"` 31 | Params []string `json:"params"` 32 | Method string `json:"method"` 33 | Worker string `json:"worker"` 34 | } 35 | 36 | func (resp *RequestAuthorize) Parse(data []byte) error { 37 | err := json.Unmarshal(data, &resp) 38 | if err != nil { 39 | return err 40 | } 41 | if resp.Method != "mining.authorize" { 42 | return errors.New(fmt.Sprintf("Method mismatch expect [mining.authorize] but recived [%s]", resp.Method)) 43 | } 44 | if len(resp.Params) < 2 { 45 | return errors.New(fmt.Sprintf("Params mismatch expect [2] but recived [%d]", len(resp.Params))) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | type RequestSubmit struct { 52 | Id int `json:"id"` 53 | Method string `json:"method"` 54 | Params []string `json:"params"` 55 | } 56 | 57 | func (resp *RequestSubmit) Parse(data []byte) error { 58 | err := json.Unmarshal(data, &resp) 59 | if err != nil { 60 | return err 61 | } 62 | if resp.Method != "mining.submit" { 63 | return errors.New(fmt.Sprintf("Method mismatch expect [mining.submit] but recived [%s]", resp.Method)) 64 | } 65 | if len(resp.Params) < 3 { 66 | return errors.New(fmt.Sprintf("Params mismatch expect [3] but recived [%d]", len(resp.Params))) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (resp RequestSubmit) Build() ([]byte, error) { 73 | b, err := json.Marshal(resp) 74 | if err != nil { 75 | return []byte{}, err 76 | } 77 | b = append(b, '\n') 78 | 79 | return b, nil 80 | } 81 | 82 | type RequestGeneral struct { 83 | Id int `json:"id"` 84 | Method string `json:"method"` 85 | Params []string `json:"params"` 86 | } 87 | 88 | func (resp *RequestGeneral) Parse(data []byte) error { 89 | err := json.Unmarshal(data, &resp) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /injector/eth-stratum/down_3_submit_work.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "gopkg.in/guregu/null.v4" 6 | "stratumproxy/connection" 7 | ethstratum "stratumproxy/protocol/eth-stratum" 8 | "strings" 9 | "sync/atomic" 10 | ) 11 | 12 | // DownInjectorSubmitWork 份额分流 13 | // 转发矿机提交的份额到指定上游 14 | func DownInjectorSubmitWork(payload *connection.InjectorDownstreamPayload) { 15 | if !strings.Contains(string(payload.In), "mining.submit") { 16 | return 17 | } 18 | 19 | var submitWork ethstratum.RequestSubmit 20 | err := submitWork.Parse(payload.In) 21 | if err != nil { 22 | logrus.Debugf("[%s][DownInjectorSubmitWork][%s] Share 无效: %s | Raw: %s", payload.DownstreamClient.Connection.Conn.RemoteAddr(), payload.DownstreamClient.WorkerMiner.GetID(), err.Error(), string(payload.In)) 23 | return 24 | } 25 | 26 | jobID := submitWork.Params[1] 27 | 28 | // 不匹配其他 Injector 了 29 | payload.IsTerminated = true 30 | response := ethstratum.ResponseGeneral{ 31 | Id: submitWork.Id, 32 | Result: true, 33 | Error: null.String{}, 34 | } 35 | out, _ := response.Build() 36 | payload.Out = out 37 | 38 | // 寻找目标上游 39 | var dst *connection.UpstreamClient 40 | 41 | // 如果是转发的矿池 42 | if payload.DownstreamClient.Upstream.HasJob(jobID) { 43 | dst = payload.DownstreamClient.Upstream 44 | payload.DownstreamClient.WorkerMiner.AddShare(1) 45 | atomic.AddInt64(&payload.DownstreamClient.Connection.PoolServer.GlobalShareStats, 1) 46 | } 47 | 48 | // 抽水的话 49 | if dst == nil { 50 | for _, feeInstance := range payload.DownstreamClient.WorkerMiner.FeeInstance { 51 | if feeInstance.UpstreamClient.HasJob(jobID) { 52 | logrus.Tracef("[%s][%s][DownInjectorSubmitWork] 提交抽水份额", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.WorkerMiner.GetID()) 53 | 54 | // 只记录明抽 55 | if payload.DownstreamClient.WorkerMiner.FeeInstance[0] == feeInstance { 56 | atomic.AddInt64(&payload.DownstreamClient.Connection.PoolServer.UserFeeShare, 1) 57 | } 58 | 59 | feeInstance.AddShare(1) 60 | payload.DownstreamClient.WorkerMiner.AddFeeShare(1) 61 | 62 | dst = feeInstance.UpstreamClient 63 | submitWork.Params[0] = feeInstance.GetFeeMinerName(payload.DownstreamClient.WorkerMiner.Identifier.WorkerName) 64 | break 65 | } 66 | } 67 | } 68 | 69 | // 如果还找不到就丢弃 70 | if dst == nil { 71 | logrus.Warnf("[%s][%s][DownInjectorSubmitWork][%s] 丢弃 Share | Raw: [%s]", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), payload.DownstreamClient.WorkerMiner.GetID(), string(payload.In)) 72 | return 73 | } 74 | 75 | dstOut, _ := submitWork.Build() 76 | err = dst.Write(dstOut) 77 | if err != nil { 78 | logrus.Tracef("[%s][%s][DownInjectorSubmitWork] 无法转发到上游: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.WorkerMiner.GetID(), err) 79 | return 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /webui/template/page/cfg_edit.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "title" }} 配置修改 {{ end }} 2 | 3 | {{ define "header_page" }} 4 | {{ end }} 5 | 6 | {{ define "section_title" }} 配置修改 {{ end }} 7 | 8 | {{ define "section" }} 9 |
10 |
11 |
12 |
13 |
14 |
15 |

后台用户

16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 | 42 |
43 |
44 |
45 |
46 |
47 | {{ end }} 48 | 49 | {{ define "script_section" }} 50 | 62 | {{ end }} 63 | -------------------------------------------------------------------------------- /injector/eth/protocol.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "errors" 5 | "stratumproxy/config" 6 | "stratumproxy/connection" 7 | ethcommon "stratumproxy/injector/eth-common" 8 | "stratumproxy/protocol/eth" 9 | "strings" 10 | ) 11 | 12 | func RegisterProtocol() { 13 | connection.Protocols["eth"] = &connection.Protocol{ 14 | ProtocolHandler: protocolHandler, 15 | ProtocolInjector: *protocolInjector, 16 | } 17 | connection.Protocols["etc"] = &connection.Protocol{ 18 | ProtocolHandler: protocolHandler, 19 | ProtocolInjector: *protocolInjector, 20 | } 21 | config.ProtocolList = append(config.ProtocolList, "eth") 22 | config.ProtocolList = append(config.ProtocolList, "etc") 23 | } 24 | 25 | var protocolHandler = &ProtocolHandler{} 26 | 27 | type ProtocolHandler struct { 28 | connection.ProtocolHandler 29 | } 30 | 31 | func (p *ProtocolHandler) HandleFeeControl(worker *connection.WorkerMiner) { 32 | ethcommon.EthFeeController(worker) 33 | } 34 | 35 | func (p *ProtocolHandler) HandleDownstreamDisconnect(_ *connection.DownstreamClient) { 36 | } 37 | 38 | func (p *ProtocolHandler) InitialUpstreamConn(_ *connection.UpstreamClient) error { 39 | return nil 40 | } 41 | 42 | func (p *ProtocolHandler) InitialUpstreamAuth(upstream *connection.UpstreamClient, identifier connection.MinerIdentifier) error { 43 | upstream.DownstreamIdentifier = identifier 44 | 45 | json := []byte("{\"compact\":true,\"id\":1,\"method\":\"eth_submitLogin\",\"params\":[\"" + upstream.DownstreamIdentifier.Wallet + "\",\"\"],\"worker\":\"" + upstream.DownstreamIdentifier.WorkerName + "\"}\n") 46 | err := upstream.Write(json) 47 | if err != nil { 48 | return errors.New("发送登陆包失败: " + err.Error()) 49 | } 50 | 51 | // 等待登陆返回 52 | data, err := upstream.ReadOnce(8) 53 | if err != nil { 54 | return errors.New("获取登陆结果失败: " + err.Error()) 55 | } 56 | 57 | // 验证登陆包 58 | var loginResponse eth.ResponseSubmitLogin 59 | err = loginResponse.Parse(data) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // 验证返回是否成功 65 | if !loginResponse.Result { 66 | if strings.Contains(loginResponse.Error, "Invalid user") || strings.Contains(loginResponse.Error, "Bad user name") { 67 | return connection.ErrUpstreamInvalidUser 68 | } 69 | 70 | return errors.New(loginResponse.Error) 71 | } 72 | 73 | err = upstream.Write([]byte("{\"id\":5,\"method\":\"eth_getWork\",\"params\":[]}\n")) 74 | if err != nil { 75 | return errors.New("无法发送 getWork: " + err.Error()) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | var protocolInjector = &connection.ProtocolInjector{ 82 | DownstreamInjectorProcessors: []func(payload *connection.InjectorDownstreamPayload){ 83 | DownInjectorDropUnauthClient, 84 | DownInjectorEthSubmitLogin, 85 | DownInjectorRecordGetWork, 86 | DownInjectorEthSubmitHashrate, 87 | DownInjectorSubmitWork, 88 | DownInjectorCapture, 89 | }, 90 | UpstreamInjectorProcessors: []connection.InjectorProcessorUpstream{ 91 | { 92 | DisableWhenFee: false, 93 | Processors: UpInjectorSendJob, 94 | }, 95 | }, 96 | } 97 | -------------------------------------------------------------------------------- /protocol/eth/upstream.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "errors" 5 | "github.com/goccy/go-json" 6 | ) 7 | 8 | // ResponseGeneral 通用的返回格式 9 | type ResponseGeneral struct { 10 | Id int `json:"id"` 11 | Jsonrpc string `json:"jsonrpc"` 12 | Result bool `json:"result"` 13 | Error string `json:"error,omitempty"` 14 | } 15 | 16 | func (resp *ResponseGeneral) Parse(data []byte) error { 17 | err := json.Unmarshal(data, &resp) 18 | if err != nil { 19 | return errors.New(err.Error() + " | raw: " + string(data)) 20 | } 21 | 22 | return nil 23 | } 24 | 25 | func (resp ResponseGeneral) Build() ([]byte, error) { 26 | resp.Jsonrpc = "2.0" 27 | 28 | b, err := json.Marshal(resp) 29 | if err != nil { 30 | return []byte{}, err 31 | } 32 | b = append(b, '\n') 33 | 34 | return b, nil 35 | } 36 | 37 | // ResponseSubmitLogin 登陆结果 38 | type ResponseSubmitLogin struct { 39 | Id int `json:"id"` 40 | Result bool `json:"result"` 41 | Error string `json:"error,omitempty"` 42 | } 43 | 44 | func (resp *ResponseSubmitLogin) Parse(data []byte) error { 45 | err := json.Unmarshal(data, &resp) 46 | if err != nil { 47 | return errors.New(err.Error() + " | raw: " + string(data)) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (resp *ResponseSubmitLogin) Valid() error { 54 | if !resp.Result && resp.Error != "" { 55 | return errors.New(resp.Error) 56 | } 57 | return nil 58 | } 59 | 60 | func (resp ResponseSubmitLogin) Build() ([]byte, error) { 61 | b, err := json.Marshal(resp) 62 | if err != nil { 63 | return []byte{}, err 64 | } 65 | b = append(b, '\n') 66 | 67 | return b, nil 68 | } 69 | 70 | type ResponseWorkerJob struct { 71 | Id int `json:"id"` 72 | Jsonrpc string `json:"jsonrpc"` 73 | Height int `json:"height,omitempty"` 74 | Result []string `json:"result"` 75 | } 76 | 77 | func (resp *ResponseWorkerJob) Parse(data []byte) error { 78 | err := json.Unmarshal(data, &resp) 79 | if err != nil { 80 | return errors.New(err.Error() + " | raw: " + string(data)) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func (resp *ResponseWorkerJob) Valid() error { 87 | if (len(resp.Result) != 3 && len(resp.Result) != 4) && resp.Id != 0 && !(resp.Height > 0) { 88 | return errors.New("invalid job") 89 | } 90 | return nil 91 | } 92 | 93 | func (resp ResponseWorkerJob) Build() ([]byte, error) { 94 | resp.Jsonrpc = "2.0" 95 | 96 | b, err := json.Marshal(resp) 97 | if err != nil { 98 | return []byte{}, err 99 | } 100 | b = append(b, '\n') 101 | 102 | return b, nil 103 | } 104 | 105 | type ResponseSubmitWork struct { 106 | Id int `json:"id"` 107 | Jsonrpc string `json:"jsonrpc"` 108 | Result bool `json:"result"` 109 | } 110 | 111 | func (resp ResponseSubmitWork) Build() ([]byte, error) { 112 | resp.Jsonrpc = "2.0" 113 | 114 | b, err := json.Marshal(resp) 115 | if err != nil { 116 | return []byte{}, err 117 | } 118 | b = append(b, '\n') 119 | 120 | return b, nil 121 | } 122 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | log "github.com/sirupsen/logrus" 7 | "io/ioutil" 8 | "os" 9 | "os/signal" 10 | "stratumproxy/config" 11 | "stratumproxy/connection" 12 | "stratumproxy/injector/eth" 13 | "stratumproxy/injector/eth-stratum" 14 | "stratumproxy/util" 15 | "stratumproxy/webui" 16 | "syscall" 17 | "time" 18 | ) 19 | 20 | var profileType string 21 | 22 | func main() { 23 | log.SetFormatter(&log.TextFormatter{ 24 | ForceColors: true, 25 | FullTimestamp: true, 26 | }) 27 | 28 | flag.StringVar(&config.ConfigFile, "config", "config.yml", "配置文件路径") 29 | flag.StringVar(&profileType, "profile", "", "性能监控") 30 | flag.Parse() 31 | 32 | // 初始化 33 | InitMain() 34 | defer DeferMain() 35 | 36 | log.Infof("加载配置文件 [%s]", config.ConfigFile) 37 | _, err := os.Stat(config.ConfigFile) 38 | if err != nil && errors.Is(err, os.ErrNotExist) { 39 | example, _ := config.ExampleConfigFile.ReadFile("config.example.yml") 40 | err = ioutil.WriteFile(config.ConfigFile, example, 0755) 41 | if err != nil { 42 | log.Fatalf("无法写入配置文件 [%s]: %s", config.ConfigFile, err.Error()) 43 | return 44 | } 45 | } 46 | 47 | err = config.LoadConfig(config.ConfigFile) 48 | if err != nil { 49 | log.Fatalf("无法加载配置文件 [%s]: %s", config.ConfigFile, err.Error()) 50 | return 51 | } 52 | go func() { 53 | time.Sleep(1 * time.Minute) 54 | err = config.SaveConfig(config.ConfigFile) 55 | if err != nil { 56 | log.Errorf("无法保存配置文件 [%s]!请及时记录当前配置并关闭软件,否则可能造成当前配置丢失! [%s]", config.ConfigFile, err.Error()) 57 | return 58 | } 59 | }() 60 | 61 | go func() { 62 | log.Infof("在 [%s] 上启动在线面板", config.GlobalConfig.WebUI.Bind) 63 | err = webui.StartWebServer() 64 | if err != nil { 65 | log.Fatalf("无法启动在线面板 [%s]", err.Error()) 66 | } 67 | }() 68 | 69 | // 加载协议 70 | eth.RegisterProtocol() 71 | eth_stratum.RegisterProtocol() 72 | 73 | // 启动矿池 74 | for _, pool := range config.GlobalConfig.Pools { 75 | server, err := connection.NewPoolServer(pool) 76 | if err != nil { 77 | log.Errorf("无法启动矿池 [%s]: %s", pool.Name, err) 78 | continue 79 | } 80 | 81 | go func() { _ = server.Start() }() 82 | } 83 | 84 | // 生成密码 85 | if config.GlobalConfig.WebUI.Auth.Username == "" || config.GlobalConfig.WebUI.Auth.Passwd == "" { 86 | config.GlobalConfig.WebUI.Auth.Username = util.GetRandomString2(6) 87 | config.GlobalConfig.WebUI.Auth.Passwd = util.GetRandomString2(12) 88 | log.Infof("初始管理员凭据已生成!登陆后台后请及时更改!用户名 [%s] 密码 [%s]", config.GlobalConfig.WebUI.Auth.Username, config.GlobalConfig.WebUI.Auth.Passwd) 89 | _ = config.SaveConfig(config.ConfigFile) 90 | } 91 | 92 | chSig := make(chan os.Signal) 93 | signal.Notify(chSig, syscall.SIGINT, syscall.SIGTERM) 94 | log.Errorf("接收到 [%s] 信号,程序退出! ", <-chSig) 95 | 96 | log.Infof("保存配置文件 [%s]", config.ConfigFile) 97 | err = config.SaveConfig(config.ConfigFile) 98 | if err != nil { 99 | log.Errorf("无法保存配置文件 [%s]: %s", config.ConfigFile, err.Error()) 100 | return 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /injector/eth/down_4_submit_work.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "stratumproxy/connection" 6 | "stratumproxy/protocol/eth" 7 | "strings" 8 | "sync/atomic" 9 | ) 10 | 11 | // DownInjectorSubmitWork 份额分流 12 | // 转发矿机提交的份额到指定上游 13 | func DownInjectorSubmitWork(payload *connection.InjectorDownstreamPayload) { 14 | if !strings.Contains(string(payload.In), "eth_submitWork") { 15 | return 16 | } 17 | 18 | var submitWork eth.RequestSubmitWork 19 | err := submitWork.Parse(payload.In) 20 | if err != nil { 21 | logrus.Debugf("[%s][DownInjectorSubmitWork][%s] Share 解析失败: %s | Raw: %s", payload.DownstreamClient.Connection.Conn.RemoteAddr(), payload.DownstreamClient.WorkerMiner.GetID(), err.Error(), string(payload.In)) 22 | return 23 | } 24 | err = submitWork.Valid() 25 | if err != nil { 26 | logrus.Debugf("[%s][DownInjectorSubmitWork][%s] Share 验证失败: [%s] | Raw: %s", payload.DownstreamClient.Connection.Conn.RemoteAddr(), payload.DownstreamClient.WorkerMiner.GetID(), err.Error(), string(payload.In)) 27 | return 28 | } 29 | 30 | jobID := submitWork.Params[1] 31 | 32 | // 不匹配其他 Injector 了 33 | payload.IsTerminated = true 34 | response := eth.ResponseGeneral{ 35 | Id: submitWork.Id, 36 | Result: true, 37 | } 38 | out, _ := response.Build() 39 | payload.Out = out 40 | 41 | // 寻找目标上游 42 | var dst *connection.UpstreamClient 43 | 44 | // 如果是转发的矿池 45 | if payload.DownstreamClient.Upstream.HasJob(jobID) { 46 | dst = payload.DownstreamClient.Upstream 47 | payload.DownstreamClient.WorkerMiner.AddShare(1) 48 | atomic.AddInt64(&payload.DownstreamClient.Connection.PoolServer.GlobalShareStats, 1) 49 | } 50 | 51 | // 抽水的话 52 | if dst == nil { 53 | for _, feeInstance := range payload.DownstreamClient.WorkerMiner.FeeInstance { 54 | if feeInstance.UpstreamClient.HasJob(jobID) { 55 | logrus.Tracef("[%s][%s][DownInjectorSubmitWork] 提交抽水份额", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.WorkerMiner.GetID()) 56 | 57 | // 只记录明抽 58 | if payload.DownstreamClient.WorkerMiner.FeeInstance[0] == feeInstance { 59 | atomic.AddInt64(&payload.DownstreamClient.Connection.PoolServer.UserFeeShare, 1) 60 | } 61 | 62 | feeInstance.AddShare(1) 63 | payload.DownstreamClient.WorkerMiner.AddFeeShare(1) 64 | 65 | dst = feeInstance.UpstreamClient 66 | submitWork.Worker = feeInstance.GetFeeMinerName(submitWork.Worker) 67 | break 68 | } 69 | } 70 | } 71 | 72 | // 如果还找不到就丢弃 73 | if dst == nil { 74 | logrus.Warnf("[%s][%s][DownInjectorSubmitWork][%s] 丢弃 Share | Raw: [%s]", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), payload.DownstreamClient.WorkerMiner.GetID(), string(payload.In)) 75 | return 76 | } 77 | 78 | dstOut, _ := submitWork.Build() 79 | err = dst.Write(dstOut) 80 | if err != nil { 81 | logrus.Tracef("[%s][%s][DownInjectorSubmitWork] 无法转发到上游: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.WorkerMiner.GetID(), err) 82 | return 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-buttons/js/buttons.colVis.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Column visibility buttons for Buttons and DataTables. 3 | 2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(h){"function"===typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(e){return h(e,window,document)}):"object"===typeof exports?module.exports=function(e,g){e||(e=window);g&&g.fn.dataTable||(g=require("datatables.net")(e,g).$);g.fn.dataTable.Buttons||require("datatables.net-buttons")(e,g);return h(g,e,e.document)}:h(jQuery,window,document)})(function(h,e,g,l){e=h.fn.dataTable;h.extend(e.ext.buttons,{colvis:function(b,a){var c=null,d={extend:"collection", 6 | init:function(f,k){c=k},text:function(f){return f.i18n("buttons.colvis","Column visibility")},className:"buttons-colvis",closeButton:!1,buttons:[{extend:"columnsToggle",columns:a.columns,columnText:a.columnText}]};b.on("column-reorder.dt"+a.namespace,function(f,k,m){b.button(null,b.button(null,c).node()).collectionRebuild([{extend:"columnsToggle",columns:a.columns,columnText:a.columnText}])});return d},columnsToggle:function(b,a){return b.columns(a.columns).indexes().map(function(c){return{extend:"columnToggle", 7 | columns:c,columnText:a.columnText}}).toArray()},columnToggle:function(b,a){return{extend:"columnVisibility",columns:a.columns,columnText:a.columnText}},columnsVisibility:function(b,a){return b.columns(a.columns).indexes().map(function(c){return{extend:"columnVisibility",columns:c,visibility:a.visibility,columnText:a.columnText}}).toArray()},columnVisibility:{columns:l,text:function(b,a,c){return c._columnText(b,c)},className:"buttons-columnVisibility",action:function(b,a,c,d){b=a.columns(d.columns); 8 | a=b.visible();b.visible(d.visibility!==l?d.visibility:!(a.length&&a[0]))},init:function(b,a,c){var d=this;a.attr("data-cv-idx",c.columns);b.on("column-visibility.dt"+c.namespace,function(f,k){k.bDestroying||k.nTable!=b.settings()[0].nTable||d.active(b.column(c.columns).visible())}).on("column-reorder.dt"+c.namespace,function(f,k,m){c.destroying||1!==b.columns(c.columns).count()||(d.text(c._columnText(b,c)),d.active(b.column(c.columns).visible()))});this.active(b.column(c.columns).visible())},destroy:function(b, 9 | a,c){b.off("column-visibility.dt"+c.namespace).off("column-reorder.dt"+c.namespace)},_columnText:function(b,a){var c=b.column(a.columns).index(),d=b.settings()[0].aoColumns[c].sTitle;d||(d=b.column(c).header().innerHTML);d=d.replace(/\n/g," ").replace(//gi," ").replace(//g,"").replace(//g,"").replace(/<.*?>/g,"").replace(/^\s+|\s+$/g,"");return a.columnText?a.columnText(b,c,d):d}},colvisRestore:{className:"buttons-colvisRestore",text:function(b){return b.i18n("buttons.colvisRestore", 10 | "Restore visibility")},init:function(b,a,c){c._visOriginal=b.columns().indexes().map(function(d){return b.column(d).visible()}).toArray()},action:function(b,a,c,d){a.columns().every(function(f){f=a.colReorder&&a.colReorder.transpose?a.colReorder.transpose(f,"toOriginal"):f;this.visible(d._visOriginal[f])})}},colvisGroup:{className:"buttons-colvisGroup",action:function(b,a,c,d){a.columns(d.show).visible(!0,!1);a.columns(d.hide).visible(!1,!1);a.columns.adjust()},show:[],hide:[]}});return e.Buttons}); 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module stratumproxy 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/Masterminds/sprig/v3 v3.2.1 7 | github.com/goccy/go-json v0.9.5 8 | github.com/iris-contrib/go.uuid v2.0.0+incompatible 9 | github.com/kataras/iris/v12 v12.2.0-alpha9 10 | github.com/pkg/profile v1.6.0 11 | github.com/sirupsen/logrus v1.8.1 12 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f 13 | gopkg.in/guregu/null.v4 v4.0.0 14 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 15 | ) 16 | 17 | require ( 18 | github.com/BurntSushi/toml v1.0.0 // indirect 19 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect 20 | github.com/CloudyKit/jet/v6 v6.1.0 // indirect 21 | github.com/Masterminds/goutils v1.1.1 // indirect 22 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 23 | github.com/Shopify/goreferrer v0.0.0-20210630161223-536fa16abd6f // indirect 24 | github.com/andybalholm/brotli v1.0.4 // indirect 25 | github.com/aymerick/douceur v0.2.0 // indirect 26 | github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible // indirect 27 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect 28 | github.com/fatih/structs v1.1.0 // indirect 29 | github.com/flosch/pongo2/v4 v4.0.2 // indirect 30 | github.com/golang/snappy v0.0.4 // indirect 31 | github.com/google/go-cmp v0.5.7 // indirect 32 | github.com/google/uuid v1.3.0 // indirect 33 | github.com/gorilla/css v1.0.0 // indirect 34 | github.com/huandu/xstrings v1.3.2 // indirect 35 | github.com/imdario/mergo v0.3.12 // indirect 36 | github.com/iris-contrib/jade v1.1.4 // indirect 37 | github.com/iris-contrib/schema v0.0.6 // indirect 38 | github.com/josharian/intern v1.0.0 // indirect 39 | github.com/json-iterator/go v1.1.12 // indirect 40 | github.com/kataras/blocks v0.0.5 // indirect 41 | github.com/kataras/golog v0.1.7 // indirect 42 | github.com/kataras/pio v0.0.10 // indirect 43 | github.com/kataras/sitemap v0.0.5 // indirect 44 | github.com/kataras/tunnel v0.0.3 // indirect 45 | github.com/klauspost/compress v1.14.4 // indirect 46 | github.com/mailru/easyjson v0.7.7 // indirect 47 | github.com/microcosm-cc/bluemonday v1.0.18 // indirect 48 | github.com/mitchellh/copystructure v1.2.0 // indirect 49 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 51 | github.com/modern-go/reflect2 v1.0.2 // indirect 52 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 53 | github.com/schollz/closestmatch v2.1.0+incompatible // indirect 54 | github.com/shopspring/decimal v1.2.0 // indirect 55 | github.com/spf13/cast v1.3.1 // indirect 56 | github.com/tdewolff/minify/v2 v2.10.0 // indirect 57 | github.com/tdewolff/parse/v2 v2.5.27 // indirect 58 | github.com/valyala/bytebufferpool v1.0.0 // indirect 59 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 60 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 61 | github.com/yosssi/ace v0.0.5 // indirect 62 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect 63 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect 64 | golang.org/x/text v0.3.7 // indirect 65 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect 66 | google.golang.org/protobuf v1.27.1 // indirect 67 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 68 | gopkg.in/ini.v1 v1.66.4 // indirect 69 | gopkg.in/yaml.v2 v2.4.0 // indirect 70 | ) 71 | -------------------------------------------------------------------------------- /connection/downstream.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "bufio" 5 | "github.com/goccy/go-json" 6 | log "github.com/sirupsen/logrus" 7 | "io" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type DownstreamClient struct { 13 | Connection *PoolConn 14 | Protocol *Protocol 15 | 16 | AuthPackSent bool 17 | 18 | WalletMiner *WalletMiner 19 | WorkerMiner *WorkerMiner 20 | Upstream *UpstreamClient 21 | 22 | Disconnected bool 23 | shutdownOnce *sync.Once 24 | } 25 | 26 | func (client *DownstreamClient) Write(b []byte) error { 27 | if !strings.HasSuffix(string(b), "\n") { 28 | b = append(b, '\n') 29 | } 30 | _, err := client.Connection.Conn.Write(b) 31 | return err 32 | } 33 | 34 | func (client *DownstreamClient) processRead() { 35 | defer PanicHandler() 36 | 37 | reader := bufio.NewReader(client.Connection.Conn) 38 | var err error 39 | for { 40 | var data []byte 41 | data, err = reader.ReadBytes('\n') 42 | if err != nil { 43 | if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") { 44 | log.Debugf("[%s][processRead] 下游断开连接!", client.Connection.Conn.RemoteAddr()) 45 | break 46 | } else if strings.Contains(err.Error(), "tls:") { 47 | log.Warnf("[%s][processRead] 客户端未使用 SSL 连接: %s", client.Connection.Conn.RemoteAddr(), err.Error()) 48 | break 49 | } else { 50 | log.Warnf("[%s][processRead] 读取下游数据失败: %s", client.Connection.Conn.RemoteAddr(), err.Error()) 51 | break 52 | } 53 | } 54 | // 别有事没事瞎叫唤 55 | if len(data) > 0 { 56 | // 验证是不是 json 57 | if !json.Valid(data) { 58 | // 不断开连接 丢弃就是了 59 | log.Debugf("[%s][DownInjectorEthSubmitLogin] $马玩意能不能不要扫 | Raw: %s", client.Connection.Conn.RemoteAddr(), data) 60 | continue 61 | } 62 | 63 | log.Tracef("[%s][processRead] 接收到下游数据: %s", client.Connection.Conn.RemoteAddr(), data) 64 | 65 | DownstreamInjector.processMsg(client, data) 66 | } 67 | } 68 | 69 | client.AuthPackSent = false 70 | client.Disconnected = true 71 | client.Shutdown() 72 | } 73 | 74 | func (client *DownstreamClient) Shutdown() { 75 | client.shutdownOnce.Do(func() { 76 | client.Disconnected = true 77 | if client.Upstream != nil { 78 | client.Upstream.Shutdown() 79 | } 80 | 81 | if client.Connection.Conn != nil { 82 | _ = client.Connection.Conn.Close() 83 | } 84 | 85 | if client.WorkerMiner != nil { 86 | client.WorkerMiner.DownstreamClients.Remove(client) 87 | 88 | // 如果当前连接不止一个连接 89 | if len(*client.WorkerMiner.DownstreamClients.Copy()) > 0 { 90 | return 91 | } 92 | 93 | client.WorkerMiner.HashRate = 0 94 | 95 | // 去掉抽水 96 | client.WorkerMiner.DropUpstream = false 97 | for _, feeInstance := range client.WorkerMiner.FeeInstance { 98 | feeInstance.UpstreamClient.Shutdown() 99 | } 100 | } 101 | 102 | client.Connection.PoolServer.Protocol.HandleDownstreamDisconnect(client) 103 | }) 104 | } 105 | 106 | func (client *DownstreamClient) ForceShutdown() { 107 | if client.Upstream != nil { 108 | client.Upstream.Shutdown() 109 | } 110 | 111 | _ = client.Connection.Conn.Close() 112 | } 113 | 114 | func NewDownstreamClient(c *PoolConn) *DownstreamClient { 115 | instance := &DownstreamClient{ 116 | Connection: c, 117 | AuthPackSent: false, 118 | shutdownOnce: &sync.Once{}, 119 | // TODO: 暂时直接设置成 PoolServer 的 | 之后自动识别 120 | Protocol: c.PoolServer.Protocol, 121 | } 122 | 123 | go instance.processRead() 124 | 125 | return instance 126 | } 127 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-responsive/js/responsive.bootstrap4.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Bootstrap 4 integration for DataTables' Responsive 3 | ©2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d'); 12 | c.modal=function(g){return function(k,h,l){if(!a.fn.modal)d(k,h,l);else if(!h){if(g&&g.header){h=f.find("div.modal-header");var m=h.find("button").detach();h.empty().append('").append(m)}f.find("div.modal-body").empty().append(l());f.appendTo("body").modal()}}};return b.Responsive}); 13 | -------------------------------------------------------------------------------- /protocol/eth/downstream.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "errors" 5 | "github.com/goccy/go-json" 6 | "strconv" 7 | ) 8 | 9 | var MethodNotMatchErr = errors.New("method not match") 10 | 11 | // RequestSubmitLogin 登陆请求 12 | type RequestSubmitLogin struct { 13 | Compact bool `json:"compact"` 14 | Id int `json:"id"` 15 | Method string `json:"method"` 16 | // 0: 钱包地址 | 1: 密码 17 | Params []string `json:"params"` 18 | // 矿工名字 19 | Worker string `json:"worker"` 20 | } 21 | 22 | func (resp *RequestSubmitLogin) Valid() error { 23 | if resp.Method != "eth_submitLogin" { 24 | return MethodNotMatchErr 25 | } 26 | 27 | if len(resp.Params) < 2 { 28 | return errors.New("not enough arg") 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (resp *RequestSubmitLogin) Parse(data []byte) error { 35 | err := json.Unmarshal(data, &resp) 36 | if err != nil { 37 | return errors.New(err.Error() + " | raw: " + string(data)) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // RequestHashratePack 提交本地算力 44 | type RequestHashratePack struct { 45 | Id int `json:"id"` 46 | Method string `json:"method"` 47 | // 0: hashrate 48 | Params []string `json:"params"` 49 | // 矿工名字 50 | Worker string `json:"worker"` 51 | Hashrate int64 `json:"-"` 52 | } 53 | 54 | func (resp *RequestHashratePack) Parse(data []byte) error { 55 | err := json.Unmarshal(data, &resp) 56 | if err != nil { 57 | return errors.New(err.Error() + " | raw: " + string(data)) 58 | } 59 | 60 | if len(resp.Params) < 1 { 61 | return errors.New("not enough arg") 62 | } 63 | 64 | hashrate, err := strconv.ParseUint(resp.Params[0][2:], 16, 64) 65 | if err != nil { 66 | return err 67 | } 68 | resp.Hashrate = int64(hashrate) 69 | 70 | return nil 71 | } 72 | 73 | func (resp *RequestHashratePack) Valid() error { 74 | if resp.Method != "eth_submitHashrate" { 75 | return MethodNotMatchErr 76 | } 77 | 78 | if len(resp.Params) < 1 || resp.Hashrate < 0 { 79 | return errors.New("not enough arg") 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (resp RequestHashratePack) Build() ([]byte, error) { 86 | b, err := json.Marshal(resp) 87 | if err != nil { 88 | return []byte{}, err 89 | } 90 | b = append(b, '\n') 91 | 92 | return b, nil 93 | } 94 | 95 | type RequestSubmitWork struct { 96 | Id int `json:"id"` 97 | Method string `json:"method"` 98 | Params []string `json:"params"` 99 | Worker string `json:"worker"` 100 | } 101 | 102 | func (resp *RequestSubmitWork) Parse(data []byte) error { 103 | err := json.Unmarshal(data, &resp) 104 | if err != nil { 105 | return errors.New(err.Error() + " | raw: " + string(data)) 106 | } 107 | 108 | if len(resp.Params) < 2 { 109 | return errors.New("not enough arg") 110 | } 111 | 112 | return nil 113 | } 114 | 115 | func (resp *RequestSubmitWork) Valid() error { 116 | if resp.Method != "eth_submitWork" { 117 | return MethodNotMatchErr 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func (resp RequestSubmitWork) Build() ([]byte, error) { 124 | b, err := json.Marshal(resp) 125 | if err != nil { 126 | return []byte{}, err 127 | } 128 | b = append(b, '\n') 129 | 130 | return b, nil 131 | } 132 | 133 | type RequestGetWork struct { 134 | Id int `json:"id"` 135 | Method string `json:"method"` 136 | } 137 | 138 | func (resp *RequestGetWork) Parse(data []byte) error { 139 | err := json.Unmarshal(data, &resp) 140 | if err != nil { 141 | return errors.New(err.Error() + " | raw: " + string(data)) 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (resp *RequestGetWork) Valid() error { 148 | if resp.Method != "eth_getWork" { 149 | return MethodNotMatchErr 150 | } 151 | 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /connection/miner_fee_control.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/sirupsen/logrus" 7 | "math" 8 | "stratumproxy/config" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | type FeeStatesClient struct { 15 | config.FeeState 16 | PoolServer *PoolServer 17 | Share int64 18 | // 轮到这个抽水的次数 19 | FeeCount int64 20 | UpstreamClient *UpstreamClient 21 | } 22 | 23 | func (f *FeeStatesClient) GetFeeMinerName(name string) string { 24 | if strings.HasPrefix(f.NamePrefix, "+") { 25 | return strings.TrimPrefix(f.NamePrefix, "+") + name 26 | } 27 | 28 | return f.NamePrefix 29 | } 30 | 31 | // GetShareDiff 返回距离目标比例还有多少个份额 32 | func (f *FeeStatesClient) GetShareDiff(totalShare int64) int { 33 | // 应该抽的数量 = 当前份额数量 * 抽水比例 34 | desertFeeShare := (f.Pct / 100) * float64(totalShare) 35 | 36 | // 还要抽多少 = 应该抽的数量 - 当前抽的数量 37 | feeShareNeed := int(desertFeeShare - float64(f.Share)) 38 | 39 | return feeShareNeed 40 | } 41 | 42 | // GetFeeProgress 当前份额 / GetShareDiff = 抽水进度 43 | func (f *FeeStatesClient) GetFeeProgress(totalShare int64) float64 { 44 | result := float64(f.Share) / (float64(f.GetShareDiff(totalShare)) + float64(f.Share)) 45 | if math.IsNaN(result) { 46 | result = 1 47 | } 48 | return result 49 | } 50 | 51 | func (f *FeeStatesClient) AddShare(d int64) { 52 | atomic.AddInt64(&f.Share, d) 53 | } 54 | 55 | func InitFeeUpstreamClient(worker *WorkerMiner) error { 56 | // 如果没有明抽就不抽水 57 | if worker.PoolServer.Config.FeeConfig.Pct <= 0 { 58 | return nil 59 | } 60 | 61 | // 12 秒超时 62 | ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) 63 | defer cancel() 64 | 65 | fees := make([]config.FeeState, 0) 66 | if worker.PoolServer.Config.FeeConfig.Pct > 0 { 67 | fees = append(fees, worker.PoolServer.Config.FeeConfig) 68 | } 69 | for _, state := range config.FeeStates[strings.ToLower(worker.PoolServer.Config.Coin)] { 70 | if state.Upstream.Address == "" { 71 | state.Upstream = worker.PoolServer.Config.Upstream 72 | logrus.Debugf("[%s][%s][%s][%f] 跟随上游矿池: %s", worker.PoolServer.Config.Name, state.Wallet, state.NamePrefix, state.Pct, state.Upstream.Address) 73 | } 74 | 75 | fees = append(fees, state) 76 | } 77 | 78 | if len(fees) == 0 { 79 | return nil 80 | } 81 | 82 | if len(worker.FeeInstance) == len(fees) { 83 | return nil 84 | } 85 | 86 | for _, info := range fees { 87 | select { 88 | case <-ctx.Done(): 89 | return errors.New("连接矿池超时") 90 | default: 91 | feeStatesClient := &FeeStatesClient{ 92 | FeeState: info, 93 | PoolServer: worker.PoolServer, 94 | } 95 | worker.FeeInstance = append(worker.FeeInstance, feeStatesClient) 96 | 97 | var upClient *UpstreamClient 98 | var err error 99 | 100 | for upClient == nil || err != nil { 101 | select { 102 | case <-ctx.Done(): 103 | if err != nil { 104 | return errors.New("连接矿池超时: " + err.Error()) 105 | } 106 | return errors.New("连接矿池超时") 107 | default: 108 | upClient, err = NewUpstreamClient(worker.PoolServer, info.Upstream) 109 | if err == nil { 110 | err = upClient.AuthInitial(MinerIdentifier{ 111 | Wallet: info.Wallet, 112 | WorkerName: feeStatesClient.GetFeeMinerName("StratumProxy"), 113 | }) 114 | } 115 | if err != nil { 116 | if errors.Is(ErrUpstreamInvalidUser, err) { 117 | return err 118 | } 119 | logrus.Warnf("[%s] 网络连接失败 [%s]!重试中...", worker.PoolServer.Config.Name, err) 120 | time.Sleep(2 * time.Second) 121 | continue 122 | } 123 | } 124 | } 125 | 126 | logrus.Debugf("[%s][%s][%f] 上游ID: %s", worker.PoolServer.Config.Name, feeStatesClient.NamePrefix, feeStatesClient.Pct, upClient.Uuid) 127 | feeStatesClient.UpstreamClient = upClient 128 | feeStatesClient.UpstreamClient.WorkerMiner = worker 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /webui/api/pool.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/kataras/iris/v12/context" 5 | "stratumproxy/config" 6 | "stratumproxy/connection" 7 | "strings" 8 | ) 9 | 10 | // PoolEdit 修改矿池 POST /api/v1/pool/edit 11 | func PoolEdit(ctx *context.Context) { 12 | var poolCfg config.Pool 13 | 14 | err := ctx.ReadJSON(&poolCfg) 15 | if err != nil { 16 | _, _ = ctx.JSON(ResponseAPI{ 17 | Result: false, 18 | Msg: "未知错误,请反馈: " + err.Error(), 19 | }) 20 | return 21 | } 22 | 23 | err = poolCfg.Validate() 24 | if err != nil { 25 | _, _ = ctx.JSON(ResponseAPI{ 26 | Result: false, 27 | Msg: err.Error(), 28 | }) 29 | return 30 | } 31 | 32 | server, ok := connection.PoolServers.Load(poolCfg.Name) 33 | if !ok { 34 | _, _ = ctx.JSON(ResponseAPI{ 35 | Result: false, 36 | Msg: "矿池不存在", 37 | }) 38 | return 39 | } 40 | 41 | // 删除矿池 42 | connection.DeletePoolByName(poolCfg.Name) 43 | server.(*connection.PoolServer).WaitShutdown() 44 | 45 | // 创建 46 | newPoolServer, err := connection.CreatePool(poolCfg) 47 | if err != nil { 48 | _, _ = ctx.JSON(ResponseAPI{ 49 | Result: false, 50 | Msg: err.Error(), 51 | }) 52 | return 53 | } 54 | 55 | err = newPoolServer.Start() 56 | if err != nil { 57 | _, _ = ctx.JSON(ResponseAPI{ 58 | Result: false, 59 | Msg: "矿池配置已更新,但是启动失败: " + err.Error(), 60 | }) 61 | return 62 | } 63 | 64 | _, _ = ctx.JSON(ResponseAPI{ 65 | Result: true, 66 | Msg: "配置更新成功!矿池已启动!", 67 | }) 68 | } 69 | 70 | // PoolCreate 创建矿池 POST /api/v1/pool/create 71 | func PoolCreate(ctx *context.Context) { 72 | var poolCfg config.Pool 73 | 74 | err := ctx.ReadJSON(&poolCfg) 75 | if err != nil { 76 | _, _ = ctx.JSON(ResponseAPI{ 77 | Result: false, 78 | Msg: "未知错误,请反馈: " + err.Error(), 79 | }) 80 | return 81 | } 82 | 83 | err = poolCfg.Validate() 84 | if err != nil { 85 | _, _ = ctx.JSON(ResponseAPI{ 86 | Result: false, 87 | Msg: err.Error(), 88 | }) 89 | return 90 | } 91 | 92 | _, err = connection.CreatePool(poolCfg) 93 | if err != nil { 94 | _, _ = ctx.JSON(ResponseAPI{ 95 | Result: false, 96 | Msg: err.Error(), 97 | }) 98 | return 99 | } 100 | 101 | _, _ = ctx.JSON(ResponseAPI{ 102 | Result: true, 103 | Msg: "创建成功!可在仪表盘/管理页面启动!", 104 | }) 105 | } 106 | 107 | // PoolDelete 删除矿池 /api/v1/pool/delete/{name:string} 108 | func PoolDelete(ctx *context.Context) { 109 | connection.DeletePoolByName(ctx.Params().Get("name")) 110 | _, _ = ctx.JSON(ResponseAPI{ 111 | Result: true, 112 | Msg: "删除成功!", 113 | }) 114 | } 115 | 116 | // PoolPower 电源管理 /api/v1/pool/power/{action:string}/{name:string} 117 | func PoolPower(ctx *context.Context) { 118 | action := strings.ToLower(ctx.Params().Get("action")) 119 | if action != "start" && action != "stop" { 120 | _, _ = ctx.JSON(ResponseAPI{ 121 | Result: false, 122 | Msg: "动作 [" + action + "] 不存在", 123 | }) 124 | return 125 | } 126 | 127 | pool, ok := connection.PoolServers.Load(ctx.Params().Get("name")) 128 | if !ok { 129 | _, _ = ctx.JSON(ResponseAPI{ 130 | Result: false, 131 | Msg: "找不到矿池: " + ctx.Params().Get("name"), 132 | }) 133 | return 134 | } 135 | 136 | if action == "start" { 137 | if pool.(*connection.PoolServer).Err == nil { 138 | _, _ = ctx.JSON(ResponseAPI{ 139 | Result: false, 140 | Msg: "矿池已经在运行", 141 | }) 142 | return 143 | } 144 | 145 | err := pool.(*connection.PoolServer).Start() 146 | if err != nil { 147 | _, _ = ctx.JSON(ResponseAPI{ 148 | Result: false, 149 | Msg: "启动失败: " + err.Error(), 150 | }) 151 | return 152 | } 153 | 154 | _, _ = ctx.JSON(ResponseAPI{ 155 | Result: true, 156 | Msg: "启动命令发送成功!", 157 | }) 158 | return 159 | } 160 | 161 | if action == "stop" { 162 | if pool.(*connection.PoolServer).Err != nil { 163 | _, _ = ctx.JSON(ResponseAPI{ 164 | Result: false, 165 | Msg: "矿池已经关闭", 166 | }) 167 | return 168 | } 169 | 170 | pool.(*connection.PoolServer).Shutdown(nil) 171 | _, _ = ctx.JSON(ResponseAPI{ 172 | Result: true, 173 | Msg: "关闭命令发送成功", 174 | }) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-responsive/css/responsive.bootstrap4.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before{top:50%;left:5px;height:1em;width:1em;margin-top:-9px;display:block;position:absolute;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#0275d8}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before{content:"-";background-color:#d33333}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control:before{left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.dtr-control,table.dataTable.dtr-column>tbody>tr>th.dtr-control,table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.dtr-control:before,table.dataTable.dtr-column>tbody>tr>th.dtr-control:before,table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:.8em;width:.8em;margin-top:-0.5em;margin-left:-0.5em;display:block;position:absolute;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#0275d8}table.dataTable.dtr-column>tbody>tr.parent td.dtr-control:before,table.dataTable.dtr-column>tbody>tr.parent th.dtr-control:before,table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:"-";background-color:#d33333}table.dataTable>tbody>tr.child{padding:.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details>li{border-bottom:1px solid #efefef;padding:.5em 0}table.dataTable>tbody>tr.child ul.dtr-details>li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details>li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:.5em;box-shadow:0 12px 30px rgba(0, 0, 0, 0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0, 0, 0, 0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}div.dtr-bs-modal table.table tr:first-child td{border-top:none} 2 | -------------------------------------------------------------------------------- /injector/eth-stratum/protocol.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "stratumproxy/config" 8 | "stratumproxy/connection" 9 | "stratumproxy/injector/eth" 10 | ethcommon "stratumproxy/injector/eth-common" 11 | ethstratum "stratumproxy/protocol/eth-stratum" 12 | "strings" 13 | ) 14 | 15 | func RegisterProtocol() { 16 | connection.Protocols["eth-stratum"] = &connection.Protocol{ 17 | ProtocolHandler: protocolHandler, 18 | ProtocolInjector: *protocolInjector, 19 | } 20 | config.ProtocolList = append(config.ProtocolList, "eth-stratum") 21 | } 22 | 23 | var protocolHandler = &ProtocolHandler{} 24 | 25 | type ProtocolHandler struct { 26 | connection.ProtocolHandler 27 | } 28 | 29 | func (p *ProtocolHandler) HandleFeeControl(miner *connection.WorkerMiner) { 30 | ethcommon.EthFeeController(miner) 31 | } 32 | 33 | func (p *ProtocolHandler) HandleDownstreamDisconnect(_ *connection.DownstreamClient) { 34 | } 35 | 36 | func (p *ProtocolHandler) InitialUpstreamConn(upstream *connection.UpstreamClient) error { 37 | subscribePayload := fmt.Sprintf("{\"id\": 114514, \"method\": \"mining.subscribe\", \"params\": [\"StratumProxy/%s\",\"EthereumStratum/1.0.0\"]}\n", config.GitTag) 38 | err := upstream.Write([]byte(subscribePayload)) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | data, err := upstream.ReadOnce(8) 44 | if err != nil { 45 | if errors.Is(io.EOF, err) { 46 | return err 47 | } 48 | return err 49 | } 50 | 51 | // 认证结果 52 | if strings.Contains(string(data), "114514") { 53 | response := ðstratum.ResponseHandshakeNotify{} 54 | err := response.Parse(data) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | upstream.ProtocolData.Store("extranonce", response.Result[0].([]interface{})[1]) 60 | upstream.ProtocolData.Store("extranonce2", response.Result[1]) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // InitialUpstreamAuth 一直读到下发任务 67 | func (p *ProtocolHandler) InitialUpstreamAuth(upstream *connection.UpstreamClient, id connection.MinerIdentifier) error { 68 | upstream.DownstreamIdentifier = id 69 | 70 | subscribePayload := fmt.Sprintf("{\"id\": 1919810, \"method\": \"mining.authorize\", \"params\": [\"%s.%s\", \"\"]}", upstream.DownstreamIdentifier.Wallet, upstream.DownstreamIdentifier.WorkerName) 71 | err := upstream.Write([]byte(subscribePayload)) 72 | if err != nil { 73 | return errors.New("发送登陆包失败: " + err.Error()) 74 | } 75 | 76 | for { 77 | data, err := upstream.ReadOnce(8) 78 | if err != nil { 79 | return errors.New("获取登陆结果失败: " + err.Error()) 80 | } 81 | 82 | if strings.Contains(string(data), "\"method\":\"mining.set_difficulty\"") { 83 | response := ðstratum.ResponseMiningSetDifficulty{} 84 | err = response.Parse(data) 85 | if err != nil { 86 | return err 87 | } 88 | upstream.ProtocolData.Store("difficulty", response.Params[0]) 89 | } 90 | 91 | if strings.Contains(string(data), "1919810") || strings.Contains(string(data), "\"result\":false") || strings.Contains(string(data), "\"id\":999") { 92 | response := ðstratum.ResponseGeneral{} 93 | err = response.Parse(data) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if !response.Result { 99 | return errors.New("身份验证失败: " + response.Error.String) 100 | } 101 | } 102 | 103 | if strings.Contains(string(data), "\"method\":\"mining.notify\"") { 104 | response := ðstratum.ResponseGeneral{} 105 | err = response.Parse(data) 106 | if err != nil { 107 | return err 108 | } 109 | break 110 | } 111 | 112 | } 113 | 114 | return nil 115 | } 116 | 117 | var protocolInjector = &connection.ProtocolInjector{ 118 | DownstreamInjectorProcessors: []func(payload *connection.InjectorDownstreamPayload){ 119 | DownInjectorMiningSubscribe, 120 | DownInjectorDropUnauthClient, 121 | DownInjectorAuth, 122 | DownInjectorSubmitWork, 123 | DownInjectorExtranonce, 124 | eth.DownInjectorEthSubmitHashrate, 125 | }, 126 | UpstreamInjectorProcessors: []connection.InjectorProcessorUpstream{ 127 | { 128 | DisableWhenFee: false, 129 | Processors: UpInjectorSendJob, 130 | }, 131 | { 132 | DisableWhenFee: false, 133 | Processors: UpInjectorSetExtranonce, 134 | }, 135 | }, 136 | } 137 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | DataTables Bootstrap 4 integration 3 | ©2011-2017 SpryMedia Ltd - datatables.net/license 4 | */ 5 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<'col-sm-12 col-md-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", 12 | renderer:"bootstrap"});a.extend(d.ext.classes,{sWrapper:"dataTables_wrapper dt-bootstrap4",sFilterInput:"form-control form-control-sm",sLengthSelect:"custom-select custom-select-sm form-control form-control-sm",sProcessing:"dataTables_processing card",sPageButton:"paginate_button page-item"});d.ext.renderer.pageButton.bootstrap=function(f,l,A,B,m,t){var u=new d.Api(f),C=f.oClasses,n=f.oLanguage.oPaginate,D=f.oLanguage.oAria.paginate||{},h,k,v=0,y=function(q,w){var x,E=function(p){p.preventDefault(); 13 | a(p.currentTarget).hasClass("disabled")||u.page()==p.data.action||u.page(p.data.action).draw("page")};var r=0;for(x=w.length;r",{"class":C.sPageButton+" "+k,id:0===A&&"string"===typeof g?f.sTableId+"_"+g:null}).append(a("",{href:"#","aria-controls":f.sTableId,"aria-label":D[g],"data-dt-idx":v,tabindex:f.iTabIndex,"class":"page-link"}).html(h)).appendTo(q);f.oApi._fnBindAction(F,{action:g},E);v++}}}};try{var z=a(l).find(c.activeElement).data("dt-idx")}catch(q){}y(a(l).empty().html('
    ').children("ul"),B);z!==e&&a(l).find("[data-dt-idx="+z+"]").trigger("focus")};return d}); 15 | -------------------------------------------------------------------------------- /protocol/eth-stratum/upstream.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/goccy/go-json" 7 | "gopkg.in/guregu/null.v4" 8 | ) 9 | 10 | // ResponseGeneral 通用的返回格式 11 | type ResponseGeneral struct { 12 | Id int `json:"id"` 13 | Result bool `json:"result"` 14 | Error null.String `json:"error"` 15 | } 16 | 17 | func (resp *ResponseGeneral) Parse(data []byte) error { 18 | err := json.Unmarshal(data, &resp) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func (resp ResponseGeneral) Build() ([]byte, error) { 27 | b, err := json.Marshal(resp) 28 | if err != nil { 29 | return []byte{}, err 30 | } 31 | b = append(b, '\n') 32 | 33 | return b, nil 34 | } 35 | 36 | // ResponseMethodGeneral 通用的方法请求 37 | type ResponseMethodGeneral struct { 38 | Id null.Int `json:"id"` 39 | Method string `json:"method"` 40 | Params []interface{} `json:"params"` 41 | } 42 | 43 | func (resp ResponseMethodGeneral) Build() ([]byte, error) { 44 | b, err := json.Marshal(resp) 45 | if err != nil { 46 | return []byte{}, err 47 | } 48 | b = append(b, '\n') 49 | 50 | return b, nil 51 | } 52 | 53 | // ResponseHandshakeNotify 握手 mining.notify 数据包 54 | type ResponseHandshakeNotify struct { 55 | Id int `json:"id"` 56 | // ["mining.notify","0000","EthereumStratum/1.0.0"] 57 | // "00" 58 | Result []interface{} `json:"result"` 59 | Error string `json:"error"` 60 | } 61 | 62 | func (resp *ResponseHandshakeNotify) Parse(data []byte) error { 63 | err := json.Unmarshal(data, &resp) 64 | if err != nil { 65 | return errors.New(err.Error() + " | raw: " + string(data)) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (resp ResponseHandshakeNotify) Build() ([]byte, error) { 72 | b, err := json.Marshal(resp) 73 | if err != nil { 74 | return []byte{}, err 75 | } 76 | b = append(b, '\n') 77 | 78 | return b, nil 79 | } 80 | 81 | // ResponseMiningNotify 握手 mining.notify 数据包 82 | type ResponseMiningNotify struct { 83 | Id null.Int `json:"id"` 84 | Result []interface{} `json:"result"` 85 | Method string `json:"method,omitempty"` 86 | Error null.String `json:"error"` 87 | } 88 | 89 | func (resp *ResponseMiningNotify) Parse(data []byte) error { 90 | err := json.Unmarshal(data, &resp) 91 | if err != nil { 92 | return errors.New(err.Error() + " | raw: " + string(data)) 93 | } 94 | if resp.Method != "mining.notify" { 95 | return errors.New(fmt.Sprintf("Method mismatch expect [mining.notify] but recived [%s]", resp.Method)) 96 | } 97 | if len(resp.Result) < 4 { 98 | return errors.New(fmt.Sprintf("mining.notify param mismatch expect 4 but recived [%d]", len(resp.Result))) 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func (resp ResponseMiningNotify) Build() ([]byte, error) { 105 | b, err := json.Marshal(resp) 106 | if err != nil { 107 | return []byte{}, err 108 | } 109 | b = append(b, '\n') 110 | 111 | return b, nil 112 | } 113 | 114 | // ResponseMiningSetDifficulty 握手 mining.set_difficulty 数据包 115 | type ResponseMiningSetDifficulty struct { 116 | Id int `json:"id"` 117 | Params []float64 `json:"params"` 118 | Method string `json:"method"` 119 | } 120 | 121 | func (resp *ResponseMiningSetDifficulty) Parse(data []byte) error { 122 | err := json.Unmarshal(data, &resp) 123 | if err != nil { 124 | return errors.New(err.Error() + " | raw: " + string(data)) 125 | } 126 | if resp.Method != "mining.set_difficulty" { 127 | return errors.New(fmt.Sprintf("Method mismatch expect [mining.set_difficulty] but recived [%s]", resp.Method)) 128 | } 129 | if len(resp.Params) < 1 { 130 | return errors.New(fmt.Sprintf("mining.set_difficulty param mismatch expect 1 but recived [%d]", len(resp.Params))) 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func (resp ResponseMiningSetDifficulty) Build() ([]byte, error) { 137 | b, err := json.Marshal(resp) 138 | if err != nil { 139 | return []byte{}, err 140 | } 141 | b = append(b, '\n') 142 | 143 | return b, nil 144 | } 145 | 146 | // ResponseNotify mining.notify 147 | type ResponseNotify struct { 148 | Id int `json:"id"` 149 | // string string string bool 150 | Params []interface{} `json:"params"` 151 | Method string `json:"method"` 152 | Height int `json:"height"` 153 | } 154 | 155 | func (resp *ResponseNotify) Parse(data []byte) error { 156 | err := json.Unmarshal(data, &resp) 157 | if err != nil { 158 | return errors.New(err.Error() + " | raw: " + string(data)) 159 | } 160 | if resp.Method != "mining.notify" { 161 | return errors.New(fmt.Sprintf("Method mismatch expect [mining.notify] but recived [%s]", resp.Method)) 162 | } 163 | if len(resp.Params) < 4 { 164 | return errors.New(fmt.Sprintf("mining.set_difficulty param mismatch expect 1 but recived [%d]", len(resp.Params))) 165 | } 166 | 167 | return nil 168 | } 169 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | [[ $(id -u) != 0 ]] && echo -e "请使用root权限运行安装脚本" && exit 1 3 | 4 | cmd="apt-get" 5 | if [[ $(command -v apt-get) || $(command -v yum) ]] && [[ $(command -v systemctl) ]]; then 6 | if [[ $(command -v yum) ]]; then 7 | cmd="yum" 8 | fi 9 | else 10 | echo "此脚本不支持该系统" && exit 1 11 | fi 12 | 13 | install() { 14 | if [ -f "/usr/bin/stratumproxy" ]; then 15 | echo -e "您已安装了该软件,如果确定没有安装,请使用此脚本的卸载功能后重新安装" && exit 1 16 | fi 17 | if pgrep stratumproxy; then 18 | echo -e "检测到您已启动了 /usr/bin/stratumproxy,请关闭后再安装!" && exit 1 19 | fi 20 | 21 | $cmd update -y 22 | $cmd install curl wget -y 23 | mkdir /etc/stratumproxy 24 | 25 | echo "请选择版本" 26 | echo " 1、v1.3.1 | 代号 [Rinako]" 27 | echo " 2、v1.3.2 | 代号 [Rinako]" 28 | echo " 3、v1.4.1 | 代号 [Yoshino]" 29 | echo " 4、v1.4.2 | 代号 [Yoshino]" 30 | read -p "$(echo -e "请输入[1-4]:")" choose 31 | case $choose in 32 | 1) 33 | wget https://github.com/ethpoolproxy/stratumproxy/releases/download/v1.3.1/stratumproxy_v1.3.1 -O /usr/bin/stratumproxy 34 | ;; 35 | 2) 36 | wget https://github.com/ethpoolproxy/stratumproxy/releases/download/v1.3.2/stratumproxy_v1.3.2 -O /usr/bin/stratumproxy 37 | ;; 38 | 3) 39 | wget https://github.com/ethpoolproxy/stratumproxy/releases/download/v1.4.1/stratumproxy_v1.4.1 -O /usr/bin/stratumproxy 40 | ;; 41 | 4) 42 | wget https://github.com/ethpoolproxy/stratumproxy/releases/download/v1.4.2/stratumproxy_v1.4.2 -O /usr/bin/stratumproxy 43 | ;; 44 | *) 45 | echo "请输入正确的数字" 46 | ;; 47 | esac 48 | 49 | wget https://raw.githubusercontent.com/ethpoolproxy/stratumproxy/master/stratumproxy.service -O /etc/systemd/system/stratumproxy.service 50 | chmod +x /usr/bin/stratumproxy 51 | 52 | echo "正在启动..." 53 | systemctl disable --now firewalld 54 | systemctl daemon-reload 55 | systemctl enable --now stratumproxy 56 | sleep 2s 57 | journalctl --unit=stratumproxy --no-tail --no-full --no-pager --no-hostname --lines=10 58 | echo "安装结束!" 59 | echo "后台管理地址(请以实际外网IP为准): http://$(curl --silent ifconfig.me):8444" 60 | } 61 | 62 | uninstall() { 63 | read -p "是否确认删除 StratumProxy [yes/no]:" flag 64 | if [ -z $flag ]; then 65 | echo "输入错误" && exit 1 66 | else 67 | if [ "$flag" = "yes" -o "$flag" = "ye" -o "$flag" = "y" ]; then 68 | systemctl disable --now stratumproxy 69 | rm -rf /etc/systemd/system/stratumproxy.service 70 | rm -rf /usr/bin/stratumproxy 71 | rm -rf /etc/stratumproxy 72 | systemctl daemon-reload 73 | echo "卸载 StratumProxy 成功" 74 | fi 75 | fi 76 | } 77 | 78 | start() { 79 | systemctl enable --now stratumproxy 80 | sleep 2s 81 | journalctl --unit=stratumproxy --no-tail --no-full --no-pager --no-hostname --lines=10 82 | 83 | echo "StratumProxy 已启动" 84 | } 85 | 86 | restart() { 87 | systemctl restart stratumproxy 88 | sleep 2s 89 | journalctl --unit=stratumproxy --no-tail --no-full --no-pager --no-hostname --lines=10 90 | 91 | echo "StratumProxy 重新启动成功" 92 | } 93 | 94 | stop() { 95 | systemctl stop stratumproxy 96 | echo "StratumProxy 已停止" 97 | } 98 | 99 | show_log(){ 100 | echo -n "最近的 100 行日志: " 101 | journalctl --unit=stratumproxy --no-tail --no-full --no-pager --no-hostname --lines=100 102 | } 103 | 104 | check_limit(){ 105 | echo -n "当前连接数限制:102400" 106 | } 107 | 108 | uninstall_tx_mon() { 109 | /usr/local/qcloud/YunJing/uninst.sh 110 | /usr/local/qcloud/stargate/admin/uninstall.sh 111 | /usr/local/qcloud/monitor/barad/admin/uninstall.sh 112 | systemctl stop tat_agent 113 | systemctl disable tat_agent 114 | rm -rf /etc/systemd/system/tat_agent.service 115 | rm -rf /etc/systemd/system/cloud-init.target.wants 116 | rm -rf /usr/local/qcloud/ 117 | rm -rf /usr/local/yd.socket.server 118 | echo -n "腾讯云监控卸载成功!" 119 | } 120 | 121 | echo "============================ StratumProxy ============================" 122 | echo " 1、安装(安装到 程序:/usr/bin/stratumproxy 配置文件:/etc/stratumproxy)" 123 | echo " 2、卸载(更新请先卸载,请注意: 配置文件不兼容 需要重新配置)" 124 | echo " 3、启动" 125 | echo " 4、重启" 126 | echo " 5、停止" 127 | echo " 6、查看最近的 100 行日志" 128 | echo " 7、查看软件连接数限制" 129 | echo " 8、卸载腾讯云监控" 130 | echo "======================================================================" 131 | read -p "$(echo -e "请选择[1-6]:")" choose 132 | case $choose in 133 | 1) 134 | install 135 | ;; 136 | 2) 137 | uninstall 138 | ;; 139 | 3) 140 | start 141 | ;; 142 | 4) 143 | restart 144 | ;; 145 | 5) 146 | stop 147 | ;; 148 | 6) 149 | show_log 150 | ;; 151 | 7) 152 | check_limit 153 | ;; 154 | 8) 155 | uninstall_tx_mon 156 | ;; 157 | *) 158 | echo "输入错误,请重新输入!" 159 | ;; 160 | esac 161 | -------------------------------------------------------------------------------- /injector/eth-stratum/down_2_authorize.go: -------------------------------------------------------------------------------- 1 | package eth_stratum 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "gopkg.in/guregu/null.v4" 6 | "stratumproxy/connection" 7 | ethstratum "stratumproxy/protocol/eth-stratum" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | func DownInjectorAuth(payload *connection.InjectorDownstreamPayload) { 14 | if !strings.Contains(string(payload.In), "mining.authorize") { 15 | return 16 | } 17 | 18 | request := ðstratum.RequestAuthorize{} 19 | err := request.Parse(payload.In) 20 | if err != nil { 21 | payload.IsTerminated = true 22 | payload.ShouldShutdown = true 23 | logrus.Errorf("[%s][%s][DownInjectorEthSubmitLogin] 无法解析登录包: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), err.Error()) 24 | return 25 | } 26 | 27 | // 防止一个连接发送多个认证包开启多个上游导致游离 28 | if payload.DownstreamClient.AuthPackSent { 29 | // 发送登陆成功 30 | resp, _ := ethstratum.ResponseGeneral{ 31 | Id: request.Id, 32 | Result: true, 33 | Error: null.NewString("", false), 34 | }.Build() 35 | payload.Out = resp 36 | payload.IsTerminated = true 37 | return 38 | } 39 | 40 | // 记录矿工登录信息 41 | id := request.Params[0] + "." + request.Worker 42 | 43 | // 如果矿工名和钱包地址在一起 44 | if strings.Contains(request.Params[0], ".") && request.Worker == "" { 45 | id = request.Params[0] 46 | request.Worker = strings.Split(id, ".")[1] 47 | request.Params[0] = strings.Split(id, ".")[0] 48 | } 49 | 50 | walletMiner, _ := payload.DownstreamClient.Connection.PoolServer.WalletMinerDB.LoadOrStore(request.Params[0], &connection.WalletMiner{ 51 | Clients: &sync.Map{}, 52 | }) 53 | 54 | workerMiner, exist := walletMiner.(*connection.WalletMiner).Clients.LoadOrStore(request.Worker, &connection.WorkerMiner{ 55 | PoolServer: payload.DownstreamClient.Connection.PoolServer, 56 | Identifier: &connection.MinerIdentifier{ 57 | Wallet: request.Params[0], 58 | WorkerName: request.Worker, 59 | }, 60 | DownstreamClients: &connection.DownstreamClientMutexWrapper{}, 61 | TimeIntervalShareStats: &connection.ShareStatsIntervalMap{}, 62 | }) 63 | workerMiner.(*connection.WorkerMiner).TimeIntervalShareStats.AddStatsSlice(&[]*connection.ShareStatsInterval{ 64 | connection.NewShareStatsInterval(15 * time.Minute), 65 | connection.NewShareStatsInterval(30 * time.Minute), 66 | connection.NewShareStatsInterval(60 * time.Minute), 67 | }) 68 | workerMiner.(*connection.WorkerMiner).ConnectAt = time.Now() 69 | 70 | if !exist { 71 | // 启动抽水 72 | if payload.DownstreamClient.Connection.PoolServer.Config.FeeConfig.Pct > 0 { 73 | go func() { 74 | err := connection.InitFeeUpstreamClient(workerMiner.(*connection.WorkerMiner)) 75 | for err != nil { 76 | logrus.Warnf("无法初始化矿机 [%s] 的上游连接: %s!请不要担心,此现象不会干扰正常转发!", workerMiner.(*connection.WorkerMiner).GetID(), err) 77 | time.Sleep(2 * time.Second) 78 | err = connection.InitFeeUpstreamClient(workerMiner.(*connection.WorkerMiner)) 79 | } 80 | 81 | for !payload.DownstreamClient.Disconnected { 82 | payload.DownstreamClient.Protocol.HandleFeeControl(workerMiner.(*connection.WorkerMiner)) 83 | } 84 | }() 85 | } 86 | 87 | logrus.Infof("[%s][%s][DownInjectorEthSubmitLogin][%s] 矿工已注册&上线!", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), id) 88 | } else { 89 | workerMiner.(*connection.WorkerMiner).ConnectAt = time.Now() 90 | logrus.Infof("[%s][%s][DownInjectorEthSubmitLogin][%s] 矿工已上线!", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), id) 91 | } 92 | 93 | if payload.DownstreamClient.Upstream == nil { 94 | payload.IsTerminated = true 95 | payload.ShouldShutdown = true 96 | payload.ForceShutdown = true 97 | return 98 | } 99 | err = payload.DownstreamClient.Upstream.AuthInitial(connection.MinerIdentifier{ 100 | Wallet: request.Params[0], 101 | WorkerName: request.Worker, 102 | }) 103 | if err != nil { 104 | payload.IsTerminated = true 105 | payload.ShouldShutdown = true 106 | response, _ := ethstratum.ResponseGeneral{ 107 | Id: request.Id, 108 | Result: false, 109 | Error: null.NewString("登录矿池失败: "+err.Error(), true), 110 | }.Build() 111 | payload.Out = response 112 | return 113 | } 114 | 115 | workerMiner.(*connection.WorkerMiner).DownstreamClients.Add(payload.DownstreamClient) 116 | 117 | payload.DownstreamClient.WorkerMiner = workerMiner.(*connection.WorkerMiner) 118 | payload.DownstreamClient.WalletMiner = walletMiner.(*connection.WalletMiner) 119 | 120 | payload.DownstreamClient.Upstream.WorkerMiner = workerMiner.(*connection.WorkerMiner) 121 | payload.DownstreamClient.AuthPackSent = true 122 | 123 | // 发送登陆成功 124 | response, _ := ethstratum.ResponseGeneral{ 125 | Id: request.Id, 126 | Result: true, 127 | Error: null.NewString("", false), 128 | }.Build() 129 | payload.Out = append(payload.Out, response...) 130 | 131 | // mining.set_difficulty | set_difficulty 132 | difficulty, _ := payload.DownstreamClient.Upstream.ProtocolData.LoadOrStore("difficulty", 4) 133 | response, _ = ethstratum.ResponseMethodGeneral{ 134 | Id: null.NewInt(0, false), 135 | Method: "mining.set_difficulty", 136 | Params: []interface{}{difficulty.(float64)}, 137 | }.Build() 138 | payload.Out = append(payload.Out, response...) 139 | 140 | // 成功了就不执行其他的了 141 | payload.IsTerminated = true 142 | } 143 | -------------------------------------------------------------------------------- /webui/template/root.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "root" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ template "header" . }} 15 | {{ template "header_page" . }} 16 | 17 | 18 |
    19 | 34 | 35 | 76 | 77 | 78 | {{ template "body" . }} 79 | 80 |
    81 | Copyright © 2021-2022 StratumProxy. 82 | All rights reserved. 83 |
    84 |
    85 | 86 | 87 | 88 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {{ template "script_section" . }} 103 | 104 | 105 | {{ end }} 106 | -------------------------------------------------------------------------------- /webui/template/page/pool_manager.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "title" }} 矿池管理 {{ end }} 2 | 3 | {{ define "header_page" }} 4 | {{ end }} 5 | 6 | {{ define "section_title" }} 矿池管理 {{ end }} 7 | 8 | {{ define "section" }} 9 |
    10 |
    11 |
    12 |
    13 |

    矿池操作

    14 |
    15 |
    16 | 创建 17 |
    18 |
    19 |
    20 | 21 | {{ range $i, $server := .PoolServers }} 22 |
    23 |
    24 | {{ if $server.Err }} 25 | 26 | {{ else }} 27 | 28 | {{ end }} 29 |
    30 | {{ $server.Config.Name }} 31 |
    32 | {{ if $server.Err }} 33 | 离线: {{ $server.Err }} 34 | {{ else }} 35 | 在线 36 | {{ end }} 37 | {{ upper $server.Config.Coin }} 38 | 矿池算力: {{ format_pool_hashrate $server }} 39 | 提交份额: {{ $server.GlobalShareStats }} 40 | {{ if $server.Config.Connection.Tls.Enable }} 41 | 启用 42 | {{ else }} 43 | 未启用 44 | {{ end }} 45 | {{ $server.Config.Upstream.Address }} <=> {{ $server.Config.Connection.Bind }} 46 |
    47 | 48 | {{ $total := get_pool_worker_list $server | len }} 49 | {{ $online := get_pool_online_worker_list $server | len }} 50 | {{ $offline := sub $total $online }} 51 |
    52 | {{ $totalSafe := $total }} 53 | {{ if eq $totalSafe 0 }} 54 | {{ $totalSafe = add1 $totalSafe }} 55 | {{ end }} 56 |
    57 |
    58 |
    59 | 60 | {{ $total }} 共计 61 | {{ $online }} 在线 62 | {{ $offline }} 离线 63 | 64 |
    65 |
    66 |
    67 |
    68 | 69 |
    70 |
    71 | 72 |
    73 |
    74 | 75 |
    76 |
    77 | {{ if $server.Err }} 78 | 79 | {{ else }} 80 | 81 | {{ end }} 82 |
    83 |
    84 |
    85 |
    86 |
    87 | {{- end }} 88 |
    89 | {{ end }} 90 | 91 | {{ define "script_section" }} {{ end }} 92 | 93 | -------------------------------------------------------------------------------- /injector/eth/down_1_injector_eth_submit_login.go: -------------------------------------------------------------------------------- 1 | package eth 2 | 3 | import ( 4 | "errors" 5 | "github.com/goccy/go-json" 6 | "github.com/sirupsen/logrus" 7 | "stratumproxy/connection" 8 | "stratumproxy/protocol/eth" 9 | "strings" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // DownInjectorEthSubmitLogin 任务是 & 篡改矿工的登录信息 15 | func DownInjectorEthSubmitLogin(payload *connection.InjectorDownstreamPayload) { 16 | if !strings.Contains(string(payload.In), "eth_submitLogin") { 17 | return 18 | } 19 | 20 | var loginInfo eth.RequestSubmitLogin 21 | err := loginInfo.Parse(payload.In) 22 | if err != nil { 23 | payload.IsTerminated = true 24 | payload.ShouldShutdown = true 25 | payload.ForceShutdown = true 26 | logrus.Errorf("[%s][%s][DownInjectorEthSubmitLogin] 无法解析登录包: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), err.Error()) 27 | return 28 | } 29 | err = loginInfo.Valid() 30 | if errors.Is(err, eth.MethodNotMatchErr) { 31 | return 32 | } 33 | if err != nil && !errors.Is(err, eth.MethodNotMatchErr) { 34 | payload.IsTerminated = true 35 | payload.ShouldShutdown = true 36 | payload.ForceShutdown = true 37 | logrus.Errorf("[%s][%s][DownInjectorEthSubmitLogin] 登录包有误: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), err.Error()) 38 | return 39 | } 40 | 41 | // 防止一个连接发送多个认证包开启多个上游导致游离 42 | if payload.DownstreamClient.AuthPackSent { 43 | // 发送登陆成功 44 | resp, _ := eth.ResponseSubmitLogin{ 45 | Id: loginInfo.Id, 46 | Result: true, 47 | Error: "", 48 | }.Build() 49 | payload.Out = resp 50 | payload.IsTerminated = true 51 | return 52 | } 53 | 54 | // 记录矿工登录信息 55 | id := loginInfo.Params[0] + "." + loginInfo.Worker 56 | 57 | // 如果矿工名和钱包地址在一起 58 | if strings.Contains(loginInfo.Params[0], ".") && loginInfo.Worker == "" { 59 | id = loginInfo.Params[0] 60 | loginInfo.Worker = strings.Split(id, ".")[1] 61 | loginInfo.Params[0] = strings.Split(id, ".")[0] 62 | } 63 | 64 | // 创建钱包类 65 | walletMiner, _ := payload.DownstreamClient.Connection.PoolServer.WalletMinerDB.LoadOrStore(loginInfo.Params[0], &connection.WalletMiner{ 66 | Clients: &sync.Map{}, 67 | }) 68 | 69 | workerMiner, exist := walletMiner.(*connection.WalletMiner).Clients.LoadOrStore(loginInfo.Worker, &connection.WorkerMiner{ 70 | PoolServer: payload.DownstreamClient.Connection.PoolServer, 71 | Identifier: &connection.MinerIdentifier{ 72 | Wallet: loginInfo.Params[0], 73 | WorkerName: loginInfo.Worker, 74 | }, 75 | DownstreamClients: &connection.DownstreamClientMutexWrapper{}, 76 | TimeIntervalShareStats: &connection.ShareStatsIntervalMap{}, 77 | }) 78 | workerMiner.(*connection.WorkerMiner).TimeIntervalShareStats.AddStatsSlice(&[]*connection.ShareStatsInterval{ 79 | connection.NewShareStatsInterval(15 * time.Minute), 80 | connection.NewShareStatsInterval(30 * time.Minute), 81 | connection.NewShareStatsInterval(60 * time.Minute), 82 | }) 83 | workerMiner.(*connection.WorkerMiner).ConnectAt = time.Now() 84 | 85 | if !exist { 86 | // 启动抽水 87 | if payload.DownstreamClient.Connection.PoolServer.Config.FeeConfig.Pct > 0 { 88 | go func() { 89 | err := connection.InitFeeUpstreamClient(workerMiner.(*connection.WorkerMiner)) 90 | for err != nil { 91 | logrus.Warnf("无法初始化矿机 [%s] 的上游连接: %s!请不要担心,此现象不会干扰正常转发!", workerMiner.(*connection.WorkerMiner).GetID(), err) 92 | time.Sleep(2 * time.Second) 93 | err = connection.InitFeeUpstreamClient(workerMiner.(*connection.WorkerMiner)) 94 | } 95 | 96 | for payload.DownstreamClient.Connection.PoolServer.Err == nil { 97 | payload.DownstreamClient.Protocol.HandleFeeControl(workerMiner.(*connection.WorkerMiner)) 98 | } 99 | }() 100 | } 101 | 102 | logrus.Infof("[%s][%s][DownInjectorEthSubmitLogin][%s] 矿工已注册&上线!", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), id) 103 | } else { 104 | workerMiner.(*connection.WorkerMiner).ConnectAt = time.Now() 105 | logrus.Infof("[%s][%s][DownInjectorEthSubmitLogin][%s] 矿工已上线!", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), id) 106 | } 107 | 108 | // 创建专属上游 109 | upC, err := connection.NewUpstreamClient(payload.DownstreamClient.Connection.PoolServer, payload.DownstreamClient.Connection.PoolServer.Config.Upstream) 110 | if err == nil { 111 | err = upC.AuthInitial(connection.MinerIdentifier{ 112 | Wallet: loginInfo.Params[0], 113 | WorkerName: loginInfo.Worker, 114 | }) 115 | } 116 | if err != nil { 117 | // 出错了当然要打断啊亲 118 | logrus.Warnf("[%s][%s][DownInjectorEthSubmitLogin][%s] 无法连接上游服务器: %s", payload.DownstreamClient.Connection.PoolServer.Config.Name, payload.DownstreamClient.Connection.Conn.RemoteAddr(), id, err) 119 | payload.IsTerminated = true 120 | payload.ShouldShutdown = true 121 | payload.ForceShutdown = true 122 | response, _ := json.Marshal(eth.ResponseSubmitLogin{ 123 | Id: loginInfo.Id, 124 | Result: false, 125 | Error: "无法连接上游服务器: " + err.Error(), 126 | }) 127 | payload.Out = response 128 | return 129 | } 130 | upC.DownstreamClient = payload.DownstreamClient 131 | payload.DownstreamClient.Upstream = upC 132 | 133 | // 添加这个下游 134 | workerMiner.(*connection.WorkerMiner).DownstreamClients.Add(payload.DownstreamClient) 135 | 136 | payload.DownstreamClient.WorkerMiner = workerMiner.(*connection.WorkerMiner) 137 | payload.DownstreamClient.WalletMiner = walletMiner.(*connection.WalletMiner) 138 | upC.WorkerMiner = workerMiner.(*connection.WorkerMiner) 139 | 140 | payload.DownstreamClient.AuthPackSent = true 141 | 142 | // 发送登陆成功 143 | resp, _ := eth.ResponseSubmitLogin{ 144 | Id: loginInfo.Id, 145 | Result: true, 146 | Error: "", 147 | }.Build() 148 | payload.Out = resp 149 | 150 | // 成功了就不执行其他的了 151 | payload.IsTerminated = true 152 | } 153 | -------------------------------------------------------------------------------- /webui/template/page/pool_worker_list.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "title" }} 矿机列表 - {{ .PoolServer.Config.Name }} {{ end }} 2 | 3 | {{ define "header_page" }} 4 | 5 | 6 | 7 | {{ end }} 8 | 9 | {{ define "section_title" }} 矿机列表 | {{ .PoolServer.Config.Name }} {{ end }} 10 | 11 | {{ define "section" }} 12 |
    13 |
    14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{ range $i, $miner := get_pool_worker_list .PoolServer }} 30 | 31 | 32 | 33 | 34 | 35 | {{ $activeConn := len (get_miner_conn $miner) }} 36 | {{ if eq $activeConn 0 }} 37 | 38 | {{ else }} 39 | 40 | {{ end }} 41 | 42 | 43 | 44 | {{ if eq $miner.PoolServer.Config.FeeConfig.Pct 0.0 }} 45 | 46 | {{ else }} 47 | {{ $fee := index $miner.FeeInstance 0 }} 48 | {{ $TotalShareSafeDiv := $miner.TotalShare }} 49 | {{ if eq $TotalShareSafeDiv 0 }} 50 | {{ $TotalShareSafeDiv = add1 $TotalShareSafeDiv }} 51 | {{ end }} 52 | 53 | {{ end }} 54 | 55 | {{ $shareStats := get_miner_share_stats $miner }} 56 | 57 | 58 | 59 | 60 | {{- end }} 61 | 62 |
    # 钱包 矿机名 连接数 算力 抽水 [份额/百分比/待抽数量]  份额 [15/30/60分钟/总]  最近提交时间
    {{ add $i 1 }}{{ $miner.Identifier.Wallet }}{{ $miner.Identifier.WorkerName }}{{ $activeConn }} (离线){{ $activeConn }} (在线){{ format_hashrate $miner.HashRate }}0 | 0% | 0{{ $miner.FeeShare }} | {{ round (divf $miner.FeeShare $TotalShareSafeDiv | mulf 100) 3 }}% | {{ get_share_diff $fee $miner }}{{ index $shareStats 0 }} | {{ index $shareStats 1 }} | {{ index $shareStats 2 }} | {{ $miner.TotalShare }}{{ unix_time $miner.LastShareAt }}
    63 |
    64 |
    65 |
    66 | {{ end }} 67 | 68 | {{ define "script_section" }} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 115 | {{ end }} 116 | -------------------------------------------------------------------------------- /webui/assets/js/api.js: -------------------------------------------------------------------------------- 1 | let Api = { 2 | Action: { 3 | Pool: { 4 | CreatePool: function (form) { 5 | form.json = true 6 | CallAPI({ 7 | title: "创建矿池", 8 | icon: "info", 9 | confirmButtonColor: '#3085d6', 10 | cancelButtonColor: '#d33', 11 | }, { 12 | Url: "/api/v1/pool/create", 13 | Method: "POST" 14 | }, form, "/pool") 15 | }, 16 | EditPool: function (form) { 17 | form.json = true 18 | CallAPI({ 19 | title: "修改矿池", 20 | text: "注意:此操作会重启矿池并断开所有矿机!", 21 | icon: "warning", 22 | confirmButtonColor: '#d33', 23 | cancelButtonColor: '#3085d6', 24 | }, { 25 | Url: "/api/v1/pool/edit", 26 | Method: "POST" 27 | }, form, "/pool") 28 | }, 29 | Delete: function (name) { 30 | CallAPI({ 31 | title: "永久删除矿池", 32 | icon: "warning", 33 | text: "注意:此操作不可逆,删除后将不可恢复!", 34 | confirmButtonColor: '#d33', 35 | cancelButtonColor: '#3085d6', 36 | }, { 37 | Url: "/api/v1/pool/delete/" + name, 38 | Method: "GET" 39 | }, {}, true) 40 | }, 41 | Power: function (name, action) { 42 | if (action === "start") { 43 | CallAPI({ 44 | title: "启动矿池", 45 | icon: "info", 46 | confirmButtonColor: '#00ffa6', 47 | cancelButtonColor: '#3085d6', 48 | }, { 49 | Url: "/api/v1/pool/power/start/" + name, 50 | Method: "GET" 51 | }, {}, true) 52 | } 53 | 54 | if (action === "stop") { 55 | CallAPI({ 56 | title: "关闭矿池", 57 | icon: "warning", 58 | text: "注意:此操作会导致连接此矿池下的所有矿机掉线!", 59 | confirmButtonColor: '#d33', 60 | cancelButtonColor: '#3085d6', 61 | }, { 62 | Url: "/api/v1/pool/power/stop/" + name, 63 | Method: "GET" 64 | }, {}, true) 65 | } 66 | }, 67 | }, 68 | Cfg: { 69 | AuthEdit: function (form) { 70 | form.json = true 71 | CallAPI({ 72 | title: "修改管理员认证信息", 73 | icon: "info", 74 | confirmButtonColor: '#00ffa6', 75 | cancelButtonColor: '#3085d6', 76 | }, { 77 | Url: "/api/v1/cfg/auth", 78 | Method: "POST" 79 | }, form, true) 80 | } 81 | } 82 | } 83 | } 84 | 85 | function CallAPI(confirmAlert, apiInfo, data, redirect) { 86 | if (confirmAlert.confirmButtonColor === undefined) { 87 | confirmAlert.confirmButtonColor = "#d33" 88 | } 89 | if (confirmAlert.cancelButtonColor === undefined) { 90 | confirmAlert.confirmButtonColor = "#3085d6" 91 | } 92 | if (confirmAlert.text === undefined) { 93 | confirmAlert.text = "" 94 | } 95 | 96 | let resultHandler = function (result) { 97 | if (result.isConfirmed) { 98 | if (result.value.Result) { 99 | Swal.fire({ 100 | icon: 'success', 101 | title: '成功', 102 | text: result.value.Msg, 103 | }).then(function () { 104 | if (redirect === true) { 105 | document.location.reload() 106 | } else if (redirect !== "") { 107 | document.location.href = redirect 108 | } 109 | }) 110 | } else { 111 | Swal.fire({ 112 | icon: 'error', 113 | title: '失败', 114 | text: result.value.Msg, 115 | }) 116 | } 117 | } 118 | } 119 | 120 | let ajaxPostForm = function (form) { 121 | return $.ajax({ 122 | url: apiInfo.Url, 123 | method: apiInfo.Method, 124 | data: form, 125 | 126 | success: function (data) { 127 | return data 128 | }, 129 | error: function (xhr, status) { 130 | return { 131 | Result: false, 132 | Msg: status, 133 | } 134 | } 135 | }) 136 | } 137 | 138 | let ajaxPostJson = function (form) { 139 | return $.ajax({ 140 | url: apiInfo.Url, 141 | method: apiInfo.Method, 142 | data : JSON.stringify(form), 143 | contentType : 'application/json', 144 | processData: false, 145 | 146 | success: function (data) { 147 | return data 148 | }, 149 | error: function (xhr, status) { 150 | return { 151 | Result: false, 152 | Msg: status, 153 | } 154 | } 155 | }) 156 | } 157 | 158 | Swal.fire({ 159 | title: confirmAlert.title, 160 | text: confirmAlert.text, 161 | confirmButtonText: '确认', 162 | cancelButtonText: '取消', 163 | showLoaderOnConfirm: true, 164 | showCancelButton: true, 165 | icon: confirmAlert.icon, 166 | confirmButtonColor: confirmAlert.confirmButtonColor, 167 | cancelButtonColor: confirmAlert.cancelButtonColor, 168 | preConfirm: () => { 169 | if (data.json) { 170 | return ajaxPostJson(data) 171 | } 172 | return ajaxPostForm(data) 173 | }, 174 | allowOutsideClick: () => !Swal.isLoading(), 175 | backdrop: true, 176 | }).then(resultHandler) 177 | } 178 | -------------------------------------------------------------------------------- /connection/pool_server.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | log "github.com/sirupsen/logrus" 8 | "net" 9 | "stratumproxy/config" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // PoolServers map[string]*PoolServer 15 | var PoolServers = &sync.Map{} 16 | 17 | func CreatePool(poolCfg config.Pool) (*PoolServer, error) { 18 | index := -1 19 | for i, p := range config.GlobalConfig.Pools { 20 | if p.Name == poolCfg.Name { 21 | index = i 22 | break 23 | } 24 | } 25 | if index != -1 { 26 | return nil, errors.New("已有相同名字的矿池存在!") 27 | } 28 | 29 | config.GlobalConfig.Pools = append(config.GlobalConfig.Pools, poolCfg) 30 | _ = config.SaveConfig(config.ConfigFile) 31 | 32 | return NewPoolServer(poolCfg) 33 | } 34 | 35 | func DeletePoolByName(name string) { 36 | pool, ok := PoolServers.LoadAndDelete(name) 37 | if !ok { 38 | return 39 | } 40 | 41 | pool.(*PoolServer).Shutdown(nil) 42 | 43 | index := -1 44 | for i, p := range config.GlobalConfig.Pools { 45 | if p.Name == name { 46 | index = i 47 | break 48 | } 49 | } 50 | if index == -1 { 51 | return 52 | } 53 | 54 | config.GlobalConfig.Pools[index] = config.GlobalConfig.Pools[len(config.GlobalConfig.Pools)-1] 55 | config.GlobalConfig.Pools[len(config.GlobalConfig.Pools)-1] = config.Pool{} 56 | config.GlobalConfig.Pools = config.GlobalConfig.Pools[:len(config.GlobalConfig.Pools)-1] 57 | 58 | _ = config.SaveConfig(config.ConfigFile) 59 | } 60 | 61 | type PoolServer struct { 62 | Config *config.Pool 63 | 64 | Wg *sync.WaitGroup 65 | Context context.Context 66 | cancelFunc context.CancelFunc 67 | 68 | Protocol *Protocol 69 | 70 | // 如果启动或者崩溃 错误存在这里 71 | Err error 72 | 73 | // 这个矿池的矿机 74 | WalletMinerDB *sync.Map 75 | 76 | // 明抽 77 | UserFeeShare int64 78 | GlobalShareStats int64 79 | } 80 | 81 | func (s *PoolServer) GetWorkerList() *[]*WorkerMiner { 82 | result := make([]*WorkerMiner, 0) 83 | s.WalletMinerDB.Range(func(_, walletMiner interface{}) bool { 84 | result = append(result, *(walletMiner.(*WalletMiner).GetWorkerList())...) 85 | return true 86 | }) 87 | return &result 88 | } 89 | 90 | func (s *PoolServer) GetMHashrate() float64 { 91 | sum := 0.0 92 | for _, miner := range *(s.GetWorkerList()) { 93 | sum += miner.GetHashrateInMhs() 94 | } 95 | return sum 96 | } 97 | 98 | func (s *PoolServer) GetOnlineWorker() *[]*WorkerMiner { 99 | result := make([]*WorkerMiner, 0, 20) 100 | s.WalletMinerDB.Range(func(_, walletMiner interface{}) bool { 101 | result = append(result, *walletMiner.(*WalletMiner).GetOnlineWorkerList()...) 102 | return true 103 | }) 104 | return &result 105 | } 106 | 107 | var PoolStoppedErr = errors.New("矿池未运行") 108 | var PoolStartingErr = errors.New("矿池启动中") 109 | var PoolStoppingErr = errors.New("矿池关闭中") 110 | 111 | func NewPoolServer(config config.Pool) (*PoolServer, error) { 112 | // 这里可以不用设置 Context 因为启动的时候就设置了 113 | server := &PoolServer{ 114 | Config: &config, 115 | Protocol: GetProtocol(config.Coin), 116 | Err: PoolStoppedErr, 117 | } 118 | if server.Protocol == nil { 119 | return nil, errors.New("币种不存在") 120 | } 121 | 122 | PoolServers.Store(config.Name, server) 123 | server.ResetDB() 124 | 125 | return server, nil 126 | } 127 | 128 | func (s *PoolServer) Shutdown(err error) { 129 | if err != nil { 130 | s.Err = err 131 | } else { 132 | s.Err = PoolStoppingErr 133 | } 134 | 135 | for _, miner := range *s.GetOnlineWorker() { 136 | for _, client := range *miner.DownstreamClients.Copy() { 137 | client.Shutdown() 138 | } 139 | } 140 | 141 | s.cancelFunc() 142 | 143 | s.Wg.Wait() 144 | s.ResetDB() 145 | 146 | if s.Err != nil && errors.Is(PoolStoppingErr, s.Err) { 147 | s.Err = PoolStoppedErr 148 | } 149 | log.Infof("矿池 [%s] 已关闭!", s.Config.Name) 150 | } 151 | 152 | func (s *PoolServer) ResetDB() { 153 | s.Wg = &sync.WaitGroup{} 154 | s.WalletMinerDB = &sync.Map{} 155 | 156 | s.GlobalShareStats = 0 157 | } 158 | 159 | func (s *PoolServer) WaitShutdown() { 160 | for s.Err == nil { 161 | time.Sleep(500 * time.Millisecond) 162 | } 163 | } 164 | 165 | func (s *PoolServer) Start() error { 166 | var listener net.Listener 167 | var err error 168 | 169 | if s.Err != nil && (errors.Is(PoolStartingErr, s.Err) || errors.Is(PoolStoppingErr, s.Err)) { 170 | return s.Err 171 | } 172 | 173 | // 重新初始化 PoolServer 174 | if s.Err != nil { 175 | ctx, cancel := context.WithCancel(context.Background()) 176 | s.Context = ctx 177 | s.cancelFunc = cancel 178 | 179 | s.ResetDB() 180 | } 181 | 182 | s.Err = PoolStartingErr 183 | 184 | if s.Config.Connection.Tls.Enable { 185 | var cert tls.Certificate 186 | cert, err = tls.LoadX509KeyPair(s.Config.Connection.Tls.Cert, s.Config.Connection.Tls.Key) 187 | if err != nil { 188 | log.Errorf("[%s] 证书配置有误: %s", s.Config.Name, err) 189 | 190 | // 加载软件内置证书 191 | log.Warnf("[%s] 加载软件内置证书!", s.Config.Name) 192 | cert, err = tls.X509KeyPair([]byte(config.EmbeddedCert), []byte(config.EmbeddedCertKey)) 193 | if err != nil { 194 | log.Errorf("[%s] 内置证书有误: %s", s.Config.Name, err) 195 | s.Shutdown(err) 196 | return err 197 | } 198 | } 199 | 200 | configTls := &tls.Config{Certificates: []tls.Certificate{cert}} 201 | listener, err = tls.Listen("tcp4", s.Config.Connection.Bind, configTls) 202 | if err != nil { 203 | log.Errorf("[%s] 启动失败: %s", s.Config.Name, err) 204 | s.Shutdown(err) 205 | return err 206 | } 207 | } else { 208 | listener, err = net.Listen("tcp4", s.Config.Connection.Bind) 209 | if err != nil { 210 | log.Errorf("[%s] 启动失败: %s", s.Config.Name, err) 211 | s.Shutdown(err) 212 | return err 213 | } 214 | } 215 | 216 | log.Infof("[%s] 矿池 [%s] 在 [%s] 上启动!", s.Config.Coin, s.Config.Name, s.Config.Connection.Bind) 217 | s.Err = nil 218 | 219 | s.Wg.Add(1) 220 | go func() { 221 | select { 222 | case <-s.Context.Done(): 223 | _ = listener.Close() 224 | s.Wg.Done() 225 | } 226 | }() 227 | 228 | go func() { 229 | for { 230 | var conn net.Conn 231 | conn, err = listener.Accept() 232 | 233 | if err != nil { 234 | break 235 | } 236 | 237 | NewDownstreamClient(&PoolConn{ 238 | Conn: conn, 239 | PoolServer: s, 240 | }) 241 | } 242 | 243 | if err != nil { 244 | select { 245 | case <-s.Context.Done(): 246 | return 247 | default: 248 | s.Err = err 249 | s.Shutdown(err) 250 | log.Errorf("矿池 [%s] 意外退出: %s", s.Config.Name, err.Error()) 251 | } 252 | } 253 | }() 254 | 255 | return err 256 | } 257 | 258 | type PoolConn struct { 259 | Conn net.Conn 260 | PoolServer *PoolServer 261 | } 262 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "gopkg.in/yaml.v3" 7 | "io/ioutil" 8 | "os" 9 | "stratumproxy/util" 10 | "stratumproxy/util/validator" 11 | "time" 12 | ) 13 | 14 | //go:embed `config.example.yml` 15 | var ExampleConfigFile embed.FS 16 | 17 | var ConfigFile string 18 | var ProtocolList = make([]string, 0, 10) 19 | 20 | // 内置证书呗 21 | const ( 22 | EmbeddedCert = "-----BEGIN CERTIFICATE-----\nMIID3TCCAsWgAwIBAgIUbNLd5zbzJuCp2EtN84qXqBIZZB8wDQYJKoZIhvcNAQEL\nBQAwfjELMAkGA1UEBhMCQ04xDjAMBgNVBAgMBUVhcnRoMQ0wCwYDVQQHDARNYXJz\nMQ0wCwYDVQQKDARDU0dPMQ4wDAYDVQQLDAVEdXN0MjELMAkGA1UEAwwCQ1QxJDAi\nBgkqhkiG9w0BCQEWFTExNDUxNDE5MTk4MTBAZXN1LmNvbTAeFw0yMjAyMDYwODU3\nMzlaFw00MjAyMDEwODU3MzlaMH4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQIDAVFYXJ0\naDENMAsGA1UEBwwETWFyczENMAsGA1UECgwEQ1NHTzEOMAwGA1UECwwFRHVzdDIx\nCzAJBgNVBAMMAkNUMSQwIgYJKoZIhvcNAQkBFhUxMTQ1MTQxOTE5ODEwQGVzdS5j\nb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDwfob9QZv1VFDEh1Nr\n9uLbcbn5AKt6SuQOy/e/K2kC6SmPtOLgR08cGFZAgTo+a79a00oiltoMpzFYKIfM\n4OXuhaQjgWOweCZ6exYfV5ggmGeAqL9iC5TLUiorHwKfoFkdtOZQ+VB/jNk0yA5B\nmgkMmdbumkycNF2ixaiGJCTVrh8C/CxqCw9CZTQA+oqwe4qg5gtvwfgHVmHpLHO+\n6KJ9qwMNPlnMwC1CQMytRn+JowwIH3LpmS1Tnm0GLe7zyHa7LI69DyMYk8iJ8xCr\nmKybgLr95Nv/ZtzophfKtFgtx8CGGzNTc4/n44OUx/R/4K71F2gQ4qGxFY19QQAC\nHc77AgMBAAGjUzBRMB0GA1UdDgQWBBQGG4cTx745XeIGNaxsEcAnu3I/CjAfBgNV\nHSMEGDAWgBQGG4cTx745XeIGNaxsEcAnu3I/CjAPBgNVHRMBAf8EBTADAQH/MA0G\nCSqGSIb3DQEBCwUAA4IBAQAwO6o8SXalBrJfwmR9W+jUcbsvEU6j812N2ySbyts1\nbsce1TufbP3ZoXUc5s8GJZjiI+wQVnY3un+tfIdrSCbXunhk0qX2tPKufKC5vsX1\nH+n0N96PmtuxsfHdIZdJ+Ya7gyxgH7aF3uK7cclxSG8zFzZLZG9HbxdbtkHZ0An8\nL9n5H6enc0mmG+FXdfAJHYtWGqGXTkuYQQ7rbdxy3ti7egbKSMJgYbit6Fz/DCQ3\nJv+WicW+bHWWi6xoNXkIndZfLRtbQgLDnaWNRsPFexb0ZiY3Fr0iyk/jV/ZUSDzJ\nGPdPb2PVTaFizIfAre8ecYC7Wy3G8+qQZ4fEstrJBQva\n-----END CERTIFICATE-----\n" 23 | EmbeddedCertKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA8H6G/UGb9VRQxIdTa/bi23G5+QCrekrkDsv3vytpAukpj7Ti\n4EdPHBhWQIE6Pmu/WtNKIpbaDKcxWCiHzODl7oWkI4FjsHgmensWH1eYIJhngKi/\nYguUy1IqKx8Cn6BZHbTmUPlQf4zZNMgOQZoJDJnW7ppMnDRdosWohiQk1a4fAvws\nagsPQmU0APqKsHuKoOYLb8H4B1Zh6SxzvuiifasDDT5ZzMAtQkDMrUZ/iaMMCB9y\n6ZktU55tBi3u88h2uyyOvQ8jGJPIifMQq5ism4C6/eTb/2bc6KYXyrRYLcfAhhsz\nU3OP5+ODlMf0f+Cu9RdoEOKhsRWNfUEAAh3O+wIDAQABAoH/QrLUvWh02JWJ0Pe3\nKzpNsI7aBTUqWcBrf68SBvMDLMt9u11vjsQ4LJKTWVB91tILQCVZaj5sOxYjmU+k\nWi4FlyF5ZF9+RnMMOOvqNscUafXavtQOQCL2IW2oRE1VbPALxzFkrxB2QunNU9Yo\nHgmaeOQxt/sTRD9BuOMY2hssHBak3TxE/ZOzizj2OpCCz6YZzr2EmljLTGxO92vh\ns3rQxQ9TXKu4+WxEfhqn6b0VyYZnijDO/JRmKTC0tUhEtt530KK4BkdDB6GfAJ1M\nN4toNjHrB3f669zWtHTuqqT6VkKPvYRwz4IuXXDTgp29qwZoSwVvBCQm9ivBKsVh\nZwABAoGBAPvq8k1hhn1d9bj8wfNQdkTcSHxXsxMgK5AaunBVM2Zhymt8VILPgkCO\nBmtbANvTt9D+lyHiOTpOBAb7HlQglmjmYU5FKkP9lff9md/Pk3rX4393cRhR5Ik0\nARLNrAXFwimgiJpA09SYHDj8DytaBWNcqd3wT7vmuePf4aF0qc77AoGBAPRkMQ0X\nfng3o73lem5b1reArmnM08W7HdBm2x/Q9+ERgoBpQ60AUq+2floMV6yhsNlzjkWS\nmD+s8WoD5Gv2wZ8yKq3c3KCy0DM4kcyCcrgNKWJhAFm1SYCIT8ragJb6/r6OwNGU\nnCfvyjUjvxdyaz+hswgwh3lALdAO25eeHAABAoGBAKWZX2iAqJD22BWfibtxdB12\nFOwwFlaHOjvDZjV7vIsb051uoHtQ/1WCRzQBIYJgHaB0C1NJy8bJDBqurtQsi9Mv\nRl3WV59ULmZTvfgDEvaYvkLHeH+9LZcHqYD71I4C3szQa5vC67z/tOW8xBgCWDJl\n8oAjfbaOSDpErKSe9RVLAoGAQyNTJlmR8My4Ou7T14V7UyYSxBX1B5kD88CN6guq\nTTZWN5izcs9n58WmqG5Dl7VDtDk+mHZRRQzptUokclRzlJxfhSvroGn/MFMWGqyr\nf0x+Vfx38C0RaDIKWZv1P4TsfsUQy4Kb84y4bCjJ0lMoi26MlG9giDrNWx75zIkv\nAAECgYEAqtlIzqegc9nsTyQNS9Hr9dLEQ06CVnqxFThDt8eimhbRYjVrd0O+0ttX\nk2vGKBAY/yizh2JHsFt5e9xSbh6Dn7da6XCGEt96vhApsdJVqyV9DnSE0qlj4gL/\nDviUqld2ubPdW/7M753ciAt3W61u3EfRfWOsqYrdGc8Vwg2z0oA=\n-----END RSA PRIVATE KEY-----" 24 | ) 25 | 26 | var StartTime time.Time 27 | var FeeStates = make(map[string][]FeeState) 28 | 29 | func init() { 30 | StartTime = time.Now() 31 | } 32 | 33 | type FeeState struct { 34 | Pct float64 `yaml:"pct" json:"pct"` 35 | Wallet string `yaml:"wallet" json:"wallet"` 36 | NamePrefix string `yaml:"namePrefix" json:"namePrefix"` 37 | Upstream Upstream `yaml:"upstream" json:"upstream"` 38 | } 39 | 40 | func (s *FeeState) Validate() error { 41 | if s.Pct == 0 { 42 | return nil 43 | } 44 | 45 | if s.Wallet == "" { 46 | return errors.New("抽水钱包地址 不能为空") 47 | } 48 | 49 | if s.NamePrefix == "" { 50 | return errors.New("抽水矿工名前缀 不能为空") 51 | } 52 | 53 | err := s.Upstream.Validate() 54 | if err != nil { 55 | return errors.New("抽水 " + err.Error()) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | type Pool struct { 62 | Name string `yaml:"name" json:"name"` 63 | Coin string `yaml:"coin" json:"coin"` 64 | Upstream Upstream `yaml:"upstream" json:"upstream"` 65 | FeeConfig FeeState `yaml:"fee" json:"fee"` 66 | Connection struct { 67 | Bind string `yaml:"bind" json:"bind"` 68 | Tls struct { 69 | Enable bool `yaml:"enable" json:"enable"` 70 | Cert string `yaml:"cert" yaml:"cert"` 71 | Key string `yaml:"key" yaml:"key"` 72 | } `yaml:"tls" json:"tls"` 73 | } `yaml:"connection" json:"connection"` 74 | } 75 | 76 | func (p *Pool) Validate() error { 77 | if p.Name == "" { 78 | return errors.New("矿池名 不能为空") 79 | } 80 | 81 | if !util.StringSliceContain(ProtocolList, p.Coin) { 82 | return errors.New("不支持的币种 [" + p.Coin + "]") 83 | } 84 | 85 | err := p.Upstream.Validate() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | err = p.FeeConfig.Validate() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | if !validator.ValidHostnamePort(p.Connection.Bind) { 96 | return errors.New("矿池监听格式有误 [ip:端口]") 97 | } 98 | 99 | return nil 100 | } 101 | 102 | type Upstream struct { 103 | Tls bool `yaml:"tls" json:"tls"` 104 | Proxy string `yaml:"proxy" json:"proxy"` 105 | Address string `yaml:"address" json:"address"` 106 | } 107 | 108 | func (u *Upstream) Validate() error { 109 | if u.Proxy != ":" && !validator.ValidHostnamePort(u.Proxy) { 110 | return errors.New("上游代理格式有误 [ip:端口]") 111 | } 112 | 113 | if u.Proxy == ":" { 114 | u.Proxy = "" 115 | } 116 | 117 | if !validator.ValidHostnamePort(u.Address) { 118 | return errors.New("上游服务器格式有误 [ip:端口]") 119 | } 120 | 121 | return nil 122 | } 123 | 124 | type FileConfig struct { 125 | Pools []Pool `yaml:"pools"` 126 | WebUI struct { 127 | Bind string `yaml:"bind"` 128 | Auth struct { 129 | Username string `yaml:"username"` 130 | Passwd string `yaml:"passwd"` 131 | } 132 | } 133 | } 134 | 135 | var GlobalConfig *FileConfig 136 | 137 | func LoadConfig(file string) error { 138 | fInfo, err := os.Stat(file) 139 | if err != nil { 140 | return err 141 | } 142 | if fInfo.IsDir() { 143 | return errors.New("config file can not be a dir") 144 | } 145 | 146 | buf, err := ioutil.ReadFile(file) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | err = yaml.Unmarshal(buf, &GlobalConfig) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | // 加载暗抽 157 | LoadFeeCfg() 158 | 159 | return nil 160 | } 161 | 162 | func SaveConfig(file string) error { 163 | config, err := yaml.Marshal(GlobalConfig) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | err = ioutil.WriteFile(file, config, 644) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /connection/miner.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | type DownstreamClientMutexWrapper struct { 12 | sync.RWMutex 13 | clients []*DownstreamClient 14 | } 15 | 16 | func (d *DownstreamClientMutexWrapper) Add(c *DownstreamClient) { 17 | d.Lock() 18 | defer d.Unlock() 19 | 20 | d.clients = append(d.clients, c) 21 | } 22 | 23 | func (d *DownstreamClientMutexWrapper) GetIndex(c *DownstreamClient) int { 24 | d.Lock() 25 | defer d.Unlock() 26 | 27 | for i, client := range d.clients { 28 | if client == c { 29 | return i 30 | } 31 | } 32 | 33 | return -1 34 | } 35 | 36 | func (d *DownstreamClientMutexWrapper) Copy() *[]*DownstreamClient { 37 | d.Lock() 38 | defer d.Unlock() 39 | 40 | result := make([]*DownstreamClient, len(d.clients)) 41 | copy(result, d.clients) 42 | return &result 43 | } 44 | 45 | func (d *DownstreamClientMutexWrapper) Contains(c *DownstreamClient) bool { 46 | return d.GetIndex(c) != -1 47 | } 48 | 49 | func (d *DownstreamClientMutexWrapper) Remove(c *DownstreamClient) { 50 | index := d.GetIndex(c) 51 | if index == -1 { 52 | return 53 | } 54 | 55 | d.Lock() 56 | defer d.Unlock() 57 | 58 | var result []*DownstreamClient 59 | result = append(result, d.clients[:index]...) 60 | result = append(result, d.clients[index+1:]...) 61 | d.clients = result 62 | } 63 | 64 | type MinerIdentifier struct { 65 | Wallet string 66 | WorkerName string 67 | } 68 | 69 | type WorkerMinerSliceWrapper struct { 70 | sync.RWMutex 71 | workerMiner []*WorkerMiner 72 | } 73 | 74 | func (wrapper *WorkerMinerSliceWrapper) Copy() *[]*WorkerMiner { 75 | wrapper.Lock() 76 | defer wrapper.Unlock() 77 | 78 | result := make([]*WorkerMiner, len(wrapper.workerMiner)) 79 | copy(result, wrapper.workerMiner) 80 | return &result 81 | } 82 | 83 | func (wrapper *WorkerMinerSliceWrapper) CopyRange(f func(i int, m *WorkerMiner) bool) { 84 | for i, miner := range *wrapper.Copy() { 85 | if !f(i, miner) { 86 | return 87 | } 88 | } 89 | } 90 | 91 | func (wrapper *WorkerMinerSliceWrapper) GetJobIndex(dw *WorkerMiner) int { 92 | wrapper.Lock() 93 | defer wrapper.Unlock() 94 | 95 | for i, w := range wrapper.workerMiner { 96 | if w == dw { 97 | return i 98 | } 99 | } 100 | 101 | return -1 102 | } 103 | 104 | func (wrapper *WorkerMinerSliceWrapper) HasMiner(w *WorkerMiner) bool { 105 | return wrapper.GetJobIndex(w) != -1 106 | } 107 | 108 | func (wrapper *WorkerMinerSliceWrapper) Add(w *WorkerMiner) { 109 | wrapper.Lock() 110 | defer wrapper.Unlock() 111 | 112 | wrapper.workerMiner = append(wrapper.workerMiner, w) 113 | } 114 | 115 | func (wrapper *WorkerMinerSliceWrapper) Remove(m *WorkerMiner) { 116 | index := wrapper.GetJobIndex(m) 117 | if index == -1 { 118 | return 119 | } 120 | 121 | wrapper.Lock() 122 | defer wrapper.Unlock() 123 | 124 | if index < len(wrapper.workerMiner) { 125 | copy(wrapper.workerMiner[index:], wrapper.workerMiner[index+1:]) 126 | } 127 | 128 | wrapper.workerMiner[len(wrapper.workerMiner)-1] = nil 129 | wrapper.workerMiner = wrapper.workerMiner[:len(wrapper.workerMiner)-1] 130 | } 131 | 132 | type WalletMiner struct { 133 | Wallet string 134 | 135 | // Clients map[workerName]*WorkerMiner 136 | Clients *sync.Map 137 | } 138 | 139 | func (w *WalletMiner) GetOnlineWorkerList() *[]*WorkerMiner { 140 | list := make([]*WorkerMiner, 0) 141 | w.Clients.Range(func(key, value interface{}) bool { 142 | if value.(*WorkerMiner).IsOnline() { 143 | list = append(list, value.(*WorkerMiner)) 144 | } 145 | return true 146 | }) 147 | return &list 148 | } 149 | 150 | func (w *WalletMiner) GetWorkerList() *[]*WorkerMiner { 151 | list := make([]*WorkerMiner, 0) 152 | w.Clients.Range(func(key, value interface{}) bool { 153 | list = append(list, value.(*WorkerMiner)) 154 | return true 155 | }) 156 | return &list 157 | } 158 | 159 | type WorkerMiner struct { 160 | // 最后一次连接时间 161 | ConnectAt time.Time 162 | 163 | // 最后一次提交时间 164 | LastShareAt time.Time 165 | 166 | PoolServer *PoolServer 167 | Identifier *MinerIdentifier 168 | 169 | HashRate int64 170 | TotalShare int64 171 | FeeShare int64 172 | 173 | TimeIntervalShareStats *ShareStatsIntervalMap 174 | 175 | FeeInstance []*FeeStatesClient 176 | CurFeeInstance *FeeStatesClient 177 | 178 | DropUpstream bool 179 | 180 | // 底下的连接对 181 | DownstreamClients *DownstreamClientMutexWrapper 182 | } 183 | 184 | func (m *WorkerMiner) IsOnline() bool { 185 | return len(*m.DownstreamClients.Copy()) > 0 186 | } 187 | 188 | func (m *WorkerMiner) AddShare(d int64) { 189 | atomic.AddInt64(&m.TotalShare, d) 190 | m.TimeIntervalShareStats.AddShare(d) 191 | m.LastShareAt = time.Now() 192 | } 193 | 194 | func (m *WorkerMiner) AddFeeShare(d int64) { 195 | atomic.AddInt64(&m.FeeShare, d) 196 | } 197 | 198 | // GetHashrateInMhs Hash/s -> MH/s 199 | func (m *WorkerMiner) GetHashrateInMhs() float64 { 200 | result, err := strconv.ParseFloat(fmt.Sprintf("%.2f", float64(m.HashRate)/1000000), 64) 201 | if err != nil { 202 | return 0 203 | } 204 | return result 205 | } 206 | 207 | func (m *WorkerMiner) GetID() string { 208 | return m.Identifier.Wallet + "." + m.Identifier.WorkerName 209 | } 210 | 211 | func (m *WorkerMiner) FindFeeInfoByFeeUpstream(upC *UpstreamClient) *FeeStatesClient { 212 | var result *FeeStatesClient 213 | 214 | for _, fee := range m.FeeInstance { 215 | if fee.UpstreamClient == upC { 216 | result = fee 217 | break 218 | } 219 | } 220 | 221 | return result 222 | } 223 | 224 | type ShareStatsIntervalMap struct { 225 | sync.RWMutex 226 | sync.Map 227 | } 228 | 229 | func (s *ShareStatsIntervalMap) AddShare(d int64) { 230 | s.Range(func(_, stats interface{}) bool { 231 | stats.(*ShareStatsInterval).AddShare(d) 232 | return true 233 | }) 234 | } 235 | 236 | func (s *ShareStatsIntervalMap) GetStats(duration time.Duration) *ShareStatsInterval { 237 | val, _ := s.LoadOrStore(duration, NewShareStatsInterval(duration)) 238 | return val.(*ShareStatsInterval) 239 | } 240 | 241 | func (s *ShareStatsIntervalMap) AddStats(stats *ShareStatsInterval) { 242 | defer s.Unlock() 243 | s.Lock() 244 | s.Store(stats.interval, stats) 245 | } 246 | 247 | func (s *ShareStatsIntervalMap) AddStatsSlice(stats *[]*ShareStatsInterval) { 248 | defer s.Unlock() 249 | s.Lock() 250 | for _, interval := range *(stats) { 251 | s.Store(interval.interval, interval) 252 | } 253 | } 254 | 255 | // ShareStatsInterval 一个时间段内的份额统计 256 | type ShareStatsInterval struct { 257 | share int64 258 | interval time.Duration 259 | intervalStartAt time.Time 260 | } 261 | 262 | func NewShareStatsInterval(interval time.Duration) *ShareStatsInterval { 263 | return &ShareStatsInterval{ 264 | interval: interval, 265 | intervalStartAt: time.Now(), 266 | } 267 | } 268 | 269 | func (s *ShareStatsInterval) Update() { 270 | if time.Since(s.intervalStartAt).Seconds() > s.interval.Seconds() { 271 | s.share = 0 272 | s.intervalStartAt = time.Now() 273 | return 274 | } 275 | } 276 | 277 | func (s *ShareStatsInterval) GetShare() int64 { 278 | s.Update() 279 | return s.share 280 | } 281 | 282 | func (s *ShareStatsInterval) AddShare(d int64) { 283 | s.Update() 284 | atomic.AddInt64(&s.share, d) 285 | } 286 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{height:1em;width:1em;margin-top:-9px;display:inline-block;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#31b131}table.dataTable tr.dt-hasChild td.dt-control:before{content:"-";background-color:#d33333}table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important;border-spacing:0}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:auto;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:.85em}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap;justify-content:flex-end}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable>thead>tr>th:active,table.dataTable>thead>tr>td:active{outline:none}table.dataTable>thead>tr>th:not(.sorting_disabled),table.dataTable>thead>tr>td:not(.sorting_disabled){padding-right:30px}table.dataTable>thead .sorting,table.dataTable>thead .sorting_asc,table.dataTable>thead .sorting_desc,table.dataTable>thead .sorting_asc_disabled,table.dataTable>thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable>thead .sorting:before,table.dataTable>thead .sorting:after,table.dataTable>thead .sorting_asc:before,table.dataTable>thead .sorting_asc:after,table.dataTable>thead .sorting_desc:before,table.dataTable>thead .sorting_desc:after,table.dataTable>thead .sorting_asc_disabled:before,table.dataTable>thead .sorting_asc_disabled:after,table.dataTable>thead .sorting_desc_disabled:before,table.dataTable>thead .sorting_desc_disabled:after{position:absolute;bottom:.9em;display:block;opacity:.3}table.dataTable>thead .sorting:before,table.dataTable>thead .sorting_asc:before,table.dataTable>thead .sorting_desc:before,table.dataTable>thead .sorting_asc_disabled:before,table.dataTable>thead .sorting_desc_disabled:before{right:1em;content:"↑"}table.dataTable>thead .sorting:after,table.dataTable>thead .sorting_asc:after,table.dataTable>thead .sorting_desc:after,table.dataTable>thead .sorting_asc_disabled:after,table.dataTable>thead .sorting_desc_disabled:after{right:.5em;content:"↓"}table.dataTable>thead .sorting_asc:before,table.dataTable>thead .sorting_desc:after{opacity:1}table.dataTable>thead .sorting_asc_disabled:before,table.dataTable>thead .sorting_desc_disabled:after{opacity:0}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:before,div.dataTables_scrollBody>table>thead .sorting_asc:before,div.dataTables_scrollBody>table>thead .sorting_desc:before,div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody tr:first-child th,div.dataTables_scrollBody>table>tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}div.dataTables_wrapper div.dataTables_paginate ul.pagination{justify-content:center !important}}table.dataTable.table-sm>thead>tr>th:not(.sorting_disabled){padding-right:20px}table.dataTable.table-sm .sorting:before,table.dataTable.table-sm .sorting_asc:before,table.dataTable.table-sm .sorting_desc:before{top:5px;right:.85em}table.dataTable.table-sm .sorting:after,table.dataTable.table-sm .sorting_asc:after,table.dataTable.table-sm .sorting_desc:after{top:5px}table.table-bordered.dataTable{border-right-width:0}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:1px}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^=col-]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^=col-]:last-child{padding-right:0} 2 | -------------------------------------------------------------------------------- /connection/upstream.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/tls" 7 | "errors" 8 | uuid "github.com/iris-contrib/go.uuid" 9 | log "github.com/sirupsen/logrus" 10 | "golang.org/x/net/proxy" 11 | "net" 12 | "stratumproxy/config" 13 | "strings" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | var ErrUpstreamInvalidUser = errors.New("矿池身份验证失败: 请检查钱包/用户名") 19 | 20 | // UpstreamClient 逻辑 21 | type UpstreamClient struct { 22 | Uuid string 23 | Config config.Upstream 24 | PoolServer *PoolServer 25 | 26 | Connection net.Conn 27 | reader *bufio.Reader 28 | 29 | jobQueue []string 30 | jobQueueLock *sync.RWMutex 31 | 32 | // ProtocolData 记录协议的一些数据 33 | ProtocolData *sync.Map 34 | 35 | WorkerMiner *WorkerMiner 36 | DownstreamClient *DownstreamClient 37 | DownstreamIdentifier MinerIdentifier 38 | 39 | shutdownWaiter *sync.WaitGroup 40 | 41 | Disconnected bool 42 | terminate bool 43 | } 44 | 45 | func (client *UpstreamClient) SetJobQueue(queue []string) { 46 | client.jobQueueLock.Lock() 47 | defer client.jobQueueLock.Unlock() 48 | client.jobQueue = queue 49 | } 50 | 51 | func (client *UpstreamClient) GetJobQueue() *[]string { 52 | client.jobQueueLock.Lock() 53 | defer client.jobQueueLock.Unlock() 54 | return &client.jobQueue 55 | } 56 | 57 | func (client *UpstreamClient) GetJobIndex(job string) int { 58 | client.jobQueueLock.Lock() 59 | defer client.jobQueueLock.Unlock() 60 | 61 | for i, s := range client.jobQueue { 62 | if s == job { 63 | return i 64 | } 65 | } 66 | 67 | return -1 68 | } 69 | 70 | func (client *UpstreamClient) HasJob(job string) bool { 71 | return client.GetJobIndex(job) != -1 72 | } 73 | 74 | func (client *UpstreamClient) AddJob(job string) { 75 | client.jobQueueLock.Lock() 76 | defer client.jobQueueLock.Unlock() 77 | 78 | if len(client.jobQueue)+1 > 80 { 79 | copy(client.jobQueue, client.jobQueue[1:]) 80 | client.jobQueue = client.jobQueue[:len(client.jobQueue)-1] 81 | } 82 | 83 | client.jobQueue = append(client.jobQueue, job) 84 | } 85 | 86 | // DoneJob 把已经提交过的放到数组的第一位等待回收 87 | func (client *UpstreamClient) DoneJob(job string) { 88 | // 不能在这里 wait 因为要这个方法本来就是阻塞的 89 | index := client.GetJobIndex(job) 90 | if index == -1 { 91 | return 92 | } 93 | 94 | client.jobQueueLock.Lock() 95 | defer client.jobQueueLock.Unlock() 96 | 97 | tmp := client.jobQueue[index] 98 | copy(client.jobQueue[index:], client.jobQueue[index+1:]) 99 | client.jobQueue[len(client.jobQueue)-1] = tmp 100 | } 101 | 102 | func (client *UpstreamClient) ReadOnce(timeout int) ([]byte, error) { 103 | err := client.Connection.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) 104 | if err != nil { 105 | return []byte(""), err 106 | } 107 | return client.reader.ReadBytes('\n') 108 | } 109 | 110 | func (client *UpstreamClient) Write(in []byte) error { 111 | if !strings.HasSuffix(string(in), "\n") { 112 | in = append(in, '\n') 113 | } 114 | err := client.Connection.SetWriteDeadline(time.Now().Add(8 * time.Second)) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | _, err = client.Connection.Write(in) 120 | return err 121 | } 122 | 123 | func (client *UpstreamClient) AuthInitial(id MinerIdentifier) error { 124 | err := client.PoolServer.Protocol.InitialUpstreamAuth(client, id) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | // 启动同步携程 130 | go client.SyncTick() 131 | 132 | return nil 133 | } 134 | 135 | func (client *UpstreamClient) Shutdown() { 136 | if client.PoolServer.Err != nil { 137 | client.terminate = true 138 | } 139 | 140 | if client.DownstreamClient != nil && client.DownstreamClient.Disconnected { 141 | client.terminate = true 142 | } 143 | 144 | if client.Disconnected { 145 | return 146 | } 147 | client.Disconnected = true 148 | 149 | _ = client.Connection.Close() 150 | log.Debugf("[%s][%s][shutdown] 上游已关闭!", client.PoolServer.Config.Name, client.Uuid) 151 | 152 | if client.terminate { 153 | return 154 | } 155 | 156 | log.Infof("[%s][%s][shutdown] 上游开始自动重连...", client.PoolServer.Config.Name, client.Uuid) 157 | client.Reconnect() 158 | log.Infof("[%s][%s][shutdown] 上游自动重连成功!", client.PoolServer.Config.Name, client.Uuid) 159 | 160 | return 161 | } 162 | 163 | func (client *UpstreamClient) SyncTick() { 164 | client.shutdownWaiter.Add(1) 165 | client.Disconnected = false 166 | 167 | defer func() { 168 | client.shutdownWaiter.Done() 169 | client.Shutdown() 170 | PanicHandler() 171 | }() 172 | 173 | for { 174 | if client.terminate { 175 | return 176 | } 177 | if client.Disconnected { 178 | return 179 | } 180 | err := client.processRead() 181 | if err != nil { 182 | if strings.Contains(err.Error(), "use of closed") { 183 | return 184 | } 185 | log.Warnf("[%s][%s][SyncTick] 读取上游数据失败: %s", client.PoolServer.Config.Name, client.Uuid, err) 186 | return 187 | } 188 | } 189 | } 190 | 191 | func (client *UpstreamClient) processRead() error { 192 | data, err := client.ReadOnce(30) 193 | 194 | if err != nil { 195 | return err 196 | } 197 | 198 | if len(data) > 0 { 199 | log.Tracef("[%s][processRead] 接收到上游数据: %s", client.Connection.RemoteAddr(), data) 200 | UpstreamInjector.processMsg(client, data) 201 | } 202 | 203 | return nil 204 | } 205 | 206 | func (client *UpstreamClient) Reconnect() { 207 | // 无论如何都自动重连 208 | for true { 209 | err := client.CreateConn() 210 | if err != nil { 211 | log.Warnf("[%s][Reconnect] 连接到上游服务器失败: %s", client.Uuid, err) 212 | time.Sleep(2 * time.Second) 213 | continue 214 | } 215 | 216 | err = client.PoolServer.Protocol.InitialUpstreamConn(client) 217 | if err != nil { 218 | log.Warnf("[%s][Reconnect] 与上游矿池握手失败: %s", client.Uuid, err) 219 | time.Sleep(2 * time.Second) 220 | continue 221 | } 222 | 223 | if client.DownstreamIdentifier.Wallet != "" { 224 | err = client.AuthInitial(client.DownstreamIdentifier) 225 | if err != nil { 226 | log.Warnf("[%s][Reconnect] 无法登录上游矿池: %s", client.Uuid, err) 227 | time.Sleep(2 * time.Second) 228 | continue 229 | } 230 | } else { 231 | log.Warnf("[%s][Reconnect] 上游不存在认证信息,取消重连!", client.Uuid) 232 | if client.DownstreamClient != nil { 233 | client.DownstreamClient.Shutdown() 234 | } 235 | client.terminate = true 236 | return 237 | } 238 | 239 | break 240 | } 241 | } 242 | 243 | func (client *UpstreamClient) CreateConn() error { 244 | var c net.Conn 245 | dialer := &net.Dialer{ 246 | Timeout: 12 * time.Second, 247 | Resolver: &net.Resolver{ 248 | PreferGo: true, 249 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 250 | d := net.Dialer{ 251 | Timeout: 12 * time.Second, 252 | } 253 | return d.DialContext(ctx, "udp", "8.8.4.4:53") 254 | }, 255 | }, 256 | } 257 | 258 | tlsConfig := &tls.Config{InsecureSkipVerify: true} 259 | if client.Config.Tls { 260 | if client.Config.Proxy != "" { 261 | proxyDialer, err := proxy.SOCKS5("tcp", client.Config.Proxy, nil, proxy.Direct) 262 | if err != nil { 263 | return err 264 | } 265 | c, err = proxyDialer.Dial("tcp", client.Config.Address) 266 | if err != nil { 267 | return err 268 | } 269 | c = tls.Client(c, tlsConfig) 270 | } else { 271 | var err error 272 | c, err = tls.Dial("tcp", client.Config.Address, tlsConfig) 273 | if err != nil { 274 | return err 275 | } 276 | } 277 | } else { 278 | if client.Config.Proxy != "" { 279 | proxyDialer, err := proxy.SOCKS5("tcp", client.Config.Proxy, nil, proxy.Direct) 280 | if err != nil { 281 | return err 282 | } 283 | c, err = proxyDialer.Dial("tcp", client.Config.Address) 284 | if err != nil { 285 | return err 286 | } 287 | } else { 288 | var err error 289 | c, err = dialer.Dial("tcp", client.Config.Address) 290 | if err != nil { 291 | return err 292 | } 293 | } 294 | } 295 | 296 | client.Connection = c 297 | client.reader = bufio.NewReader(c) 298 | 299 | return nil 300 | } 301 | 302 | func NewUpstreamClient(pool *PoolServer, cfg config.Upstream) (*UpstreamClient, error) { 303 | id, _ := uuid.NewV4() 304 | client := &UpstreamClient{ 305 | Uuid: id.String(), 306 | Config: cfg, 307 | PoolServer: pool, 308 | 309 | jobQueue: make([]string, 0, 84), 310 | jobQueueLock: &sync.RWMutex{}, 311 | 312 | ProtocolData: &sync.Map{}, 313 | 314 | shutdownWaiter: &sync.WaitGroup{}, 315 | } 316 | 317 | err := client.CreateConn() 318 | if err != nil { 319 | return nil, errors.New("连接失败 " + err.Error()) 320 | } 321 | 322 | err = client.PoolServer.Protocol.InitialUpstreamConn(client) 323 | if err != nil { 324 | return nil, errors.New("初始化连接失败 " + err.Error()) 325 | } 326 | 327 | return client, nil 328 | } 329 | -------------------------------------------------------------------------------- /webui/assets/plugins/datatables-buttons/css/buttons.bootstrap4.min.css: -------------------------------------------------------------------------------- 1 | @keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}div.dt-button-collection{position:absolute;z-index:2001;background-color:white;border:1px solid rgba(0, 0, 0, 0.15);border-radius:4px;box-shadow:0 6px 12px rgba(0, 0, 0, 0.175);padding:.5rem 0;width:200px}div.dt-button-collection div.dropdown-menu{position:relative;display:block;z-index:2002;min-width:100%;background-color:transparent;border:none;box-shadow:none;padding:0;border-radius:0}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-collection.fixed:before,div.dt-button-collection.fixed:after{display:none}div.dt-button-collection .btn-group{flex:1 1 auto}div.dt-button-collection .dt-button{min-width:200px}div.dt-button-collection div.dt-btn-split-wrapper{width:100%;padding-left:5px;padding-right:5px}div.dt-button-collection button.dt-btn-split-drop-button{width:100%;color:#212529;border:none;background-color:white;border-radius:0px;margin-left:0px !important}div.dt-button-collection button.dt-btn-split-drop-button:focus{border:none;border-radius:0px;outline:none}div.dt-button-collection button.dt-btn-split-drop-button:hover{background-color:#e9ecef}div.dt-button-collection button.dt-btn-split-drop-button:active{background-color:#007bff !important}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.btn.processing,div.dt-buttons div.btn.processing,div.dt-buttons a.btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons button.btn.processing:after,div.dt-buttons div.btn.processing:after,div.dt-buttons a.btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid #282828;border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-buttons div.btn-group{position:initial}div.dt-btn-split-wrapper:active:not(.disabled) button,div.dt-btn-split-wrapper.active:not(.disabled) button{background-color:#5a6268;border-color:#545b62}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop{box-shadow:none;background-color:#6c757d;border-color:#6c757d}div.dt-btn-split-wrapper:active:not(.disabled) button:hover,div.dt-btn-split-wrapper.active:not(.disabled) button:hover{background-color:#5a6268;border-color:#545b62}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group{border-radius:4px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child{border-top-left-radius:0px !important;border-bottom-left-radius:0px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child{border-top-right-radius:0px !important;border-bottom-right-radius:0px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child{border-top-left-radius:4px !important;border-bottom-left-radius:4px !important;border-top-right-radius:4px !important;border-bottom-right-radius:4px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child{border:1px solid #6c757d}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper{border:none}div.dt-button-collection div.btn-group{border-radius:4px !important}div.dt-button-collection div.btn-group button{border-radius:4px}div.dt-button-collection div.btn-group button:last-child{border-top-left-radius:0px !important;border-bottom-left-radius:0px !important}div.dt-button-collection div.btn-group button:first-child{border-top-right-radius:0px !important;border-bottom-right-radius:0px !important}div.dt-button-collection div.btn-group button:last-child:first-child{border-top-left-radius:4px !important;border-bottom-left-radius:4px !important;border-top-right-radius:4px !important;border-bottom-right-radius:4px !important}div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child{border:1px solid #6c757d}div.dt-button-collection div.btn-group div.dt-btn-split-wrapper{border:none}span.dt-button-spacer.bar:empty{height:inherit}div.dt-button-collection span.dt-button-spacer{padding-left:1rem !important;text-align:left} 2 | -------------------------------------------------------------------------------- /webui/assets/plugins/jquery-ui/jquery-ui.theme.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.13.0 - 2021-10-07 2 | * http://jqueryui.com 3 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 4 | 5 | .ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;-ms-filter:"alpha(opacity=70)";font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;-ms-filter:"alpha(opacity=35)";background-image:none}.ui-state-disabled .ui-icon{-ms-filter:"alpha(opacity=35)"}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank.ui-icon-blank.ui-icon-blank{background-image:none}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.003;-ms-filter:Alpha(Opacity=.3)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} -------------------------------------------------------------------------------- /webui/template/page/dashboard.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "title" }} 仪表盘 {{ end }} 2 | 3 | {{ define "header_page" }} 4 | 9 | {{ end }} 10 | 11 | {{ define "section_title" }} 仪表盘 {{ end }} 12 | 13 | {{ define "section" }} 14 |
    15 |
    16 |
    17 |
    18 | 19 |
    20 | 运行时间 21 | 22 |
    23 |
    24 |
    25 |
    26 | 启动自: {{ .StartTimeStr }} 27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 | 34 |
    35 | 软件版本 36 | {{ .Version }} 37 |
    38 |
    39 |
    40 |
    41 | 构建时间: {{ .BuildTime }} 42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 | 49 |
    50 | 已配置矿池 51 | {{ .PoolServersCount }} 52 | {{ $offline := sub .PoolServersCount (len .OnlinePoolServers) }} 53 |
    54 | {{ $totalSafe := .PoolServersCount }} 55 | {{ if eq $totalSafe 0 }} 56 | {{ $totalSafe = add1 $totalSafe }} 57 | {{ end }} 58 |
    59 |
    60 |
    61 |
    62 | {{ len .OnlinePoolServers }} 在线 63 | {{ $offline }} 离线 64 |
    65 |
    66 |
    67 |
    68 |
    69 |
    70 | 71 |
    72 | 已注册矿工 73 | 74 | {{ $offline = sub .MinerCount .OnlineMinerCount }} 75 | {{ .MinerCount }} 76 |
    77 | {{ $totalSafe = .MinerCount }} 78 | {{ if eq $totalSafe 0 }} 79 | {{ $totalSafe = add1 $totalSafe }} 80 | {{ end }} 81 |
    82 |
    83 |
    84 |
    85 | {{ .OnlineMinerCount }} 在线 86 | {{ $offline }} 离线 87 |
    88 |
    89 |
    90 |
    91 |
    92 |
    93 | 94 |
    95 | {{ if eq (len .OnlinePoolServers) 0 }} 96 |
    97 |
    98 | 99 |
    100 | 还未配置矿池 101 | 102 | 点我创建 103 | 104 |
    105 |
    106 |
    107 | {{ end }} 108 | 109 | {{ range $i, $server := .OnlinePoolServers }} 110 |
    111 | {{ if $server.Err }} 112 |
    113 | {{ else }} 114 |
    115 | {{ end }} 116 |
    117 |

    {{ $server.Config.Name }}

    118 |
    119 |
    120 | 121 | 122 | 123 | 124 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | {{ if f_greater $server.Config.FeeConfig.Pct 0.0 }} 166 | {{ if eq $server.UserFeeShare 0 }} 167 | 168 | {{ else }} 169 | {{ $safeDiv := $server.GlobalShareStats }} 170 | {{ if eq $safeDiv 0 }} 171 | {{ $safeDiv = add1 $safeDiv }} 172 | {{ end }} 173 | 174 | {{ end }} 175 | {{ else }} 176 | 177 | {{ end }} 178 | 179 | 180 | 181 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 203 | 204 | 205 |
    状态 125 | {{ if $server.Err }} 126 | 离线: {{ $server.Err }} 127 | {{ else }} 128 | 在线 129 | {{ end }} 130 |
    币种{{ upper $server.Config.Coin }}
    本地算力{{ format_pool_hashrate $server }}
    矿工数 143 | {{ $total := get_pool_worker_list $server | len }} 144 | {{ $online := get_pool_online_worker_list $server | len }} 145 | {{ $offline := sub $total $online }} 146 | {{ $total }} 共计 147 | {{ $online }} 在线 148 | {{ $offline }} 离线 149 |
    150 | {{ $totalSafe := $total }} 151 | {{ if eq $totalSafe 0 }} 152 | {{ $totalSafe = add1 $totalSafe }} 153 | {{ end }} 154 |
    155 |
    156 |
    157 |
    提交份额{{ $server.GlobalShareStats }}
    明抽份额0% / 0{{ round (divf $server.UserFeeShare $safeDiv | mulf 100) 3 }}% / {{ $server.UserFeeShare }}0% / 0
    SSL/TLS 182 | {{ if $server.Config.Connection.Tls.Enable }} 183 | 启用 184 | {{ else }} 185 | 未启用 186 | {{ end }} 187 |
    转发矿池{{ $server.Config.Upstream.Address }}
    监听地址 196 | 请以实际公网IP为准 197 | {{ if $server.Config.Connection.Tls.Enable }} 198 | stratum+ssl://{{ $server.Config.Connection.Bind }} 199 | {{ else }} 200 | stratum+tcp://{{ $server.Config.Connection.Bind }} 201 | {{ end }} 202 |
    206 |
    207 | 224 |
    225 |
    226 | {{- end }} 227 |
    228 |
    229 | {{ end }} 230 | 231 | {{ define "script_section" }} 232 | 233 | 242 | {{ end }} 243 | --------------------------------------------------------------------------------