refactor(daemon): extract 842 lines into http_handlers.go
server.go: 2339 → 1497 lines (-36%) http_handlers.go: 842 lines (REST, SSE, metrics, openapi, binary endpoints, service discovery, analytics, crypto utils, address tools) Real structural improvement — server.go is now under 1500 lines. All tests pass. No behaviour change. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
6f0b331b23
commit
3b72a1ab14
2 changed files with 865 additions and 844 deletions
865
daemon/http_handlers.go
Normal file
865
daemon/http_handlers.go
Normal file
|
|
@ -0,0 +1,865 @@
|
|||
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/blockchain/chain"
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
"dappco.re/go/core/blockchain/crypto"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// REST, SSE, Metrics, and OpenAPI handlers.
|
||||
// Extracted from server.go for maintainability.
|
||||
|
||||
// --- REST-style HTTP endpoints (no JSON-RPC wrapper) ---
|
||||
|
||||
func (s *Server) handleRESTInfo(w http.ResponseWriter, r *http.Request) {
|
||||
height, _ := s.chain.Height()
|
||||
_, meta := s.safeTopBlock()
|
||||
if meta == nil {
|
||||
meta = &chain.BlockMeta{}
|
||||
}
|
||||
aliases := s.chain.GetAllAliases()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"height": height, "difficulty": meta.Difficulty,
|
||||
"aliases": len(aliases), "node": "CoreChain/Go",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTBlock(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("height")
|
||||
h, err := parseUint64(q)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "?height= required"})
|
||||
return
|
||||
}
|
||||
blk, meta, err := s.chain.GetBlockByHeight(h)
|
||||
if err != nil {
|
||||
w.WriteHeader(404)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"height": meta.Height, "hash": meta.Hash.String(),
|
||||
"timestamp": blk.Timestamp, "difficulty": meta.Difficulty,
|
||||
"tx_count": len(blk.TxHashes), "version": blk.MajorVersion,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTAliases(w http.ResponseWriter, r *http.Request) {
|
||||
aliases := s.chain.GetAllAliases()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(aliases)
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTAlias(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
w.WriteHeader(400)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "?name= required"})
|
||||
return
|
||||
}
|
||||
alias, err := s.chain.GetAlias(name)
|
||||
if err != nil {
|
||||
w.WriteHeader(404)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(alias)
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTSearch(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
if q == "" {
|
||||
w.WriteHeader(400)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "?q= required"})
|
||||
return
|
||||
}
|
||||
// Reuse the RPC search logic
|
||||
fakeReq := jsonRPCRequest{Params: json.RawMessage(core.Sprintf(`{"query":"%s"}`, q))}
|
||||
s.rpcSearch(w, fakeReq)
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTHealth(w http.ResponseWriter, r *http.Request) {
|
||||
height, _ := s.chain.Height()
|
||||
aliases := s.chain.GetAllAliases()
|
||||
status := "ok"
|
||||
if height == 0 {
|
||||
status = "syncing"
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": status, "height": height,
|
||||
"aliases": len(aliases), "node": "CoreChain/Go",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Server-Sent Events for real-time updates ---
|
||||
// (SSE is simpler than WebSocket and works with curl)
|
||||
|
||||
func (s *Server) handleSSEBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming not supported", 500)
|
||||
return
|
||||
}
|
||||
|
||||
lastHeight := uint64(0)
|
||||
ctx := r.Context()
|
||||
heartbeat := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
height, _ := s.chain.Height()
|
||||
if height > lastHeight && lastHeight > 0 {
|
||||
for h := lastHeight + 1; h <= height; h++ {
|
||||
blk, meta, err := s.chain.GetBlockByHeight(h)
|
||||
if err != nil { continue }
|
||||
data := core.Sprintf(`{"height":%d,"hash":"%s","timestamp":%d,"difficulty":%d,"tx_count":%d}`,
|
||||
meta.Height, meta.Hash.String(), blk.Timestamp, meta.Difficulty, len(blk.TxHashes))
|
||||
if _, err := w.Write([]byte(core.Sprintf("event: block\ndata: %s\n\n", data))); err != nil {
|
||||
return // client disconnected
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
heartbeat = 0
|
||||
}
|
||||
lastHeight = height
|
||||
|
||||
// Send keepalive every 30s (15 * 2s poll)
|
||||
heartbeat++
|
||||
if heartbeat >= 15 {
|
||||
if _, err := w.Write([]byte(": keepalive\n\n")); err != nil {
|
||||
return // client disconnected
|
||||
}
|
||||
flusher.Flush()
|
||||
heartbeat = 0
|
||||
}
|
||||
|
||||
// Poll every 2 seconds
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- SWAP & gateway service methods ---
|
||||
|
||||
func (s *Server) rpcGetAliasCapabilities(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
alias, err := s.chain.GetAlias(params.Alias)
|
||||
if err != nil {
|
||||
writeError(w, req.ID, -1, core.Sprintf("alias %s not found", params.Alias))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse v=lthn1;type=gateway;cap=vpn,dns,proxy;hns=charon.lthn
|
||||
parsed := make(map[string]string)
|
||||
for _, part := range splitSemicolon(alias.Comment) {
|
||||
idx := -1
|
||||
for i, c := range part {
|
||||
if c == '=' { idx = i; break }
|
||||
}
|
||||
if idx > 0 {
|
||||
parsed[part[:idx]] = part[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
var caps []string
|
||||
if c, ok := parsed["cap"]; ok {
|
||||
for _, part := range splitComma(c) {
|
||||
caps = append(caps, part)
|
||||
}
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"alias": alias.Name,
|
||||
"version": parsed["v"],
|
||||
"type": parsed["type"],
|
||||
"capabilities": caps,
|
||||
"hns": parsed["hns"],
|
||||
"raw_comment": alias.Comment,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func splitComma(s string) []string {
|
||||
var parts []string
|
||||
current := ""
|
||||
for _, c := range s {
|
||||
if c == ',' {
|
||||
if current != "" { parts = append(parts, current) }
|
||||
current = ""
|
||||
} else {
|
||||
current += string(c)
|
||||
}
|
||||
}
|
||||
if current != "" { parts = append(parts, current) }
|
||||
return parts
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetServiceEndpoints(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var endpoints []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := make(map[string]string)
|
||||
for _, part := range splitSemicolon(a.Comment) {
|
||||
idx := -1
|
||||
for i, c := range part { if c == '=' { idx = i; break } }
|
||||
if idx > 0 { parsed[part[:idx]] = part[idx+1:] }
|
||||
}
|
||||
|
||||
if parsed["type"] == "" { continue }
|
||||
|
||||
var caps []string
|
||||
if c, ok := parsed["cap"]; ok {
|
||||
for _, p := range splitComma(c) { caps = append(caps, p) }
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"type": parsed["type"],
|
||||
"capabilities": caps,
|
||||
"hns_name": parsed["hns"],
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"endpoints": endpoints,
|
||||
"count": len(endpoints),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetTotalCoins(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
height, _ := s.chain.Height()
|
||||
// Premine: 10M LTHN + 1 LTHN per block after genesis
|
||||
premine := uint64(10000000) // LTHN
|
||||
mined := height // 1 LTHN per block
|
||||
total := premine + mined
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"total_coins": total,
|
||||
"premine": premine,
|
||||
"mined": mined,
|
||||
"block_reward": 1,
|
||||
"height": height,
|
||||
"unit": "LTHN",
|
||||
"atomic_total": total * 1000000000000,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Self-documentation ---
|
||||
|
||||
func (s *Server) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
|
||||
methods := []map[string]string{
|
||||
{"method": "getinfo", "type": "chain", "desc": "Chain height, difficulty, alias count, hardfork status"},
|
||||
{"method": "getheight", "type": "chain", "desc": "Current block height"},
|
||||
{"method": "getblockcount", "type": "chain", "desc": "Total block count"},
|
||||
{"method": "getblockheaderbyheight", "type": "chain", "desc": "Block header by height"},
|
||||
{"method": "getblockheaderbyhash", "type": "chain", "desc": "Block header by hash"},
|
||||
{"method": "getlastblockheader", "type": "chain", "desc": "Latest block header"},
|
||||
{"method": "on_getblockhash", "type": "chain", "desc": "Block hash by height"},
|
||||
{"method": "get_blocks_details", "type": "chain", "desc": "Batch block details"},
|
||||
{"method": "get_block_hash_by_height", "type": "chain", "desc": "Height to hash"},
|
||||
{"method": "get_recent_blocks", "type": "chain", "desc": "Last N blocks"},
|
||||
{"method": "get_tx_details", "type": "chain", "desc": "Transaction details by hash"},
|
||||
{"method": "get_all_alias_details", "type": "alias", "desc": "All registered aliases"},
|
||||
{"method": "get_alias_details", "type": "alias", "desc": "Single alias by name"},
|
||||
{"method": "get_alias_by_address", "type": "alias", "desc": "Aliases for an address"},
|
||||
{"method": "get_aliases_by_type", "type": "alias", "desc": "Filter aliases by type"},
|
||||
{"method": "get_alias_capabilities", "type": "alias", "desc": "Parse alias v=lthn1 capabilities"},
|
||||
{"method": "get_alias_reward", "type": "alias", "desc": "Alias registration cost"},
|
||||
{"method": "get_gateways", "type": "discovery", "desc": "All gateway nodes with capabilities"},
|
||||
{"method": "get_service_endpoints", "type": "discovery", "desc": "All service endpoints"},
|
||||
{"method": "get_asset_info", "type": "asset", "desc": "Asset descriptor by ID or ticker"},
|
||||
{"method": "get_assets_list", "type": "asset", "desc": "All known assets"},
|
||||
{"method": "get_pool_info", "type": "chain", "desc": "Transaction pool info"},
|
||||
{"method": "getblockchaininfo", "type": "chain", "desc": "Full blockchain info"},
|
||||
{"method": "get_hardfork_status", "type": "chain", "desc": "Hardfork schedule with countdown"},
|
||||
{"method": "get_chain_stats", "type": "analytics", "desc": "Chain statistics and averages"},
|
||||
{"method": "get_difficulty_history", "type": "analytics", "desc": "Difficulty history for charts"},
|
||||
{"method": "get_total_coins", "type": "analytics", "desc": "Total coin supply calculation"},
|
||||
{"method": "get_est_height_from_date", "type": "chain", "desc": "Estimate height from timestamp"},
|
||||
{"method": "get_current_core_tx_expiration_median", "type": "chain", "desc": "TX expiration median"},
|
||||
{"method": "get_version", "type": "system", "desc": "Node version and type"},
|
||||
{"method": "get_node_info", "type": "system", "desc": "Node capabilities and stats"},
|
||||
{"method": "search", "type": "utility", "desc": "Universal search (block/tx/alias/address)"},
|
||||
{"method": "validate_signature", "type": "crypto", "desc": "Schnorr signature verification (native CGo)"},
|
||||
{"method": "generate_keys", "type": "crypto", "desc": "Ed25519 keypair generation (native CGo)"},
|
||||
{"method": "generate_key_image", "type": "crypto", "desc": "Key image from keypair (native CGo)"},
|
||||
{"method": "fast_hash", "type": "crypto", "desc": "Keccak-256 hash (native CGo)"},
|
||||
{"method": "check_key", "type": "crypto", "desc": "Validate Ed25519 public key (native CGo)"},
|
||||
{"method": "check_keyimages", "type": "crypto", "desc": "Check spent key images (native)"},
|
||||
{"method": "validate_address", "type": "crypto", "desc": "Validate iTHN address format (native)"},
|
||||
{"method": "make_integrated_address", "type": "crypto", "desc": "Encode integrated address (native)"},
|
||||
{"method": "split_integrated_address", "type": "crypto", "desc": "Decode integrated address (native)"},
|
||||
{"method": "marketplace_global_get_offers_ex", "type": "marketplace", "desc": "Marketplace offers"},
|
||||
{"method": "sendrawtransaction", "type": "relay", "desc": "Broadcast raw transaction"},
|
||||
}
|
||||
|
||||
// Count by type
|
||||
counts := make(map[string]int)
|
||||
for _, m := range methods {
|
||||
counts[m["type"]]++
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"node": "CoreChain/Go v0.3.0",
|
||||
"module": "dappco.re/go/core/blockchain",
|
||||
"rpc_methods": methods,
|
||||
"method_count": len(methods),
|
||||
"categories": counts,
|
||||
"http_endpoints": []string{
|
||||
"/json_rpc", "/getheight", "/start_mining", "/stop_mining",
|
||||
"/gettransactions", "/api/info", "/api/block", "/api/aliases",
|
||||
"/api/alias", "/api/search", "/health", "/events/blocks", "/openapi",
|
||||
},
|
||||
"wallet_proxy_methods": []string{
|
||||
"getbalance", "getaddress", "get_wallet_info", "transfer",
|
||||
"make_integrated_address", "split_integrated_address",
|
||||
"deploy_asset", "emit_asset", "burn_asset",
|
||||
"register_alias", "update_alias", "get_bulk_payments",
|
||||
"get_recent_txs_and_info", "store", "get_restore_info", "sign_message",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// --- Ring member selection (critical for native wallet tx construction) ---
|
||||
|
||||
func (s *Server) rpcGetRandomOuts(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Amounts []uint64 `json:"amounts"`
|
||||
Count int `json:"outs_count"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
if params.Count == 0 {
|
||||
params.Count = 15 // HF4 mandatory decoy set size
|
||||
}
|
||||
|
||||
var outs []map[string]interface{}
|
||||
for _, amount := range params.Amounts {
|
||||
totalOutputs, _ := s.chain.OutputCount(amount)
|
||||
if totalOutputs == 0 {
|
||||
outs = append(outs, map[string]interface{}{
|
||||
"amount": amount,
|
||||
"outs": []interface{}{},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Select random output indices
|
||||
var selected []map[string]interface{}
|
||||
step := totalOutputs / uint64(params.Count)
|
||||
if step == 0 { step = 1 }
|
||||
|
||||
for i := 0; i < params.Count && uint64(i)*step < totalOutputs; i++ {
|
||||
globalIdx := uint64(i) * step
|
||||
txHash, outIdx, err := s.chain.GetOutput(amount, globalIdx)
|
||||
if err != nil { continue }
|
||||
selected = append(selected, map[string]interface{}{
|
||||
"global_amount_index": globalIdx,
|
||||
"out_key": txHash.String(),
|
||||
"tx_out_index": outIdx,
|
||||
})
|
||||
}
|
||||
|
||||
outs = append(outs, map[string]interface{}{
|
||||
"amount": amount,
|
||||
"outs": selected,
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"outs": outs,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Admin methods ---
|
||||
|
||||
func (s *Server) rpcGetPeerList(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
// Go node doesn't maintain a peer list yet (sync-only, not P2P server)
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"white_list": []interface{}{},
|
||||
"gray_list": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetConnections(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"connections": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Binary endpoints (portable storage format) ---
|
||||
|
||||
func (s *Server) handleGetBlocksBin(w http.ResponseWriter, r *http.Request) {
|
||||
// Read request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse portable storage request
|
||||
// For now, serve JSON — binary format needs full portable storage integration
|
||||
// The p2p/encode.go has EncodeStorage but the HTTP binary format
|
||||
// uses a slightly different wrapper than P2P
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = body
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"note": "binary endpoint — use JSON-RPC get_blocks_details instead",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetOutputIndexesBin(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
_ = body
|
||||
|
||||
// Parse tx hash from request, return output global indexes
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"o_indexes": []uint64{},
|
||||
})
|
||||
}
|
||||
|
||||
// --- Remaining parity methods ---
|
||||
|
||||
func (s *Server) rpcGetAllPoolTxList(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"ids": []string{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetPoolTxsDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"txs": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetPoolTxsBriefDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"txs": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcResetTxPool(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
func (s *Server) rpcRemoveTxFromPool(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
func (s *Server) rpcForceRelay(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetMultisigInfo(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"multisig_outs": 0,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetAlternateBlocksDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"alt_blocks": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetVotes(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"votes": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetMainBlockDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Height uint64 `json:"height"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
blk, meta, err := s.chain.GetBlockByHeight(params.Height)
|
||||
if err != nil {
|
||||
writeError(w, req.ID, -1, core.Sprintf("block not found at %d", params.Height))
|
||||
return
|
||||
}
|
||||
|
||||
txHashes := make([]string, len(blk.TxHashes))
|
||||
for i, h := range blk.TxHashes {
|
||||
txHashes[i] = h.String()
|
||||
}
|
||||
|
||||
minerTxHash := wire.TransactionHash(&blk.MinerTx).String()
|
||||
|
||||
// Check if block is PoS (bit 0 of flags)
|
||||
isPoS := blk.Flags&1 != 0
|
||||
blockType := "PoW"
|
||||
if isPoS {
|
||||
blockType = "PoS"
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"block_details": map[string]interface{}{
|
||||
"height": meta.Height,
|
||||
"hash": meta.Hash.String(),
|
||||
"prev_hash": blk.PrevID.String(),
|
||||
"timestamp": blk.Timestamp,
|
||||
"difficulty": meta.Difficulty,
|
||||
"cumulative_diff": meta.CumulativeDiff,
|
||||
"major_version": blk.MajorVersion,
|
||||
"minor_version": blk.MinorVersion,
|
||||
"nonce": blk.Nonce,
|
||||
"flags": blk.Flags,
|
||||
"block_type": blockType,
|
||||
"miner_tx_hash": minerTxHash,
|
||||
"tx_count": len(blk.TxHashes),
|
||||
"tx_hashes": txHashes,
|
||||
"base_reward": config.Coin, // 1 LTHN
|
||||
},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetAltBlockDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
// No alt blocks in Go node (read-only, follows canonical chain)
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"block_details": map[string]interface{}{},
|
||||
"status": "NOT_FOUND",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Native wallet utility methods (no C++ wallet needed) ---
|
||||
|
||||
func (s *Server) rpcDerivePaymentID(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
// Generate a random 8-byte payment ID
|
||||
var id [8]byte
|
||||
pubKey, _, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
writeError(w, req.ID, -1, "random generation failed")
|
||||
return
|
||||
}
|
||||
copy(id[:], pubKey[:8])
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"payment_id": hex.EncodeToString(id[:]),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetAddressType(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
addr, prefix, err := types.DecodeAddress(params.Address)
|
||||
if err != nil {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"valid": false, "error": err.Error(), "status": "OK",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addrType := "standard"
|
||||
switch prefix {
|
||||
case 0x1eaf7: addrType = "standard"
|
||||
case 0xdeaf7: addrType = "integrated"
|
||||
case 0x3ceff7: addrType = "auditable"
|
||||
case 0x8b077: addrType = "auditable_integrated"
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"valid": true,
|
||||
"type": addrType,
|
||||
"prefix": prefix,
|
||||
"spend_key": hex.EncodeToString(addr.SpendPublicKey[:]),
|
||||
"view_key": hex.EncodeToString(addr.ViewPublicKey[:]),
|
||||
"flags": addr.Flags,
|
||||
"is_auditable": addr.Flags != 0,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetCoinSupply(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
height, _ := s.chain.Height()
|
||||
premine := uint64(10000000)
|
||||
mined := height
|
||||
total := premine + mined
|
||||
burned := uint64(0) // fees are burned post-HF4
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"total_supply": total,
|
||||
"circulating": total - burned,
|
||||
"premine": premine,
|
||||
"mined": mined,
|
||||
"burned_fees": burned,
|
||||
"block_reward": 1,
|
||||
"blocks_per_day": 720, // ~120s blocks, PoW+PoS alternating
|
||||
"daily_emission": 720,
|
||||
"annual_emission": 262800,
|
||||
"height": height,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetNetworkHashrate(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
height, _ := s.chain.Height()
|
||||
_, meta := s.safeTopBlock()
|
||||
if meta == nil {
|
||||
meta = &chain.BlockMeta{}
|
||||
}
|
||||
|
||||
// Hashrate ≈ difficulty / block_time
|
||||
hashrate := meta.Difficulty / 120
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"hashrate": hashrate,
|
||||
"difficulty": meta.Difficulty,
|
||||
"height": height,
|
||||
"avg_block_time": 120,
|
||||
"unit": "H/s",
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- LetherNet service layer (Go-exclusive, no C++ equivalent) ---
|
||||
|
||||
func (s *Server) rpcGetGatewayEndpoints(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var endpoints []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
if !core.Contains(a.Comment, "type=gateway") { continue }
|
||||
parsed := parseComment(a.Comment)
|
||||
|
||||
ep := map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"type": "gateway",
|
||||
}
|
||||
|
||||
if hns, ok := parsed["hns"]; ok {
|
||||
ep["dns_name"] = hns
|
||||
} else {
|
||||
ep["dns_name"] = a.Name + ".lthn"
|
||||
}
|
||||
|
||||
if caps, ok := parsed["cap"]; ok {
|
||||
ep["capabilities"] = splitComma(caps)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"gateways": endpoints,
|
||||
"count": len(endpoints),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetVPNGateways(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var vpns []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := parseComment(a.Comment)
|
||||
caps := parsed["cap"]
|
||||
if !core.Contains(caps, "vpn") { continue }
|
||||
|
||||
vpns = append(vpns, map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"dns_name": parsed["hns"],
|
||||
"capabilities": splitComma(caps),
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"vpn_gateways": vpns,
|
||||
"count": len(vpns),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetDNSGateways(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var dns []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := parseComment(a.Comment)
|
||||
caps := parsed["cap"]
|
||||
if !core.Contains(caps, "dns") { continue }
|
||||
|
||||
dns = append(dns, map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"dns_name": parsed["hns"],
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"dns_gateways": dns,
|
||||
"count": len(dns),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetNetworkTopology(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
|
||||
topology := map[string]int{
|
||||
"total_aliases": len(all),
|
||||
"gateways": 0,
|
||||
"services": 0,
|
||||
"vpn_capable": 0,
|
||||
"dns_capable": 0,
|
||||
"proxy_capable": 0,
|
||||
"exit_capable": 0,
|
||||
}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := parseComment(a.Comment)
|
||||
switch parsed["type"] {
|
||||
case "gateway":
|
||||
topology["gateways"]++
|
||||
case "service":
|
||||
topology["services"]++
|
||||
}
|
||||
caps := parsed["cap"]
|
||||
if core.Contains(caps, "vpn") { topology["vpn_capable"]++ }
|
||||
if core.Contains(caps, "dns") { topology["dns_capable"]++ }
|
||||
if core.Contains(caps, "proxy") { topology["proxy_capable"]++ }
|
||||
if core.Contains(caps, "exit") { topology["exit_capable"]++ }
|
||||
}
|
||||
|
||||
height, _ := s.chain.Height()
|
||||
topology["chain_height"] = int(height)
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"topology": topology,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func parseComment(comment string) map[string]string {
|
||||
parsed := make(map[string]string)
|
||||
for _, part := range splitSemicolon(comment) {
|
||||
idx := -1
|
||||
for i, c := range part { if c == '=' { idx = i; break } }
|
||||
if idx > 0 { parsed[part[:idx]] = part[idx+1:] }
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTTopology(w http.ResponseWriter, r *http.Request) {
|
||||
fakeReq := jsonRPCRequest{}
|
||||
s.rpcGetNetworkTopology(w, fakeReq)
|
||||
}
|
||||
|
||||
// --- Forge integration ---
|
||||
|
||||
func (s *Server) rpcGetForgeInfo(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"forge_url": "https://forge.lthn.ai",
|
||||
"org": "core",
|
||||
"repo": "go-blockchain",
|
||||
"dev_branch": "dev",
|
||||
"actions": []string{"publish_release", "create_issue", "dispatch_build", "chain_event"},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Metrics endpoint (Prometheus-compatible) ---
|
||||
|
||||
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
height, _ := s.chain.Height()
|
||||
_, meta := s.safeTopBlock()
|
||||
if meta == nil {
|
||||
meta = &chain.BlockMeta{}
|
||||
}
|
||||
aliases := s.chain.GetAllAliases()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
||||
|
||||
metrics := core.Concat(
|
||||
core.Sprintf("# HELP lethean_chain_height Current blockchain height\n"),
|
||||
core.Sprintf("# TYPE lethean_chain_height gauge\n"),
|
||||
core.Sprintf("lethean_chain_height %d\n\n", height),
|
||||
|
||||
core.Sprintf("# HELP lethean_difficulty Current mining difficulty\n"),
|
||||
core.Sprintf("# TYPE lethean_difficulty gauge\n"),
|
||||
core.Sprintf("lethean_difficulty %d\n\n", meta.Difficulty),
|
||||
|
||||
core.Sprintf("# HELP lethean_alias_count Number of registered aliases\n"),
|
||||
core.Sprintf("# TYPE lethean_alias_count gauge\n"),
|
||||
core.Sprintf("lethean_alias_count %d\n\n", len(aliases)),
|
||||
|
||||
core.Sprintf("# HELP lethean_cumulative_difficulty Cumulative chain difficulty\n"),
|
||||
core.Sprintf("# TYPE lethean_cumulative_difficulty counter\n"),
|
||||
core.Sprintf("lethean_cumulative_difficulty %d\n\n", meta.CumulativeDiff),
|
||||
|
||||
core.Sprintf("# HELP lethean_rpc_endpoints Number of API endpoints\n"),
|
||||
core.Sprintf("# TYPE lethean_rpc_endpoints gauge\n"),
|
||||
core.Sprintf("lethean_rpc_endpoints 101\n\n"),
|
||||
|
||||
core.Sprintf("# HELP lethean_node_info Node identification\n"),
|
||||
core.Sprintf("# TYPE lethean_node_info gauge\n"),
|
||||
core.Sprintf("lethean_node_info{version=\"0.4.0\",type=\"CoreChain/Go\"} 1\n"),
|
||||
)
|
||||
|
||||
w.Write([]byte(metrics))
|
||||
}
|
||||
|
||||
844
daemon/server.go
844
daemon/server.go
|
|
@ -12,7 +12,6 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core"
|
||||
|
||||
|
|
@ -21,7 +20,6 @@ import (
|
|||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wallet"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
// Server serves the Lethean daemon JSON-RPC API backed by a Go chain.
|
||||
|
|
@ -1467,848 +1465,6 @@ func (s *Server) rpcGetDifficultyHistory(w http.ResponseWriter, req jsonRPCReque
|
|||
})
|
||||
}
|
||||
|
||||
// --- REST-style HTTP endpoints (no JSON-RPC wrapper) ---
|
||||
|
||||
func (s *Server) handleRESTInfo(w http.ResponseWriter, r *http.Request) {
|
||||
height, _ := s.chain.Height()
|
||||
_, meta := s.safeTopBlock()
|
||||
if meta == nil {
|
||||
meta = &chain.BlockMeta{}
|
||||
}
|
||||
aliases := s.chain.GetAllAliases()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"height": height, "difficulty": meta.Difficulty,
|
||||
"aliases": len(aliases), "node": "CoreChain/Go",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTBlock(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("height")
|
||||
h, err := parseUint64(q)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "?height= required"})
|
||||
return
|
||||
}
|
||||
blk, meta, err := s.chain.GetBlockByHeight(h)
|
||||
if err != nil {
|
||||
w.WriteHeader(404)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"height": meta.Height, "hash": meta.Hash.String(),
|
||||
"timestamp": blk.Timestamp, "difficulty": meta.Difficulty,
|
||||
"tx_count": len(blk.TxHashes), "version": blk.MajorVersion,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTAliases(w http.ResponseWriter, r *http.Request) {
|
||||
aliases := s.chain.GetAllAliases()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(aliases)
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTAlias(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
w.WriteHeader(400)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "?name= required"})
|
||||
return
|
||||
}
|
||||
alias, err := s.chain.GetAlias(name)
|
||||
if err != nil {
|
||||
w.WriteHeader(404)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(alias)
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTSearch(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
if q == "" {
|
||||
w.WriteHeader(400)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "?q= required"})
|
||||
return
|
||||
}
|
||||
// Reuse the RPC search logic
|
||||
fakeReq := jsonRPCRequest{Params: json.RawMessage(core.Sprintf(`{"query":"%s"}`, q))}
|
||||
s.rpcSearch(w, fakeReq)
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTHealth(w http.ResponseWriter, r *http.Request) {
|
||||
height, _ := s.chain.Height()
|
||||
aliases := s.chain.GetAllAliases()
|
||||
status := "ok"
|
||||
if height == 0 {
|
||||
status = "syncing"
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": status, "height": height,
|
||||
"aliases": len(aliases), "node": "CoreChain/Go",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Server-Sent Events for real-time updates ---
|
||||
// (SSE is simpler than WebSocket and works with curl)
|
||||
|
||||
func (s *Server) handleSSEBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming not supported", 500)
|
||||
return
|
||||
}
|
||||
|
||||
lastHeight := uint64(0)
|
||||
ctx := r.Context()
|
||||
heartbeat := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
height, _ := s.chain.Height()
|
||||
if height > lastHeight && lastHeight > 0 {
|
||||
for h := lastHeight + 1; h <= height; h++ {
|
||||
blk, meta, err := s.chain.GetBlockByHeight(h)
|
||||
if err != nil { continue }
|
||||
data := core.Sprintf(`{"height":%d,"hash":"%s","timestamp":%d,"difficulty":%d,"tx_count":%d}`,
|
||||
meta.Height, meta.Hash.String(), blk.Timestamp, meta.Difficulty, len(blk.TxHashes))
|
||||
if _, err := w.Write([]byte(core.Sprintf("event: block\ndata: %s\n\n", data))); err != nil {
|
||||
return // client disconnected
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
heartbeat = 0
|
||||
}
|
||||
lastHeight = height
|
||||
|
||||
// Send keepalive every 30s (15 * 2s poll)
|
||||
heartbeat++
|
||||
if heartbeat >= 15 {
|
||||
if _, err := w.Write([]byte(": keepalive\n\n")); err != nil {
|
||||
return // client disconnected
|
||||
}
|
||||
flusher.Flush()
|
||||
heartbeat = 0
|
||||
}
|
||||
|
||||
// Poll every 2 seconds
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- SWAP & gateway service methods ---
|
||||
|
||||
func (s *Server) rpcGetAliasCapabilities(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
alias, err := s.chain.GetAlias(params.Alias)
|
||||
if err != nil {
|
||||
writeError(w, req.ID, -1, core.Sprintf("alias %s not found", params.Alias))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse v=lthn1;type=gateway;cap=vpn,dns,proxy;hns=charon.lthn
|
||||
parsed := make(map[string]string)
|
||||
for _, part := range splitSemicolon(alias.Comment) {
|
||||
idx := -1
|
||||
for i, c := range part {
|
||||
if c == '=' { idx = i; break }
|
||||
}
|
||||
if idx > 0 {
|
||||
parsed[part[:idx]] = part[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
var caps []string
|
||||
if c, ok := parsed["cap"]; ok {
|
||||
for _, part := range splitComma(c) {
|
||||
caps = append(caps, part)
|
||||
}
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"alias": alias.Name,
|
||||
"version": parsed["v"],
|
||||
"type": parsed["type"],
|
||||
"capabilities": caps,
|
||||
"hns": parsed["hns"],
|
||||
"raw_comment": alias.Comment,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func splitComma(s string) []string {
|
||||
var parts []string
|
||||
current := ""
|
||||
for _, c := range s {
|
||||
if c == ',' {
|
||||
if current != "" { parts = append(parts, current) }
|
||||
current = ""
|
||||
} else {
|
||||
current += string(c)
|
||||
}
|
||||
}
|
||||
if current != "" { parts = append(parts, current) }
|
||||
return parts
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetServiceEndpoints(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var endpoints []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := make(map[string]string)
|
||||
for _, part := range splitSemicolon(a.Comment) {
|
||||
idx := -1
|
||||
for i, c := range part { if c == '=' { idx = i; break } }
|
||||
if idx > 0 { parsed[part[:idx]] = part[idx+1:] }
|
||||
}
|
||||
|
||||
if parsed["type"] == "" { continue }
|
||||
|
||||
var caps []string
|
||||
if c, ok := parsed["cap"]; ok {
|
||||
for _, p := range splitComma(c) { caps = append(caps, p) }
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"type": parsed["type"],
|
||||
"capabilities": caps,
|
||||
"hns_name": parsed["hns"],
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"endpoints": endpoints,
|
||||
"count": len(endpoints),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetTotalCoins(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
height, _ := s.chain.Height()
|
||||
// Premine: 10M LTHN + 1 LTHN per block after genesis
|
||||
premine := uint64(10000000) // LTHN
|
||||
mined := height // 1 LTHN per block
|
||||
total := premine + mined
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"total_coins": total,
|
||||
"premine": premine,
|
||||
"mined": mined,
|
||||
"block_reward": 1,
|
||||
"height": height,
|
||||
"unit": "LTHN",
|
||||
"atomic_total": total * 1000000000000,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Self-documentation ---
|
||||
|
||||
func (s *Server) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
|
||||
methods := []map[string]string{
|
||||
{"method": "getinfo", "type": "chain", "desc": "Chain height, difficulty, alias count, hardfork status"},
|
||||
{"method": "getheight", "type": "chain", "desc": "Current block height"},
|
||||
{"method": "getblockcount", "type": "chain", "desc": "Total block count"},
|
||||
{"method": "getblockheaderbyheight", "type": "chain", "desc": "Block header by height"},
|
||||
{"method": "getblockheaderbyhash", "type": "chain", "desc": "Block header by hash"},
|
||||
{"method": "getlastblockheader", "type": "chain", "desc": "Latest block header"},
|
||||
{"method": "on_getblockhash", "type": "chain", "desc": "Block hash by height"},
|
||||
{"method": "get_blocks_details", "type": "chain", "desc": "Batch block details"},
|
||||
{"method": "get_block_hash_by_height", "type": "chain", "desc": "Height to hash"},
|
||||
{"method": "get_recent_blocks", "type": "chain", "desc": "Last N blocks"},
|
||||
{"method": "get_tx_details", "type": "chain", "desc": "Transaction details by hash"},
|
||||
{"method": "get_all_alias_details", "type": "alias", "desc": "All registered aliases"},
|
||||
{"method": "get_alias_details", "type": "alias", "desc": "Single alias by name"},
|
||||
{"method": "get_alias_by_address", "type": "alias", "desc": "Aliases for an address"},
|
||||
{"method": "get_aliases_by_type", "type": "alias", "desc": "Filter aliases by type"},
|
||||
{"method": "get_alias_capabilities", "type": "alias", "desc": "Parse alias v=lthn1 capabilities"},
|
||||
{"method": "get_alias_reward", "type": "alias", "desc": "Alias registration cost"},
|
||||
{"method": "get_gateways", "type": "discovery", "desc": "All gateway nodes with capabilities"},
|
||||
{"method": "get_service_endpoints", "type": "discovery", "desc": "All service endpoints"},
|
||||
{"method": "get_asset_info", "type": "asset", "desc": "Asset descriptor by ID or ticker"},
|
||||
{"method": "get_assets_list", "type": "asset", "desc": "All known assets"},
|
||||
{"method": "get_pool_info", "type": "chain", "desc": "Transaction pool info"},
|
||||
{"method": "getblockchaininfo", "type": "chain", "desc": "Full blockchain info"},
|
||||
{"method": "get_hardfork_status", "type": "chain", "desc": "Hardfork schedule with countdown"},
|
||||
{"method": "get_chain_stats", "type": "analytics", "desc": "Chain statistics and averages"},
|
||||
{"method": "get_difficulty_history", "type": "analytics", "desc": "Difficulty history for charts"},
|
||||
{"method": "get_total_coins", "type": "analytics", "desc": "Total coin supply calculation"},
|
||||
{"method": "get_est_height_from_date", "type": "chain", "desc": "Estimate height from timestamp"},
|
||||
{"method": "get_current_core_tx_expiration_median", "type": "chain", "desc": "TX expiration median"},
|
||||
{"method": "get_version", "type": "system", "desc": "Node version and type"},
|
||||
{"method": "get_node_info", "type": "system", "desc": "Node capabilities and stats"},
|
||||
{"method": "search", "type": "utility", "desc": "Universal search (block/tx/alias/address)"},
|
||||
{"method": "validate_signature", "type": "crypto", "desc": "Schnorr signature verification (native CGo)"},
|
||||
{"method": "generate_keys", "type": "crypto", "desc": "Ed25519 keypair generation (native CGo)"},
|
||||
{"method": "generate_key_image", "type": "crypto", "desc": "Key image from keypair (native CGo)"},
|
||||
{"method": "fast_hash", "type": "crypto", "desc": "Keccak-256 hash (native CGo)"},
|
||||
{"method": "check_key", "type": "crypto", "desc": "Validate Ed25519 public key (native CGo)"},
|
||||
{"method": "check_keyimages", "type": "crypto", "desc": "Check spent key images (native)"},
|
||||
{"method": "validate_address", "type": "crypto", "desc": "Validate iTHN address format (native)"},
|
||||
{"method": "make_integrated_address", "type": "crypto", "desc": "Encode integrated address (native)"},
|
||||
{"method": "split_integrated_address", "type": "crypto", "desc": "Decode integrated address (native)"},
|
||||
{"method": "marketplace_global_get_offers_ex", "type": "marketplace", "desc": "Marketplace offers"},
|
||||
{"method": "sendrawtransaction", "type": "relay", "desc": "Broadcast raw transaction"},
|
||||
}
|
||||
|
||||
// Count by type
|
||||
counts := make(map[string]int)
|
||||
for _, m := range methods {
|
||||
counts[m["type"]]++
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"node": "CoreChain/Go v0.3.0",
|
||||
"module": "dappco.re/go/core/blockchain",
|
||||
"rpc_methods": methods,
|
||||
"method_count": len(methods),
|
||||
"categories": counts,
|
||||
"http_endpoints": []string{
|
||||
"/json_rpc", "/getheight", "/start_mining", "/stop_mining",
|
||||
"/gettransactions", "/api/info", "/api/block", "/api/aliases",
|
||||
"/api/alias", "/api/search", "/health", "/events/blocks", "/openapi",
|
||||
},
|
||||
"wallet_proxy_methods": []string{
|
||||
"getbalance", "getaddress", "get_wallet_info", "transfer",
|
||||
"make_integrated_address", "split_integrated_address",
|
||||
"deploy_asset", "emit_asset", "burn_asset",
|
||||
"register_alias", "update_alias", "get_bulk_payments",
|
||||
"get_recent_txs_and_info", "store", "get_restore_info", "sign_message",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// --- Ring member selection (critical for native wallet tx construction) ---
|
||||
|
||||
func (s *Server) rpcGetRandomOuts(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Amounts []uint64 `json:"amounts"`
|
||||
Count int `json:"outs_count"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
if params.Count == 0 {
|
||||
params.Count = 15 // HF4 mandatory decoy set size
|
||||
}
|
||||
|
||||
var outs []map[string]interface{}
|
||||
for _, amount := range params.Amounts {
|
||||
totalOutputs, _ := s.chain.OutputCount(amount)
|
||||
if totalOutputs == 0 {
|
||||
outs = append(outs, map[string]interface{}{
|
||||
"amount": amount,
|
||||
"outs": []interface{}{},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Select random output indices
|
||||
var selected []map[string]interface{}
|
||||
step := totalOutputs / uint64(params.Count)
|
||||
if step == 0 { step = 1 }
|
||||
|
||||
for i := 0; i < params.Count && uint64(i)*step < totalOutputs; i++ {
|
||||
globalIdx := uint64(i) * step
|
||||
txHash, outIdx, err := s.chain.GetOutput(amount, globalIdx)
|
||||
if err != nil { continue }
|
||||
selected = append(selected, map[string]interface{}{
|
||||
"global_amount_index": globalIdx,
|
||||
"out_key": txHash.String(),
|
||||
"tx_out_index": outIdx,
|
||||
})
|
||||
}
|
||||
|
||||
outs = append(outs, map[string]interface{}{
|
||||
"amount": amount,
|
||||
"outs": selected,
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"outs": outs,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Admin methods ---
|
||||
|
||||
func (s *Server) rpcGetPeerList(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
// Go node doesn't maintain a peer list yet (sync-only, not P2P server)
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"white_list": []interface{}{},
|
||||
"gray_list": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetConnections(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"connections": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Binary endpoints (portable storage format) ---
|
||||
|
||||
func (s *Server) handleGetBlocksBin(w http.ResponseWriter, r *http.Request) {
|
||||
// Read request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse portable storage request
|
||||
// For now, serve JSON — binary format needs full portable storage integration
|
||||
// The p2p/encode.go has EncodeStorage but the HTTP binary format
|
||||
// uses a slightly different wrapper than P2P
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = body
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"note": "binary endpoint — use JSON-RPC get_blocks_details instead",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetOutputIndexesBin(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
_ = body
|
||||
|
||||
// Parse tx hash from request, return output global indexes
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"o_indexes": []uint64{},
|
||||
})
|
||||
}
|
||||
|
||||
// --- Remaining parity methods ---
|
||||
|
||||
func (s *Server) rpcGetAllPoolTxList(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"ids": []string{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetPoolTxsDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"txs": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetPoolTxsBriefDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"txs": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcResetTxPool(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
func (s *Server) rpcRemoveTxFromPool(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
func (s *Server) rpcForceRelay(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{"status": "OK"})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetMultisigInfo(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"multisig_outs": 0,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetAlternateBlocksDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"alt_blocks": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetVotes(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"votes": []interface{}{},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetMainBlockDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Height uint64 `json:"height"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
blk, meta, err := s.chain.GetBlockByHeight(params.Height)
|
||||
if err != nil {
|
||||
writeError(w, req.ID, -1, core.Sprintf("block not found at %d", params.Height))
|
||||
return
|
||||
}
|
||||
|
||||
txHashes := make([]string, len(blk.TxHashes))
|
||||
for i, h := range blk.TxHashes {
|
||||
txHashes[i] = h.String()
|
||||
}
|
||||
|
||||
minerTxHash := wire.TransactionHash(&blk.MinerTx).String()
|
||||
|
||||
// Check if block is PoS (bit 0 of flags)
|
||||
isPoS := blk.Flags&1 != 0
|
||||
blockType := "PoW"
|
||||
if isPoS {
|
||||
blockType = "PoS"
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"block_details": map[string]interface{}{
|
||||
"height": meta.Height,
|
||||
"hash": meta.Hash.String(),
|
||||
"prev_hash": blk.PrevID.String(),
|
||||
"timestamp": blk.Timestamp,
|
||||
"difficulty": meta.Difficulty,
|
||||
"cumulative_diff": meta.CumulativeDiff,
|
||||
"major_version": blk.MajorVersion,
|
||||
"minor_version": blk.MinorVersion,
|
||||
"nonce": blk.Nonce,
|
||||
"flags": blk.Flags,
|
||||
"block_type": blockType,
|
||||
"miner_tx_hash": minerTxHash,
|
||||
"tx_count": len(blk.TxHashes),
|
||||
"tx_hashes": txHashes,
|
||||
"base_reward": config.Coin, // 1 LTHN
|
||||
},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetAltBlockDetails(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
// No alt blocks in Go node (read-only, follows canonical chain)
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"block_details": map[string]interface{}{},
|
||||
"status": "NOT_FOUND",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Native wallet utility methods (no C++ wallet needed) ---
|
||||
|
||||
func (s *Server) rpcDerivePaymentID(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
// Generate a random 8-byte payment ID
|
||||
var id [8]byte
|
||||
pubKey, _, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
writeError(w, req.ID, -1, "random generation failed")
|
||||
return
|
||||
}
|
||||
copy(id[:], pubKey[:8])
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"payment_id": hex.EncodeToString(id[:]),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetAddressType(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
var params struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
parseParams(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
addr, prefix, err := types.DecodeAddress(params.Address)
|
||||
if err != nil {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"valid": false, "error": err.Error(), "status": "OK",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addrType := "standard"
|
||||
switch prefix {
|
||||
case 0x1eaf7: addrType = "standard"
|
||||
case 0xdeaf7: addrType = "integrated"
|
||||
case 0x3ceff7: addrType = "auditable"
|
||||
case 0x8b077: addrType = "auditable_integrated"
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"valid": true,
|
||||
"type": addrType,
|
||||
"prefix": prefix,
|
||||
"spend_key": hex.EncodeToString(addr.SpendPublicKey[:]),
|
||||
"view_key": hex.EncodeToString(addr.ViewPublicKey[:]),
|
||||
"flags": addr.Flags,
|
||||
"is_auditable": addr.Flags != 0,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetCoinSupply(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
height, _ := s.chain.Height()
|
||||
premine := uint64(10000000)
|
||||
mined := height
|
||||
total := premine + mined
|
||||
burned := uint64(0) // fees are burned post-HF4
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"total_supply": total,
|
||||
"circulating": total - burned,
|
||||
"premine": premine,
|
||||
"mined": mined,
|
||||
"burned_fees": burned,
|
||||
"block_reward": 1,
|
||||
"blocks_per_day": 720, // ~120s blocks, PoW+PoS alternating
|
||||
"daily_emission": 720,
|
||||
"annual_emission": 262800,
|
||||
"height": height,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetNetworkHashrate(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
height, _ := s.chain.Height()
|
||||
_, meta := s.safeTopBlock()
|
||||
if meta == nil {
|
||||
meta = &chain.BlockMeta{}
|
||||
}
|
||||
|
||||
// Hashrate ≈ difficulty / block_time
|
||||
hashrate := meta.Difficulty / 120
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"hashrate": hashrate,
|
||||
"difficulty": meta.Difficulty,
|
||||
"height": height,
|
||||
"avg_block_time": 120,
|
||||
"unit": "H/s",
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- LetherNet service layer (Go-exclusive, no C++ equivalent) ---
|
||||
|
||||
func (s *Server) rpcGetGatewayEndpoints(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var endpoints []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
if !core.Contains(a.Comment, "type=gateway") { continue }
|
||||
parsed := parseComment(a.Comment)
|
||||
|
||||
ep := map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"type": "gateway",
|
||||
}
|
||||
|
||||
if hns, ok := parsed["hns"]; ok {
|
||||
ep["dns_name"] = hns
|
||||
} else {
|
||||
ep["dns_name"] = a.Name + ".lthn"
|
||||
}
|
||||
|
||||
if caps, ok := parsed["cap"]; ok {
|
||||
ep["capabilities"] = splitComma(caps)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"gateways": endpoints,
|
||||
"count": len(endpoints),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetVPNGateways(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var vpns []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := parseComment(a.Comment)
|
||||
caps := parsed["cap"]
|
||||
if !core.Contains(caps, "vpn") { continue }
|
||||
|
||||
vpns = append(vpns, map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"dns_name": parsed["hns"],
|
||||
"capabilities": splitComma(caps),
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"vpn_gateways": vpns,
|
||||
"count": len(vpns),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetDNSGateways(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
var dns []map[string]interface{}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := parseComment(a.Comment)
|
||||
caps := parsed["cap"]
|
||||
if !core.Contains(caps, "dns") { continue }
|
||||
|
||||
dns = append(dns, map[string]interface{}{
|
||||
"alias": a.Name,
|
||||
"dns_name": parsed["hns"],
|
||||
})
|
||||
}
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"dns_gateways": dns,
|
||||
"count": len(dns),
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rpcGetNetworkTopology(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
all := s.chain.GetAllAliases()
|
||||
|
||||
topology := map[string]int{
|
||||
"total_aliases": len(all),
|
||||
"gateways": 0,
|
||||
"services": 0,
|
||||
"vpn_capable": 0,
|
||||
"dns_capable": 0,
|
||||
"proxy_capable": 0,
|
||||
"exit_capable": 0,
|
||||
}
|
||||
|
||||
for _, a := range all {
|
||||
parsed := parseComment(a.Comment)
|
||||
switch parsed["type"] {
|
||||
case "gateway":
|
||||
topology["gateways"]++
|
||||
case "service":
|
||||
topology["services"]++
|
||||
}
|
||||
caps := parsed["cap"]
|
||||
if core.Contains(caps, "vpn") { topology["vpn_capable"]++ }
|
||||
if core.Contains(caps, "dns") { topology["dns_capable"]++ }
|
||||
if core.Contains(caps, "proxy") { topology["proxy_capable"]++ }
|
||||
if core.Contains(caps, "exit") { topology["exit_capable"]++ }
|
||||
}
|
||||
|
||||
height, _ := s.chain.Height()
|
||||
topology["chain_height"] = int(height)
|
||||
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"topology": topology,
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func parseComment(comment string) map[string]string {
|
||||
parsed := make(map[string]string)
|
||||
for _, part := range splitSemicolon(comment) {
|
||||
idx := -1
|
||||
for i, c := range part { if c == '=' { idx = i; break } }
|
||||
if idx > 0 { parsed[part[:idx]] = part[idx+1:] }
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (s *Server) handleRESTTopology(w http.ResponseWriter, r *http.Request) {
|
||||
fakeReq := jsonRPCRequest{}
|
||||
s.rpcGetNetworkTopology(w, fakeReq)
|
||||
}
|
||||
|
||||
// --- Forge integration ---
|
||||
|
||||
func (s *Server) rpcGetForgeInfo(w http.ResponseWriter, req jsonRPCRequest) {
|
||||
writeResult(w, req.ID, map[string]interface{}{
|
||||
"forge_url": "https://forge.lthn.ai",
|
||||
"org": "core",
|
||||
"repo": "go-blockchain",
|
||||
"dev_branch": "dev",
|
||||
"actions": []string{"publish_release", "create_issue", "dispatch_build", "chain_event"},
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// --- Metrics endpoint (Prometheus-compatible) ---
|
||||
|
||||
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
height, _ := s.chain.Height()
|
||||
_, meta := s.safeTopBlock()
|
||||
if meta == nil {
|
||||
meta = &chain.BlockMeta{}
|
||||
}
|
||||
aliases := s.chain.GetAllAliases()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
||||
|
||||
metrics := core.Concat(
|
||||
core.Sprintf("# HELP lethean_chain_height Current blockchain height\n"),
|
||||
core.Sprintf("# TYPE lethean_chain_height gauge\n"),
|
||||
core.Sprintf("lethean_chain_height %d\n\n", height),
|
||||
|
||||
core.Sprintf("# HELP lethean_difficulty Current mining difficulty\n"),
|
||||
core.Sprintf("# TYPE lethean_difficulty gauge\n"),
|
||||
core.Sprintf("lethean_difficulty %d\n\n", meta.Difficulty),
|
||||
|
||||
core.Sprintf("# HELP lethean_alias_count Number of registered aliases\n"),
|
||||
core.Sprintf("# TYPE lethean_alias_count gauge\n"),
|
||||
core.Sprintf("lethean_alias_count %d\n\n", len(aliases)),
|
||||
|
||||
core.Sprintf("# HELP lethean_cumulative_difficulty Cumulative chain difficulty\n"),
|
||||
core.Sprintf("# TYPE lethean_cumulative_difficulty counter\n"),
|
||||
core.Sprintf("lethean_cumulative_difficulty %d\n\n", meta.CumulativeDiff),
|
||||
|
||||
core.Sprintf("# HELP lethean_rpc_endpoints Number of API endpoints\n"),
|
||||
core.Sprintf("# TYPE lethean_rpc_endpoints gauge\n"),
|
||||
core.Sprintf("lethean_rpc_endpoints 101\n\n"),
|
||||
|
||||
core.Sprintf("# HELP lethean_node_info Node identification\n"),
|
||||
core.Sprintf("# TYPE lethean_node_info gauge\n"),
|
||||
core.Sprintf("lethean_node_info{version=\"0.4.0\",type=\"CoreChain/Go\"} 1\n"),
|
||||
)
|
||||
|
||||
w.Write([]byte(metrics))
|
||||
}
|
||||
|
||||
// safeTopBlock returns TopBlock or zero values if chain is empty.
|
||||
func (s *Server) safeTopBlock() (*types.Block, *chain.BlockMeta) {
|
||||
blk, meta, err := s.chain.TopBlock()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue