go-blockchain/daemon/server.go
Claude ff00a29e08
Some checks failed
Security Scan / security (push) Successful in 13s
Test / Test (push) Failing after 34s
feat(daemon): add 6 more RPC methods — 17 total
New methods: getblockheaderbyhash, on_getblockhash, get_tx_details,
get_blocks_details, get_alias_reward, get_est_height_from_date

Total: 17 JSON-RPC methods + 3 HTTP endpoints = 20 API endpoints.
Covers all read operations the explorer, LNS, and status bot need.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-02 01:34:06 +01:00

551 lines
14 KiB
Go

// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
// Package daemon provides a JSON-RPC server backed by the Go chain.
package daemon
import (
"encoding/json"
"io"
"net/http"
"dappco.re/go/core"
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/types"
)
// Server serves the Lethean daemon JSON-RPC API backed by a Go chain.
//
// server := daemon.NewServer(myChain, myConfig)
// http.ListenAndServe(":46941", server)
type Server struct {
chain *chain.Chain
config *config.ChainConfig
mux *http.ServeMux
}
// NewServer creates a JSON-RPC server for the given chain.
//
// server := daemon.NewServer(c, cfg)
func NewServer(c *chain.Chain, cfg *config.ChainConfig) *Server {
s := &Server{chain: c, config: cfg, mux: http.NewServeMux()}
s.mux.HandleFunc("/json_rpc", s.handleJSONRPC)
s.mux.HandleFunc("/getheight", s.handleGetHeight)
s.mux.HandleFunc("/start_mining", s.handleStartMining)
s.mux.HandleFunc("/stop_mining", s.handleStopMining)
return s
}
// ServeHTTP implements http.Handler.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
s.mux.ServeHTTP(w, r)
}
type jsonRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
type jsonRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Result interface{} `json:"result,omitempty"`
Error *rpcErr `json:"error,omitempty"`
}
type rpcErr struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (s *Server) handleJSONRPC(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, nil, -32700, "parse error")
return
}
var req jsonRPCRequest
if err := json.Unmarshal(body, &req); err != nil {
writeError(w, nil, -32700, "parse error")
return
}
w.Header().Set("Content-Type", "application/json")
switch req.Method {
case "getinfo":
s.rpcGetInfo(w, req)
case "getheight":
s.rpcGetHeight(w, req)
case "getblockheaderbyheight":
s.rpcGetBlockHeaderByHeight(w, req)
case "getlastblockheader":
s.rpcGetLastBlockHeader(w, req)
case "get_all_alias_details":
s.rpcGetAllAliasDetails(w, req)
case "get_alias_details":
s.rpcGetAliasDetails(w, req)
case "getblockcount":
s.rpcGetBlockCount(w, req)
case "get_alias_by_address":
s.rpcGetAliasByAddress(w, req)
case "getblockheaderbyhash":
s.rpcGetBlockHeaderByHash(w, req)
case "on_getblockhash":
s.rpcOnGetBlockHash(w, req)
case "get_tx_details":
s.rpcGetTxDetails(w, req)
case "get_blocks_details":
s.rpcGetBlocksDetails(w, req)
case "get_alias_reward":
s.rpcGetAliasReward(w, req)
case "get_est_height_from_date":
s.rpcGetEstHeightFromDate(w, req)
case "get_asset_info":
s.rpcGetAssetInfo(w, req)
default:
writeError(w, req.ID, -32601, core.Sprintf("method %s not found", req.Method))
}
}
func (s *Server) handleGetHeight(w http.ResponseWriter, r *http.Request) {
height, _ := s.chain.Height()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"height": height,
"status": "OK",
})
}
func (s *Server) rpcGetInfo(w http.ResponseWriter, req jsonRPCRequest) {
height, _ := s.chain.Height()
_, meta, _ := s.chain.TopBlock()
aliases := s.chain.GetAllAliases()
result := map[string]interface{}{
"height": height,
"difficulty": meta.Difficulty,
"alias_count": len(aliases),
"tx_pool_size": 0,
"daemon_network_state": 2,
"status": "OK",
"pos_allowed": height > 0,
"is_hardfok_active": buildHardforkArray(height, s.config),
}
writeResult(w, req.ID, result)
}
func (s *Server) rpcGetHeight(w http.ResponseWriter, req jsonRPCRequest) {
height, _ := s.chain.Height()
writeResult(w, req.ID, map[string]interface{}{
"height": height,
"status": "OK",
})
}
func (s *Server) rpcGetBlockHeaderByHeight(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Height uint64 `json:"height"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
blk, meta, err := s.chain.GetBlockByHeight(params.Height)
if err != nil {
writeError(w, req.ID, -1, core.Sprintf("block not found at height %d", params.Height))
return
}
header := map[string]interface{}{
"hash": meta.Hash.String(),
"height": meta.Height,
"timestamp": blk.Timestamp,
"difficulty": core.Sprintf("%d", meta.Difficulty),
"major_version": blk.MajorVersion,
"minor_version": blk.MinorVersion,
"nonce": blk.Nonce,
"prev_hash": blk.PrevID.String(),
}
writeResult(w, req.ID, map[string]interface{}{
"block_header": header,
"status": "OK",
})
}
func (s *Server) rpcGetLastBlockHeader(w http.ResponseWriter, req jsonRPCRequest) {
blk, meta, err := s.chain.TopBlock()
if err != nil {
writeError(w, req.ID, -1, "no blocks")
return
}
header := map[string]interface{}{
"hash": meta.Hash.String(),
"height": meta.Height,
"timestamp": blk.Timestamp,
"difficulty": core.Sprintf("%d", meta.Difficulty),
"major_version": blk.MajorVersion,
}
writeResult(w, req.ID, map[string]interface{}{
"block_header": header,
"status": "OK",
})
}
func (s *Server) rpcGetAllAliasDetails(w http.ResponseWriter, req jsonRPCRequest) {
aliases := s.chain.GetAllAliases()
result := make([]map[string]string, len(aliases))
for i, a := range aliases {
result[i] = map[string]string{
"alias": a.Name,
"address": a.Address,
"comment": a.Comment,
}
}
writeResult(w, req.ID, map[string]interface{}{
"aliases": result,
"status": "OK",
})
}
func buildHardforkArray(height uint64, cfg *config.ChainConfig) []bool {
var forks []config.HardFork
if cfg.IsTestnet {
forks = config.TestnetForks
} else {
forks = config.MainnetForks
}
result := make([]bool, 7)
for _, f := range forks {
if f.Version < 7 {
result[f.Version] = height >= f.Height
}
}
return result
}
func writeResult(w http.ResponseWriter, id json.RawMessage, result interface{}) {
json.NewEncoder(w).Encode(jsonRPCResponse{
JSONRPC: "2.0",
ID: id,
Result: result,
})
}
func writeError(w http.ResponseWriter, id json.RawMessage, code int, message string) {
json.NewEncoder(w).Encode(jsonRPCResponse{
JSONRPC: "2.0",
ID: id,
Error: &rpcErr{Code: code, Message: message},
})
}
func (s *Server) rpcGetAliasDetails(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Alias string `json:"alias"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
alias, err := s.chain.GetAlias(params.Alias)
if err != nil {
writeError(w, req.ID, -1, core.Sprintf("alias %s not found", params.Alias))
return
}
writeResult(w, req.ID, map[string]interface{}{
"alias_details": map[string]string{
"alias": alias.Name,
"address": alias.Address,
"comment": alias.Comment,
},
"status": "OK",
})
}
func (s *Server) rpcGetAliasCount(w http.ResponseWriter, req jsonRPCRequest) {
aliases := s.chain.GetAllAliases()
writeResult(w, req.ID, map[string]interface{}{
"count": len(aliases),
"status": "OK",
})
}
func (s *Server) rpcGetBlockCount(w http.ResponseWriter, req jsonRPCRequest) {
height, _ := s.chain.Height()
writeResult(w, req.ID, map[string]interface{}{
"count": height,
"status": "OK",
})
}
func (s *Server) rpcGetAssetInfo(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
AssetID string `json:"asset_id"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
// For the native LTHN asset, return hardcoded descriptor
if params.AssetID == "LTHN" || params.AssetID == "d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a" {
writeResult(w, req.ID, map[string]interface{}{
"asset_descriptor": map[string]interface{}{
"ticker": "LTHN",
"full_name": "Lethean",
"total_max_supply": 0,
"current_supply": 0,
"decimal_point": 12,
"hidden_supply": false,
},
"asset_id": "d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a",
"status": "OK",
})
return
}
// For other assets, return not found (until asset index is built)
writeError(w, req.ID, -1, core.Sprintf("asset %s not found", params.AssetID))
}
func (s *Server) rpcGetAliasByAddress(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Address string `json:"address"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
// Search all aliases for matching address
aliases := s.chain.GetAllAliases()
var matches []map[string]string
for _, a := range aliases {
if a.Address == params.Address {
matches = append(matches, map[string]string{
"alias": a.Name,
"address": a.Address,
"comment": a.Comment,
})
}
}
writeResult(w, req.ID, map[string]interface{}{
"alias_info_list": matches,
"status": "OK",
})
}
// Legacy HTTP endpoints (non-JSON-RPC, used by mining scripts and monitoring)
func (s *Server) handleStartMining(w http.ResponseWriter, r *http.Request) {
// The Go daemon doesn't mine — forward to C++ daemon or return info
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "NOT MINING",
"note": "Go daemon is read-only. Use C++ daemon for mining.",
})
}
func (s *Server) handleStopMining(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
}
// --- Additional RPC methods ---
func (s *Server) rpcGetBlockHeaderByHash(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Hash string `json:"hash"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
blockHash, hashErr := types.HashFromHex(params.Hash)
if hashErr != nil {
writeError(w, req.ID, -1, core.Sprintf("invalid block hash: %s", params.Hash))
return
}
blk, meta, err := s.chain.GetBlockByHash(blockHash)
if err != nil {
writeError(w, req.ID, -1, core.Sprintf("block not found: %s", params.Hash))
return
}
writeResult(w, req.ID, map[string]interface{}{
"block_header": map[string]interface{}{
"hash": meta.Hash.String(),
"height": meta.Height,
"timestamp": blk.Timestamp,
"difficulty": core.Sprintf("%d", meta.Difficulty),
"major_version": blk.MajorVersion,
"minor_version": blk.MinorVersion,
"nonce": blk.Nonce,
"prev_hash": blk.PrevID.String(),
},
"status": "OK",
})
}
func (s *Server) rpcOnGetBlockHash(w http.ResponseWriter, req jsonRPCRequest) {
var params []uint64
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
if len(params) == 0 {
writeError(w, req.ID, -1, "height required")
return
}
_, meta, err := s.chain.GetBlockByHeight(params[0])
if err != nil {
writeError(w, req.ID, -1, core.Sprintf("block not found at %d", params[0]))
return
}
writeResult(w, req.ID, meta.Hash.String())
}
func (s *Server) rpcGetTxDetails(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
TxHash string `json:"tx_hash"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
txHash, hashErr := types.HashFromHex(params.TxHash)
if hashErr != nil {
writeError(w, req.ID, -1, core.Sprintf("invalid tx hash: %s", params.TxHash))
return
}
tx, txMeta, err := s.chain.GetTransaction(txHash)
if err != nil {
writeError(w, req.ID, -1, core.Sprintf("tx not found: %s", params.TxHash))
return
}
writeResult(w, req.ID, map[string]interface{}{
"tx_info": map[string]interface{}{
"id": params.TxHash,
"keeper_block": txMeta.KeeperBlock,
"amount": 0,
"fee": 0,
"ins": len(tx.Vin),
"outs": len(tx.Vout),
"version": tx.Version,
},
"status": "OK",
})
}
func (s *Server) rpcGetBlocksDetails(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
HeightStart uint64 `json:"height_start"`
Count uint64 `json:"count"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
if params.Count == 0 {
params.Count = 10
}
if params.Count > 100 {
params.Count = 100
}
height, _ := s.chain.Height()
var blocks []map[string]interface{}
for h := params.HeightStart; h < params.HeightStart+params.Count && h < height; h++ {
blk, meta, err := s.chain.GetBlockByHeight(h)
if err != nil {
continue
}
blocks = append(blocks, map[string]interface{}{
"height": meta.Height,
"hash": meta.Hash.String(),
"timestamp": blk.Timestamp,
"difficulty": meta.Difficulty,
"major_version": blk.MajorVersion,
"tx_count": len(blk.TxHashes),
})
}
writeResult(w, req.ID, map[string]interface{}{
"blocks": blocks,
"status": "OK",
})
}
func (s *Server) rpcGetAliasReward(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Alias string `json:"alias"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
// Alias registration costs 1 LTHN (constexpr in currency_config.h)
writeResult(w, req.ID, map[string]interface{}{
"reward": 1000000000000, // 1 LTHN in atomic units
"status": "OK",
})
}
func (s *Server) rpcGetEstHeightFromDate(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Timestamp uint64 `json:"timestamp"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
// Estimate: genesis timestamp + (height * 120s avg block time)
height, _ := s.chain.Height()
_, meta, _ := s.chain.TopBlock()
if meta.Height == 0 || params.Timestamp == 0 {
writeResult(w, req.ID, map[string]interface{}{"height": 0, "status": "OK"})
return
}
// Get genesis timestamp
genesis, _, _ := s.chain.GetBlockByHeight(0)
genesisTs := genesis.Timestamp
if params.Timestamp <= genesisTs {
writeResult(w, req.ID, map[string]interface{}{"height": 0, "status": "OK"})
return
}
// Linear estimate: (target_ts - genesis_ts) / avg_block_time
elapsed := params.Timestamp - genesisTs
avgBlockTime := (meta.Timestamp - genesisTs) / meta.Height
if avgBlockTime == 0 {
avgBlockTime = 120
}
estimatedHeight := elapsed / avgBlockTime
if estimatedHeight > height {
estimatedHeight = height
}
writeResult(w, req.ID, map[string]interface{}{
"height": estimatedHeight,
"status": "OK",
})
}