1033 lines
32 KiB
Go
1033 lines
32 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
|
|
p2poolapi "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
|
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
|
"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"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
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() {
|
|
torHost := os.Getenv("TOR_SERVICE_ADDRESS")
|
|
client.SetDefaultClientSettings(os.Getenv("MONEROD_RPC_URL"))
|
|
dbString := flag.String("db", "", "")
|
|
p2poolApiHost := flag.String("api-host", "", "Host URL for p2pool go observer consensus")
|
|
flag.Parse()
|
|
db, err := database.NewDatabase(*dbString)
|
|
if err != nil {
|
|
log.Panic(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
p2api := p2poolapi.NewP2PoolApi(*p2poolApiHost)
|
|
api, err := p2poolapi.New(db, p2api)
|
|
if err != nil {
|
|
log.Panic(err)
|
|
}
|
|
|
|
for status := p2api.Status(); !p2api.Status().Synchronized; status = p2api.Status() {
|
|
log.Printf("[API] Not synchronized (height %d, id %s), waiting five seconds", status.Height, status.Id)
|
|
time.Sleep(time.Second * 5)
|
|
}
|
|
|
|
log.Printf("[CHAIN] Consensus id = %s\n", p2api.Consensus().Id())
|
|
|
|
getSeedByHeight := func(height uint64) (hash types.Hash) {
|
|
seedHeight := randomx.SeedHeight(height)
|
|
if h := p2api.MainHeaderByHeight(seedHeight); h == nil {
|
|
return types.ZeroHash
|
|
} else {
|
|
return h.Id
|
|
}
|
|
}
|
|
|
|
getBlockWithUncles := func(id types.Hash) (block *sidechain.PoolBlock, uncles []*sidechain.PoolBlock) {
|
|
block = p2api.ByTemplateId(id)
|
|
if block == nil {
|
|
return nil, nil
|
|
}
|
|
for _, uncleId := range block.Side.Uncles {
|
|
if u := p2api.ByTemplateId(uncleId); u == nil {
|
|
return nil, nil
|
|
} else {
|
|
uncles = append(uncles, u)
|
|
}
|
|
}
|
|
return block, uncles
|
|
}
|
|
_ = getBlockWithUncles
|
|
|
|
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 types.Difficulty
|
|
|
|
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)
|
|
for u := range api.GetDatabase().GetUnclesByParentId(b.Id) {
|
|
//TODO: check this check is correct :)
|
|
if (tip.Height - u.Block.Height) > p2api.Consensus().ChainWindowSize {
|
|
continue
|
|
}
|
|
|
|
uncleCount++
|
|
if _, ok := miners[u.Block.MinerId]; !ok {
|
|
miners[u.Block.MinerId] = 0
|
|
}
|
|
miners[u.Block.MinerId]++
|
|
|
|
windowDifficulty = windowDifficulty.Add(u.Block.Difficulty)
|
|
}
|
|
}
|
|
|
|
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 IS TRUE ) + (SELECT COUNT(*) FROM uncles WHERE main_found IS TRUE) 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(types.DifficultyFrom64(poolStats.PoolStatistics.TotalHashes-poolBlocks[0].TotalHashes).Mul64(100000).Div(globalDiff).Lo) / 1000
|
|
|
|
if currentEffort <= 0 || poolBlocks[0].TotalHashes == 0 {
|
|
currentEffort = 0
|
|
}
|
|
|
|
var blockEfforts mapslice.MapSlice
|
|
for i, b := range poolBlocks {
|
|
if i < (len(poolBlocks)-1) && b.TotalHashes > 0 && poolBlocks[i+1].TotalHashes > 0 {
|
|
blockEfforts = append(blockEfforts, mapslice.MapItem{
|
|
Key: b.Hash.String(),
|
|
Value: float64((b.TotalHashes-poolBlocks[i+1].TotalHashes)*100) / float64(b.Difficulty),
|
|
})
|
|
}
|
|
}
|
|
|
|
*/
|
|
currentEffort := float64(1)
|
|
var blockEfforts mapslice.MapSlice
|
|
blockEfforts = append(blockEfforts, mapslice.MapItem{
|
|
Key: types.ZeroHash.String(),
|
|
Value: float64(1),
|
|
})
|
|
|
|
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: windowDifficulty,
|
|
},
|
|
WindowSize: int(p2api.Consensus().ChainWindowSize),
|
|
BlockTime: int(p2api.Consensus().TargetBlockTime),
|
|
UnclePenalty: int(p2api.Consensus().UnclePenalty),
|
|
Found: totalKnown.blocksFound,
|
|
Miners: totalKnown.minersKnown,
|
|
},
|
|
MainChain: poolInfoResultMainChain{
|
|
Id: tip.Template.Id,
|
|
Height: tip.Main.Height - 1,
|
|
Difficulty: tip.Template.Difficulty,
|
|
BlockTime: monero.BlockTime,
|
|
},
|
|
Versions: struct {
|
|
P2Pool versionInfo `json:"p2pool"`
|
|
Monero versionInfo `json:"monero"`
|
|
}{P2Pool: getP2PoolVersion(), Monero: getMoneroVersion()},
|
|
}); err != nil {
|
|
log.Panic(err)
|
|
} else {
|
|
_, _ = writer.Write(buf)
|
|
}
|
|
|
|
})
|
|
|
|
serveMux.HandleFunc("/api/miner_info/{miner:[^ ]+}", 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 {
|
|
miner = api.GetDatabase().GetMinerByAlias(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, coalesce(MAX(height), 0) 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, coalesce(MAX(parent_height), 0) 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(),
|
|
Alias: miner.Alias(),
|
|
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/miner_alias/{miner:4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+}", func(writer http.ResponseWriter, request *http.Request) {
|
|
minerId := mux.Vars(request)["miner"]
|
|
miner := api.GetDatabase().GetMinerByAddress(minerId)
|
|
|
|
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()
|
|
|
|
sig := strings.TrimSpace(params.Get("signature"))
|
|
|
|
message := strings.TrimSpace(params.Get("message"))
|
|
|
|
if len(message) > 20 || len(message) < 3 || !func() bool {
|
|
for _, c := range message {
|
|
if !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && c != '_' && c != '-' && c != '.' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}() || !unicode.IsLetter(rune(message[0])) {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusBadRequest)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "invalid_message",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
|
|
result := address.VerifyMessage(miner.MoneroAddress(), []byte(message), sig)
|
|
if result == address.ResultSuccessSpend {
|
|
if message == "REMOVE_MINER_ALIAS" {
|
|
message = ""
|
|
}
|
|
if api.GetDatabase().SetMinerAlias(miner.Id(), message) != nil {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusBadRequest)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "duplicate_message",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
} else {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
} else if result == address.ResultSuccessView {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusBadRequest)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "view_signature",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
} else {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusBadRequest)
|
|
buf, _ := json.Marshal(struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: "invalid_signature",
|
|
})
|
|
_, _ = writer.Write(buf)
|
|
return
|
|
}
|
|
})
|
|
|
|
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 := p2api.Consensus().ChainWindowSize
|
|
if params.Has("window") {
|
|
if i, err := strconv.Atoi(params.Get("window")); err == nil {
|
|
if i <= int(p2api.Consensus().ChainWindowSize*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.Lo,
|
|
}
|
|
|
|
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: uncleWeight.Lo,
|
|
})
|
|
s.Weight += uncleWeight.Lo
|
|
}
|
|
|
|
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: uncle.Block.Difficulty.Mul64(100 - p2pool.UnclePenalty).Div64(100).Lo,
|
|
}
|
|
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) {
|
|
if request.Header.Get("host") == torHost {
|
|
http.Redirect(writer, request, fmt.Sprintf("http://yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion/explorer/block/%d", utils.DecodeBinaryNumber(mux.Vars(request)["main_height"])), http.StatusFound)
|
|
} else {
|
|
http.Redirect(writer, request, fmt.Sprintf("https://p2pool.io/explorer/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) {
|
|
txId := utils.DecodeHexBinaryNumber(mux.Vars(request)["tx_id"])
|
|
if len(txId) != types.HashSize*2 {
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if request.Header.Get("host") == torHost {
|
|
http.Redirect(writer, request, fmt.Sprintf("http://yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion/explorer/tx/%s", txId), http.StatusFound)
|
|
} else {
|
|
http.Redirect(writer, request, fmt.Sprintf("https://p2pool.io/explorer/tx/%s", txId), 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
|
|
}
|
|
|
|
if request.Header.Get("host") == torHost {
|
|
http.Redirect(writer, request, fmt.Sprintf("http://yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion/explorer/tx/%s", b.Coinbase.Id.String()), http.StatusFound)
|
|
} else {
|
|
http.Redirect(writer, request, fmt.Sprintf("https://p2pool.io/explorer/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(float64(p2api.Consensus().ChainWindowSize * 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
|
|
}
|
|
|
|
http.Redirect(writer, request, fmt.Sprintf("/proof/%s/%d", b.Id.String(), tx.Index()), 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
|
|
}
|
|
|
|
tx := api.GetDatabase().GetCoinbaseTransactionOutputByMinerId(b.Coinbase.Id, miner.Id())
|
|
|
|
if 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
|
|
}
|
|
|
|
http.Redirect(writer, request, fmt.Sprintf("/proof/%s/%d", b.Id.String(), tx.Index()), 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)
|
|
})
|
|
|
|
//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)
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
|
|
if limit == 0 {
|
|
limit = 50
|
|
}
|
|
|
|
result := make([]*database.Block, 0, limit)
|
|
|
|
for block := range api.GetDatabase().GetAllFound(limit, minerId) {
|
|
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 > p2api.Consensus().ChainWindowSize*4*7 {
|
|
limit = p2api.Consensus().ChainWindowSize * 4 * 7
|
|
}
|
|
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|/payouts}", 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
|
|
|
|
bblock := p2api.ByTemplateId(id)
|
|
|
|
if b, _, _ := database.NewBlockFromBinaryBlock(getSeedByHeight, p2api.MainDifficultyByHeight, db, bblock, nil, 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 := p2api.ByTemplateId(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)
|
|
buf, _ := raw.MarshalBinary()
|
|
_, _ = writer.Write(buf)
|
|
case "/payouts":
|
|
result := make([]*database.Payout, 0)
|
|
if !isOrphan && !isInvalid {
|
|
blockHeight := block.GetBlock().Height
|
|
if uncle, ok := block.(*database.UncleBlock); ok {
|
|
blockHeight = uncle.ParentHeight
|
|
}
|
|
|
|
if err := db.Query("SELECT decode(b.id, 'hex') AS id, decode(b.main_id, 'hex') AS main_id, b.height AS height, b.main_height AS main_height, b.timestamp AS timestamp, decode(b.coinbase_privkey, 'hex') AS coinbase_privkey, b.uncle AS uncle, decode(o.id, 'hex') AS coinbase_id, o.amount AS amount, o.index AS index FROM (SELECT id, amount, index FROM coinbase_outputs WHERE miner = $1 ) o LEFT JOIN LATERAL (SELECT id, coinbase_id, coinbase_privkey, height, main_height, main_id, timestamp, FALSE AS uncle FROM blocks WHERE coinbase_id = o.id UNION SELECT id, coinbase_id, coinbase_privkey, height, main_height, main_id, timestamp, TRUE AS uncle FROM uncles WHERE coinbase_id = o.id) b ON b.coinbase_id = o.id WHERE height >= $2 AND height < $3 ORDER BY main_height ASC;", func(row database.RowScanInterface) error {
|
|
var blockId, mainId, privKey, coinbaseId []byte
|
|
var height, mainHeight, timestamp, amount, index uint64
|
|
var uncle bool
|
|
|
|
if err := row.Scan(&blockId, &mainId, &height, &mainHeight, ×tamp, &privKey, &uncle, &coinbaseId, &amount, &index); err != nil {
|
|
return err
|
|
}
|
|
|
|
result = append(result, &database.Payout{
|
|
Id: types.HashFromBytes(blockId),
|
|
Height: height,
|
|
Timestamp: timestamp,
|
|
Main: struct {
|
|
Id types.Hash `json:"id"`
|
|
Height uint64 `json:"height"`
|
|
}{Id: types.HashFromBytes(mainId), Height: mainHeight},
|
|
Uncle: uncle,
|
|
Coinbase: struct {
|
|
Id types.Hash `json:"id"`
|
|
Reward uint64 `json:"reward"`
|
|
PrivateKey crypto.PrivateKeyBytes `json:"private_key"`
|
|
Index uint64 `json:"index"`
|
|
}{Id: types.HashFromBytes(coinbaseId), Reward: amount, PrivateKey: crypto.PrivateKeyBytes(types.HashFromBytes(privKey)), Index: index},
|
|
})
|
|
return nil
|
|
}, block.GetBlock().MinerId, blockHeight, blockHeight+p2api.Consensus().ChainWindowSize); err != nil {
|
|
return
|
|
}
|
|
}
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
buf, _ := encodeJson(request, result)
|
|
_, _ = writer.Write(buf)
|
|
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/p2api.Consensus().TargetBlockTime)
|
|
|
|
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)/p2api.Consensus().TargetBlockTime, (3600*24*7)/p2api.Consensus().TargetBlockTime)
|
|
|
|
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
|
|
}, p2api.Consensus().ChainWindowSize, p2api.Consensus().ChainWindowSize)
|
|
|
|
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)
|
|
}
|
|
}
|