diff --git a/daemon/http_handlers.go b/daemon/http_handlers.go new file mode 100644 index 0000000..7db1896 --- /dev/null +++ b/daemon/http_handlers.go @@ -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)) +} + diff --git a/daemon/server.go b/daemon/server.go index bcd3bff..9c8c169 100644 --- a/daemon/server.go +++ b/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()