- wire: add readExtraAliasEntry (tag 33) + readExtraAliasEntryOld (tag 20) Unblocks HF4+ Zarcanum transaction deserialization - chain: alias extraction from transaction extra fields (14/14 on testnet) - chain: alias storage and retrieval via go-store - daemon: JSON-RPC server (getinfo, getheight, getblockheaderbyheight, getlastblockheader, get_all_alias_details, get_alias_details) - cmd: `chain serve` — sync from seed + serve RPC simultaneously - cmd: `wallet create` — generate keys, iTHN address, 24-word seed - cmd: `wallet address` — show base58-encoded iTHN address - cmd: `wallet seed` — show mnemonic seed phrase - cmd: `wallet scan` — scan chain for owned outputs via ECDH derivation Tested: 11,263 blocks synced in 3m11s from live testnet. All HF0-HF4 validated. 14 aliases extracted. RPC serves correct heights, difficulties, block headers, and alias data. Wallet generates valid iTHN addresses. Co-Authored-By: Charon <charon@lethean.io>
265 lines
6.6 KiB
Go
265 lines
6.6 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"
|
|
)
|
|
|
|
// 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)
|
|
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)
|
|
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, ¶ms)
|
|
}
|
|
|
|
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, ¶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
|
|
}
|
|
|
|
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",
|
|
})
|
|
}
|