From 6a7ec5ab36aec0535f2e5aa06bd6ecc9a801dd08 Mon Sep 17 00:00:00 2001 From: mike76-dev Date: Mon, 18 Mar 2024 16:29:22 +0100 Subject: [PATCH] Add /status endpoint --- api/api.go | 15 +++++++++++++ api/client.go | 6 +++++ api/server.go | 50 +++++++++++++++++++++++++++++++++++++++++ cmd/hsc/api.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/api/api.go b/api/api.go index 8dbceed..0f9b87e 100644 --- a/api/api.go +++ b/api/api.go @@ -11,6 +11,21 @@ type GatewayPeer struct { Version string `json:"version"` } +// Balance combines mature and immature values. +type Balance struct { + Siacoins types.Currency `json:"siacoins"` + ImmatureSiacoins types.Currency `json:"immatureSiacoins"` +} + +// NodeStatusResponse is the response type for /node/status. +type NodeStatusResponse struct { + Version string `json:"version"` + Height uint64 `json:"heightMainnet"` + HeightZen uint64 `json:"heightZen"` + Balance Balance `json:"balanceMainnet"` + BalanceZen Balance `json:"balanceZen"` +} + // ConsensusTipResponse is the response type for /consensus/tip. type ConsensusTipResponse struct { Network string `json:"network"` diff --git a/api/client.go b/api/client.go index ab6cbef..1d857f2 100644 --- a/api/client.go +++ b/api/client.go @@ -15,6 +15,12 @@ type Client struct { c jape.Client } +// NodeStatus returns the status of the node. +func (c *Client) NodeStatus() (resp NodeStatusResponse, err error) { + err = c.c.GET("/node/status", &resp) + return +} + // TxpoolTransactions returns all transactions in the transaction pool. func (c *Client) TxpoolTransactions(network string) (txns []types.Transaction, v2txns []types.V2Transaction, err error) { var resp TxpoolTransactionsResponse diff --git a/api/server.go b/api/server.go index c8284c8..11b3822 100644 --- a/api/server.go +++ b/api/server.go @@ -8,6 +8,7 @@ import ( "time" "github.com/mike76-dev/hostscore/hostdb" + "github.com/mike76-dev/hostscore/internal/build" "github.com/mike76-dev/hostscore/internal/walletutil" "go.sia.tech/core/consensus" "go.sia.tech/core/types" @@ -35,6 +36,53 @@ func isSynced(s *syncer.Syncer) bool { return count >= 5 } +func (s *server) nodeStatusHandler(jc jape.Context) { + height := s.cm.TipState().Index.Height + heightZen := s.cmZen.TipState().Index.Height + + scos, _, err := s.w.UnspentOutputs("mainnet") + if jc.Check("couldn't load Mainnet outputs", err) != nil { + return + } + + var sc, immature types.Currency + for _, sco := range scos { + if height >= sco.MaturityHeight { + sc = sc.Add(sco.SiacoinOutput.Value) + } else { + immature = immature.Add(sco.SiacoinOutput.Value) + } + } + + scosZen, _, err := s.w.UnspentOutputs("zen") + if jc.Check("couldn't load Zen outputs", err) != nil { + return + } + + var scZen, immatureZen types.Currency + for _, sco := range scosZen { + if heightZen >= sco.MaturityHeight { + scZen = scZen.Add(sco.SiacoinOutput.Value) + } else { + immatureZen = immatureZen.Add(sco.SiacoinOutput.Value) + } + } + + jc.Encode(NodeStatusResponse{ + Version: build.NodeVersion, + Height: height, + HeightZen: heightZen, + Balance: Balance{ + Siacoins: sc, + ImmatureSiacoins: immature, + }, + BalanceZen: Balance{ + Siacoins: scZen, + ImmatureSiacoins: immatureZen, + }, + }) +} + func (s *server) consensusNetworkHandler(jc jape.Context) { var network string if jc.DecodeForm("network", &network) != nil { @@ -319,6 +367,8 @@ func NewServer(cm *chain.Manager, cmZen *chain.Manager, s *syncer.Syncer, sZen * hdb: hdb, } return jape.Mux(map[string]jape.Handler{ + "GET /node/status": srv.nodeStatusHandler, + "GET /consensus/network": srv.consensusNetworkHandler, "GET /consensus/tip": srv.consensusTipHandler, "GET /consensus/tipstate": srv.consensusTipStateHandler, diff --git a/cmd/hsc/api.go b/cmd/hsc/api.go index 0400a86..f4d6c72 100644 --- a/cmd/hsc/api.go +++ b/cmd/hsc/api.go @@ -14,12 +14,15 @@ import ( client "github.com/mike76-dev/hostscore/api" "github.com/mike76-dev/hostscore/external" "github.com/mike76-dev/hostscore/hostdb" + "github.com/mike76-dev/hostscore/internal/build" rhpv2 "go.sia.tech/core/rhp/v2" rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.uber.org/zap" ) +var lowBalanceThreshold = types.Siacoins(200) + type APIResponse struct { Status string `json:"status"` Message string `json:"message"` @@ -49,6 +52,22 @@ type benchmarksResponse struct { Benchmarks []hostdb.BenchmarkHistory `json:"benchmarks"` } +type nodeStatus struct { + Location string `json:"location"` + Status bool `json:"status"` + Version string `json:"version"` + Height uint64 `json:"heightMainnet"` + HeightZen uint64 `json:"heightZen"` + Balance string `json:"balanceMainnet"` + BalanceZen string `json:"balanceZen"` +} + +type statusResponse struct { + APIResponse + Version string `json:"version"` + Nodes []nodeStatus `json:"nodes"` +} + type nodeInteractions struct { Uptime time.Duration `json:"uptime"` Downtime time.Duration `json:"downtime"` @@ -171,6 +190,9 @@ func (api *portalAPI) buildHTTPRoutes() { router.GET("/benchmarks", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { api.benchmarksHandler(w, req, ps) }) + router.GET("/status", func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { + api.statusHandler(w, req, ps) + }) api.mu.Lock() api.router = *router @@ -461,6 +483,45 @@ func (api *portalAPI) benchmarksHandler(w http.ResponseWriter, req *http.Request }) } +func balanceStatus(balance types.Currency) string { + if balance.IsZero() { + return "empty" + } + if balance.Cmp(lowBalanceThreshold) < 0 { + return "low" + } + return "ok" +} + +func (api *portalAPI) statusHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + var nodes []nodeStatus + for n, c := range api.clients { + status, err := c.NodeStatus() + if err != nil { + api.log.Error("couldn't get node status", zap.String("node", n), zap.Error(err)) + nodes = append(nodes, nodeStatus{ + Location: n, + Status: false, + }) + } else { + nodes = append(nodes, nodeStatus{ + Location: n, + Status: true, + Version: status.Version, + Height: status.Height, + HeightZen: status.HeightZen, + Balance: balanceStatus(status.Balance.Siacoins), + BalanceZen: balanceStatus(status.BalanceZen.Siacoins), + }) + } + } + writeJSON(w, statusResponse{ + APIResponse: APIResponse{Status: "ok"}, + Version: build.ClientVersion, + Nodes: nodes, + }) +} + func writeJSON(w http.ResponseWriter, obj interface{}) { w.Header().Set("Content-Type", "application/json; charset=utf-8") err := json.NewEncoder(w).Encode(obj)