786 lines
24 KiB
Go
786 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
|
|
"github.com/ake-persson/mapslice-json"
|
|
"github.com/gorilla/mux"
|
|
"log"
|
|
"lukechampine.com/uint128"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func encodeJson(r *http.Request, d any) ([]byte, error) {
|
|
if strings.Index(strings.ToLower(r.Header.Get("user-agent")), "mozilla") != -1 {
|
|
return json.MarshalIndent(d, "", " ")
|
|
} else {
|
|
return json.Marshal(d)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
client.SetClientSettings(os.Getenv("MONEROD_RPC_URL"))
|
|
db, err := database.NewDatabase(os.Args[1])
|
|
if err != nil {
|
|
log.Panic(err)
|
|
}
|
|
defer db.Close()
|
|
api, err := api.New(db, os.Getenv("API_FOLDER"))
|
|
if err != nil {
|
|
log.Panic(err)
|
|
}
|
|
|
|
serveMux := mux.NewRouter()
|
|
serveMux.HandleFunc("/api/pool_info", func(writer http.ResponseWriter, request *http.Request) {
|
|
tip := api.GetDatabase().GetChainTip()
|
|
|
|
blockCount := 0
|
|
uncleCount := 0
|
|
|
|
var windowDifficulty uint128.Uint128
|
|
|
|
miners := make(map[uint64]uint64)
|
|
|
|
for b := range api.GetDatabase().GetBlocksInWindow(&tip.Height, 0) {
|
|
blockCount++
|
|
|
|
if _, ok := miners[b.MinerId]; !ok {
|
|
miners[b.MinerId] = 0
|
|
}
|
|
miners[b.MinerId]++
|
|
|
|
windowDifficulty = windowDifficulty.Add(b.Difficulty.Uint128)
|
|
|
|
for u := range api.GetDatabase().GetUnclesByParentId(b.Id) {
|
|
//TODO: check this check is correct :)
|
|
if (tip.Height - u.Block.Height) > p2pool.PPLNSWindow {
|
|
continue
|
|
}
|
|
|
|
uncleCount++
|
|
if _, ok := miners[u.Block.MinerId]; !ok {
|
|
miners[u.Block.MinerId] = 0
|
|
}
|
|
miners[u.Block.MinerId]++
|
|
|
|
windowDifficulty = windowDifficulty.Add(u.Block.Difficulty.Uint128)
|
|
}
|
|
}
|
|
|
|
type totalKnownResult struct {
|
|
blocksFound uint64
|
|
minersKnown uint64
|
|
}
|
|
|
|
totalKnown := cacheResult(CacheTotalKnownBlocksAndMiners, time.Second*15, func() any {
|
|
result := &totalKnownResult{}
|
|
if err := api.GetDatabase().Query("SELECT (SELECT COUNT(*) FROM blocks WHERE main_found = 'y') + (SELECT COUNT(*) FROM uncles WHERE main_found = 'y') as found, COUNT(*) as miners FROM (SELECT miner FROM blocks GROUP BY miner UNION DISTINCT SELECT miner FROM uncles GROUP BY miner) all_known_miners;", func(row database.RowScanInterface) error {
|
|
return row.Scan(&result.blocksFound, &result.minersKnown)
|
|
}); err != nil {
|
|
return nil
|
|
}
|
|
|
|
return result
|
|
}).(*totalKnownResult)
|
|
|
|
poolBlocks, _ := api.GetPoolBlocks()
|
|
|
|
poolStats, _ := api.GetPoolStats()
|
|
|
|
globalDiff := tip.Template.Difficulty
|
|
|
|
currentEffort := float64(uint128.From64(poolStats.PoolStatistics.TotalHashes-poolBlocks[0].TotalHashes).Mul64(100000).Div(globalDiff.Uint128).Lo) / 1000
|
|
|
|
if currentEffort <= 0 {
|
|
currentEffort = 0
|
|
}
|
|
|
|
var blockEfforts mapslice.MapSlice
|
|
for i, b := range poolBlocks {
|
|
if i < (len(poolBlocks)-1) && b.TotalHashes > 0 {
|
|
blockEfforts = append(blockEfforts, mapslice.MapItem{
|
|
Key: b.Hash.String(),
|
|
Value: float64((b.TotalHashes-poolBlocks[i+1].TotalHashes)*100) / float64(b.Difficulty),
|
|
})
|
|
}
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
if buf, err := encodeJson(request, poolInfoResult{
|
|
SideChain: poolInfoResultSideChain{
|
|
Id: tip.Id,
|
|
Height: tip.Height,
|
|
Difficulty: tip.Difficulty,
|
|
Timestamp: tip.Timestamp,
|
|
Effort: poolInfoResultSideChainEffort{
|
|
Current: currentEffort,
|
|
Average: func() (result float64) {
|
|
for _, e := range blockEfforts {
|
|
result += e.Value.(float64)
|
|
}
|
|
return
|
|
}() / float64(len(blockEfforts)),
|
|
Last: blockEfforts,
|
|
},
|
|
Window: poolInfoResultSideChainWindow{
|
|
Miners: len(miners),
|
|
Blocks: blockCount,
|
|
Uncles: uncleCount,
|
|
Weight: types.Difficulty{Uint128: windowDifficulty},
|
|
},
|
|
WindowSize: p2pool.PPLNSWindow,
|
|
BlockTime: p2pool.BlockTime,
|
|
UnclePenalty: p2pool.UnclePenalty,
|
|
Found: totalKnown.blocksFound,
|
|
Miners: totalKnown.minersKnown,
|
|
},
|
|
MainChain: poolInfoResultMainChain{
|
|
Id: tip.Template.Id,
|
|
Height: tip.Main.Height,
|
|
Difficulty: tip.Template.Difficulty,
|
|
BlockTime: monero.BlockTime,
|
|
},
|
|
}); err != nil {
|
|
log.Panic(err)
|
|
} else {
|
|
_, _ = writer.Write(buf)
|
|
}
|
|
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/miner_info/{miner:^[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
minerId := mux.Vars(request)["miner"]
|
|
var miner *database.Miner
|
|
if len(minerId) > 10 && minerId[0] == '4' {
|
|
miner = api.GetDatabase().GetMinerByAddress(minerId)
|
|
}
|
|
|
|
if miner == nil {
|
|
if i, err := strconv.Atoi(minerId); err == nil {
|
|
miner = api.GetDatabase().GetMiner(uint64(i))
|
|
}
|
|
}
|
|
|
|
if miner == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
var blockData struct {
|
|
Count uint64
|
|
LastHeight uint64
|
|
}
|
|
_ = api.GetDatabase().Query("SELECT COUNT(*) as count, MAX(height) as last_height FROM blocks WHERE blocks.miner = $1;", func(row database.RowScanInterface) error {
|
|
return row.Scan(&blockData.Count, &blockData.LastHeight)
|
|
}, miner.Id())
|
|
var uncleData struct {
|
|
Count uint64
|
|
LastHeight uint64
|
|
}
|
|
_ = api.GetDatabase().Query("SELECT COUNT(*) as count, MAX(parent_height) as last_height FROM uncles WHERE uncles.miner = $1;", func(row database.RowScanInterface) error {
|
|
return row.Scan(&uncleData.Count, &uncleData.LastHeight)
|
|
}, miner.Id())
|
|
|
|
var lastShareHeight uint64
|
|
var lastShareTimestamp uint64
|
|
if blockData.Count > 0 && blockData.LastHeight > lastShareHeight {
|
|
lastShareHeight = blockData.LastHeight
|
|
}
|
|
if uncleData.Count > 0 && uncleData.LastHeight > lastShareHeight {
|
|
lastShareHeight = uncleData.LastHeight
|
|
}
|
|
|
|
if lastShareHeight > 0 {
|
|
lastShareTimestamp = api.GetDatabase().GetBlockByHeight(lastShareHeight).Timestamp
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, minerInfoResult{
|
|
Id: miner.Id(),
|
|
Address: miner.MoneroAddress(),
|
|
Shares: struct {
|
|
Blocks uint64 `json:"blocks"`
|
|
Uncles uint64 `json:"uncles"`
|
|
}{Blocks: blockData.Count, Uncles: uncleData.Count},
|
|
LastShareHeight: lastShareHeight,
|
|
LastShareTimestamp: lastShareTimestamp,
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/shares_in_window/{miner:^[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
minerId := mux.Vars(request)["miner"]
|
|
var miner *database.Miner
|
|
if len(minerId) > 10 && minerId[0] == '4' {
|
|
miner = api.GetDatabase().GetMinerByAddress(minerId)
|
|
}
|
|
|
|
if miner == nil {
|
|
if i, err := strconv.Atoi(minerId); err == nil {
|
|
miner = api.GetDatabase().GetMiner(uint64(i))
|
|
}
|
|
}
|
|
|
|
if miner == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
params := request.URL.Query()
|
|
|
|
window := uint64(p2pool.PPLNSWindow)
|
|
if params.Has("window") {
|
|
if i, err := strconv.Atoi(params.Get("window")); err == nil {
|
|
if i <= p2pool.PPLNSWindow*4 {
|
|
window = uint64(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
var from *uint64
|
|
if params.Has("from") {
|
|
if i, err := strconv.Atoi(params.Get("from")); err == nil {
|
|
if i >= 0 {
|
|
i := uint64(i)
|
|
from = &i
|
|
}
|
|
}
|
|
}
|
|
|
|
result := make([]*sharesInWindowResult, 0)
|
|
|
|
for block := range api.GetDatabase().GetBlocksByMinerIdInWindow(miner.Id(), from, window) {
|
|
s := &sharesInWindowResult{
|
|
Id: block.Id,
|
|
Height: block.Height,
|
|
Timestamp: block.Timestamp,
|
|
Weight: block.Difficulty,
|
|
}
|
|
|
|
for u := range api.GetDatabase().GetUnclesByParentId(block.Id) {
|
|
uncleWeight := u.Block.Difficulty.Mul64(p2pool.UnclePenalty).Div64(100)
|
|
s.Uncles = append(s.Uncles, sharesInWindowResultUncle{
|
|
Id: u.Block.Id,
|
|
Height: u.Block.Height,
|
|
Weight: types.Difficulty{Uint128: uncleWeight},
|
|
})
|
|
s.Weight.Uint128 = s.Weight.Add(uncleWeight)
|
|
}
|
|
|
|
result = append(result, s)
|
|
}
|
|
|
|
for uncle := range api.GetDatabase().GetUnclesByMinerIdInWindow(miner.Id(), from, window) {
|
|
s := &sharesInWindowResult{
|
|
Parent: &sharesInWindowResultParent{
|
|
Id: uncle.ParentId,
|
|
Height: uncle.ParentHeight,
|
|
},
|
|
Id: uncle.Block.Id,
|
|
Height: uncle.Block.Height,
|
|
Timestamp: uncle.Block.Timestamp,
|
|
Weight: types.Difficulty{Uint128: uncle.Block.Difficulty.Mul64(100 - p2pool.UnclePenalty).Div64(100)},
|
|
}
|
|
result = append(result, s)
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, result)
|
|
_, _ = writer.Write(buf)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/payouts/{miner:^[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
minerId := mux.Vars(request)["miner"]
|
|
var miner *database.Miner
|
|
if len(minerId) > 10 && minerId[0] == '4' {
|
|
miner = api.GetDatabase().GetMinerByAddress(minerId)
|
|
}
|
|
|
|
if miner == nil {
|
|
if i, err := strconv.Atoi(minerId); err == nil {
|
|
miner = api.GetDatabase().GetMiner(uint64(i))
|
|
}
|
|
}
|
|
|
|
if miner == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
params := request.URL.Query()
|
|
|
|
var limit uint64 = 10
|
|
|
|
if params.Has("search_limit") {
|
|
if i, err := strconv.Atoi(params.Get("search_limit")); err == nil {
|
|
limit = uint64(i)
|
|
}
|
|
}
|
|
|
|
payouts := make([]*database.Payout, 0)
|
|
|
|
for payout := range api.GetDatabase().GetPayoutsByMinerId(miner.Id(), limit) {
|
|
payouts = append(payouts, payout)
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, payouts)
|
|
_, _ = writer.Write(buf)
|
|
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/redirect/block/{main_height:^[0-9]+|.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
http.Redirect(writer, request, fmt.Sprintf("https://xmrchain.net/block/%d", utils.DecodeBinaryNumber(mux.Vars(request)["main_height"])), http.StatusFound)
|
|
})
|
|
serveMux.HandleFunc("/api/redirect/transaction/{tx_id:^.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
http.Redirect(writer, request, fmt.Sprintf("https://xmrchain.net/tx/%s", utils.DecodeHexBinaryNumber(mux.Vars(request)["tx_id"])), http.StatusFound)
|
|
})
|
|
serveMux.HandleFunc("/api/redirect/block/{coinbase:[0-9]+|.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
c := api.GetDatabase().GetBlocksByQuery("WHERE height = $1 AND main_found IS TRUE", utils.DecodeBinaryNumber(mux.Vars(request)["coinbase"]))
|
|
defer func() {
|
|
for range c {
|
|
|
|
}
|
|
}()
|
|
b := <-c
|
|
if b == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
http.Redirect(writer, request, fmt.Sprintf("https://xmrchain.net/tx/%s", b.Coinbase.Id.String()), http.StatusFound)
|
|
})
|
|
serveMux.HandleFunc("/api/redirect/share/{height:^[0-9]+|.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
c := utils.DecodeBinaryNumber(mux.Vars(request)["height"])
|
|
|
|
blockHeight := c >> 16
|
|
blockIdStart := c & 0xFFFF
|
|
|
|
var b database.BlockInterface
|
|
var b1 = <-api.GetDatabase().GetBlocksByQuery("WHERE height = $1 AND id ILIKE $2 LIMIT 1;", blockHeight, hex.EncodeToString([]byte{byte((blockIdStart >> 8) & 0xFF), byte(blockIdStart & 0xFF)})+"%")
|
|
if b1 == nil {
|
|
b2 := <-api.GetDatabase().GetUncleBlocksByQuery("WHERE height = $1 AND id ILIKE $2 LIMIT 1;", blockHeight, hex.EncodeToString([]byte{byte((blockIdStart >> 8) & 0xFF), byte(blockIdStart & 0xFF)})+"%")
|
|
if b2 != nil {
|
|
b = b2
|
|
}
|
|
} else {
|
|
b = b1
|
|
}
|
|
|
|
if b == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
http.Redirect(writer, request, fmt.Sprintf("/share/%s", b.GetBlock().Id.String()), http.StatusFound)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/redirect/prove/{height_index:[0-9]+|.[0-9A-Za-z]+}", func(writer http.ResponseWriter, request *http.Request) {
|
|
i := utils.DecodeBinaryNumber(mux.Vars(request)["height_index"])
|
|
n := uint64(math.Ceil(math.Log2(p2pool.PPLNSWindow * 4)))
|
|
|
|
height := i >> n
|
|
index := i & ((1 << n) - 1)
|
|
|
|
b := api.GetDatabase().GetBlockByHeight(height)
|
|
var tx *database.CoinbaseTransactionOutput
|
|
if b != nil {
|
|
tx = api.GetDatabase().GetCoinbaseTransactionOutputByIndex(b.Coinbase.Id, index)
|
|
}
|
|
|
|
if b == nil || tx == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
miner := api.GetDatabase().GetMiner(tx.Miner())
|
|
|
|
http.Redirect(writer, request, fmt.Sprintf("https://www.exploremonero.com/receipt/%s/%s/%s", b.Coinbase.Id.String(), miner.Address(), b.Coinbase.PrivateKey), http.StatusFound)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/redirect/miner/{miner:[0-9]+|.?[0-9A-Za-z]+}", func(writer http.ResponseWriter, request *http.Request) {
|
|
miner := api.GetDatabase().GetMiner(utils.DecodeBinaryNumber(mux.Vars(request)["miner"]))
|
|
if miner == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
http.Redirect(writer, request, fmt.Sprintf("/miner/%s", miner.Address()), http.StatusFound)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/redirect/prove/{height:[0-9]+|.[0-9A-Za-z]+}/{miner:[0-9]+|.?[0-9A-Za-z]+}", func(writer http.ResponseWriter, request *http.Request) {
|
|
b := api.GetDatabase().GetBlockByHeight(utils.DecodeBinaryNumber(mux.Vars(request)["height"]))
|
|
miner := api.GetDatabase().GetMiner(utils.DecodeBinaryNumber(mux.Vars(request)["miner"]))
|
|
if b == nil || miner == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
http.Redirect(writer, request, fmt.Sprintf("https://www.exploremonero.com/receipt/%s/%s/%s", b.Coinbase.Id.String(), miner.Address(), b.Coinbase.PrivateKey), http.StatusFound)
|
|
})
|
|
|
|
//other redirects
|
|
serveMux.HandleFunc("/api/redirect/last_found{kind:|/raw|/info}", func(writer http.ResponseWriter, request *http.Request) {
|
|
http.Redirect(writer, request, fmt.Sprintf("/api/block_by_id/%s%s%s", api.GetDatabase().GetLastFound().GetBlock().Id.String(), mux.Vars(request)["kind"], request.URL.RequestURI()), http.StatusFound)
|
|
})
|
|
serveMux.HandleFunc("/api/redirect/tip{kind:|/raw|/info}", func(writer http.ResponseWriter, request *http.Request) {
|
|
http.Redirect(writer, request, fmt.Sprintf("/api/block_by_id/%s%s%s", api.GetDatabase().GetChainTip().Id.String(), mux.Vars(request)["kind"], request.URL.RequestURI()), http.StatusFound)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/found_blocks", func(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
params := request.URL.Query()
|
|
|
|
var limit uint64 = 50
|
|
if params.Has("limit") {
|
|
if i, err := strconv.Atoi(params.Get("limit")); err == nil {
|
|
limit = uint64(i)
|
|
}
|
|
}
|
|
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
if limit == 0 {
|
|
limit = 50
|
|
}
|
|
|
|
result := make([]*database.Block, 0, limit)
|
|
|
|
for block := range api.GetDatabase().GetAllFound(limit) {
|
|
MapJSONBlock(api, block, false, params.Has("coinbase"))
|
|
result = append(result, block.GetBlock())
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, result)
|
|
_, _ = writer.Write(buf)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/shares", func(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
params := request.URL.Query()
|
|
|
|
var limit uint64 = 50
|
|
if params.Has("limit") {
|
|
if i, err := strconv.Atoi(params.Get("limit")); err == nil {
|
|
limit = uint64(i)
|
|
}
|
|
}
|
|
|
|
if limit > p2pool.PPLNSWindow {
|
|
limit = p2pool.PPLNSWindow
|
|
}
|
|
if limit == 0 {
|
|
limit = 50
|
|
}
|
|
|
|
onlyBlocks := params.Has("onlyBlocks")
|
|
|
|
var minerId uint64
|
|
|
|
if params.Has("miner") {
|
|
id := params.Get("miner")
|
|
var miner *database.Miner
|
|
|
|
if len(id) > 10 && id[0] == '4' {
|
|
miner = api.GetDatabase().GetMinerByAddress(id)
|
|
}
|
|
|
|
if miner == nil {
|
|
if i, err := strconv.Atoi(id); err == nil {
|
|
miner = api.GetDatabase().GetMiner(uint64(i))
|
|
}
|
|
}
|
|
|
|
if miner == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
minerId = miner.Id()
|
|
}
|
|
|
|
result := make([]*database.Block, 0, limit)
|
|
|
|
for b := range api.GetDatabase().GetShares(limit, minerId, onlyBlocks) {
|
|
MapJSONBlock(api, b, true, params.Has("coinbase"))
|
|
result = append(result, b.GetBlock())
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, result)
|
|
_, _ = writer.Write(buf)
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/block_by_{by:id|height}/{block:[0-9a-f]+|[0-9]+}{kind:|/raw|/info}", func(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
params := request.URL.Query()
|
|
|
|
var block database.BlockInterface
|
|
if mux.Vars(request)["by"] == "id" {
|
|
if id, err := types.HashFromString(mux.Vars(request)["block"]); err == nil {
|
|
if b := api.GetDatabase().GetBlockById(id); b != nil {
|
|
block = b
|
|
}
|
|
}
|
|
} else if mux.Vars(request)["by"] == "height" {
|
|
if height, err := strconv.ParseUint(mux.Vars(request)["block"], 10, 0); err == nil {
|
|
if b := api.GetDatabase().GetBlockByHeight(height); b != nil {
|
|
block = b
|
|
}
|
|
}
|
|
}
|
|
|
|
isOrphan := false
|
|
isInvalid := false
|
|
|
|
if block == nil && mux.Vars(request)["by"] == "id" {
|
|
if id, err := types.HashFromString(mux.Vars(request)["block"]); err == nil {
|
|
if b := api.GetDatabase().GetUncleById(id); b != nil {
|
|
block = b
|
|
} else {
|
|
isOrphan = true
|
|
|
|
if b, _, _ := api.GetShareFromRawEntry(id, false); b != nil {
|
|
block = b
|
|
} else {
|
|
isInvalid = true
|
|
if b, _ = api.GetShareFromFailedRawEntry(id); b != nil {
|
|
block = b
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if block == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
switch mux.Vars(request)["kind"] {
|
|
case "/raw":
|
|
raw, _ := api.GetRawBlockBytes(block.GetBlock().Id)
|
|
|
|
if raw == nil {
|
|
raw, _ = api.GetFailedRawBlockBytes(block.GetBlock().Id)
|
|
}
|
|
|
|
if raw == nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "not_found",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "text/plain")
|
|
writer.WriteHeader(http.StatusOK)
|
|
_, _ = writer.Write(raw)
|
|
default:
|
|
MapJSONBlock(api, block, true, params.Has("coinbase"))
|
|
|
|
func() {
|
|
block.GetBlock().Lock.Lock()
|
|
defer block.GetBlock().Lock.Unlock()
|
|
block.GetBlock().Orphan = isOrphan
|
|
if isInvalid {
|
|
block.GetBlock().Invalid = &isInvalid
|
|
}
|
|
}()
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, block.GetBlock())
|
|
_, _ = writer.Write(buf)
|
|
}
|
|
})
|
|
|
|
type difficultyStatResult struct {
|
|
Height uint64 `json:"height"`
|
|
Timestamp uint64 `json:"timestamp"`
|
|
Difficulty uint64 `json:"difficulty"`
|
|
}
|
|
|
|
type minerSeenResult struct {
|
|
Height uint64 `json:"height"`
|
|
Timestamp uint64 `json:"timestamp"`
|
|
Miners uint64 `json:"miners"`
|
|
}
|
|
|
|
serveMux.HandleFunc("/api/stats/{kind:difficulty|miner_seen|miner_seen_window$}", func(writer http.ResponseWriter, request *http.Request) {
|
|
switch mux.Vars(request)["kind"] {
|
|
case "difficulty":
|
|
result := make([]*difficultyStatResult, 0)
|
|
|
|
_ = api.GetDatabase().Query("SELECT height,timestamp,difficulty FROM blocks WHERE height % $1 = 0 ORDER BY height DESC;", func(row database.RowScanInterface) error {
|
|
r := &difficultyStatResult{}
|
|
|
|
var difficultyHex string
|
|
if err := row.Scan(&r.Height, &r.Timestamp, &difficultyHex); err != nil {
|
|
return err
|
|
}
|
|
d, _ := types.DifficultyFromString(difficultyHex)
|
|
r.Difficulty = d.Lo
|
|
|
|
result = append(result, r)
|
|
return nil
|
|
}, 3600/p2pool.BlockTime)
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, result)
|
|
_, _ = writer.Write(buf)
|
|
|
|
case "miner_seen":
|
|
result := make([]*minerSeenResult, 0)
|
|
|
|
_ = api.GetDatabase().Query("SELECT height,timestamp,(SELECT COUNT(DISTINCT(b.miner)) FROM blocks b WHERE b.height <= blocks.height AND b.height > (blocks.height - $2)) as count FROM blocks WHERE height % $1 = 0 ORDER BY height DESC;", func(row database.RowScanInterface) error {
|
|
r := &minerSeenResult{}
|
|
|
|
if err := row.Scan(&r.Height, &r.Timestamp, &r.Miners); err != nil {
|
|
return err
|
|
}
|
|
|
|
result = append(result, r)
|
|
return nil
|
|
}, (3600*24)/p2pool.BlockTime, (3600*24*7)/p2pool.BlockTime)
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, result)
|
|
_, _ = writer.Write(buf)
|
|
|
|
case "miner_seen_window":
|
|
result := make([]*minerSeenResult, 0)
|
|
|
|
_ = api.GetDatabase().Query("SELECT height,timestamp,(SELECT COUNT(DISTINCT(b.miner)) FROM blocks b WHERE b.height <= blocks.height AND b.height > (blocks.height - $2)) as count FROM blocks WHERE height % $1 = 0 ORDER BY height DESC;", func(row database.RowScanInterface) error {
|
|
r := &minerSeenResult{}
|
|
|
|
if err := row.Scan(&r.Height, &r.Timestamp, &r.Miners); err != nil {
|
|
return err
|
|
}
|
|
|
|
result = append(result, r)
|
|
return nil
|
|
}, p2pool.PPLNSWindow, p2pool.PPLNSWindow)
|
|
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, result)
|
|
_, _ = writer.Write(buf)
|
|
}
|
|
})
|
|
|
|
server := &http.Server{
|
|
Addr: "0.0.0.0:8080",
|
|
ReadTimeout: time.Second * 2,
|
|
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Method != "GET" && request.Method != "HEAD" {
|
|
writer.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
serveMux.ServeHTTP(writer, request)
|
|
}),
|
|
}
|
|
|
|
if err := server.ListenAndServe(); err != nil {
|
|
log.Panic(err)
|
|
}
|
|
}
|