package node import ( "encoding/hex" "encoding/json" "fmt" "net/http" "strconv" "strings" "go-blockchain/blockchain" "go-blockchain/economy" "go-blockchain/wallet" ) type txListEntry struct { ID string `json:"id"` Type blockchain.EventType `json:"type"` Memo string `json:"memo,omitempty"` From string `json:"from"` FromAddr string `json:"from_addr,omitempty"` To string `json:"to,omitempty"` ToAddr string `json:"to_addr,omitempty"` Amount uint64 `json:"amount_ut"` AmountDisp string `json:"amount"` Fee uint64 `json:"fee_ut"` FeeDisp string `json:"fee"` Time string `json:"time"` BlockIndex uint64 `json:"block_index"` BlockHash string `json:"block_hash,omitempty"` } func asTxListEntry(rec *blockchain.TxRecord) txListEntry { tx := rec.Tx out := txListEntry{ ID: tx.ID, Type: tx.Type, Memo: txMemo(tx), From: tx.From, To: tx.To, Amount: tx.Amount, AmountDisp: economy.FormatTokens(tx.Amount), Fee: tx.Fee, FeeDisp: economy.FormatTokens(tx.Fee), Time: tx.Timestamp.UTC().Format("2006-01-02T15:04:05Z"), BlockIndex: rec.BlockIndex, BlockHash: rec.BlockHash, } if tx.From != "" { out.FromAddr = wallet.PubKeyToAddress(tx.From) } if tx.To != "" { out.ToAddr = wallet.PubKeyToAddress(tx.To) } return out } func apiNetStats(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { stats, err := q.NetStats() if err != nil { jsonErr(w, err, 500) return } jsonOK(w, stats) } } func apiRecentBlocks(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { limit := queryInt(r, "limit", 20) blocks, err := q.RecentBlocks(limit) if err != nil { jsonErr(w, err, 500) return } type blockSummary struct { Index uint64 `json:"index"` Hash string `json:"hash"` Time string `json:"time"` Validator string `json:"validator"` TxCount int `json:"tx_count"` TotalFees uint64 `json:"total_fees_ut"` } out := make([]blockSummary, len(blocks)) for i, b := range blocks { out[i] = blockSummary{ Index: b.Index, Hash: b.HashHex(), Time: b.Timestamp.UTC().Format("2006-01-02T15:04:05Z"), Validator: wallet.PubKeyToAddress(b.Validator), TxCount: len(b.Transactions), TotalFees: b.TotalFees, } } jsonOK(w, out) } } func apiRecentTxs(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { limit := queryInt(r, "limit", 20) recs, err := q.RecentTxs(limit) if err != nil { jsonErr(w, err, 500) return } out := make([]txListEntry, len(recs)) for i := range recs { out[i] = asTxListEntry(recs[i]) } jsonOK(w, out) } } func apiBlock(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { idxStr := strings.TrimPrefix(r.URL.Path, "/api/block/") idx, err := strconv.ParseUint(idxStr, 10, 64) if err != nil { jsonErr(w, fmt.Errorf("invalid block index: %s", idxStr), 400) return } b, err := q.GetBlock(idx) if err != nil { jsonErr(w, err, 404) return } type txSummary struct { ID string `json:"id"` Type blockchain.EventType `json:"type"` Memo string `json:"memo,omitempty"` From string `json:"from"` To string `json:"to,omitempty"` Amount uint64 `json:"amount_ut,omitempty"` Fee uint64 `json:"fee_ut"` } type blockDetail struct { Index uint64 `json:"index"` Hash string `json:"hash"` PrevHash string `json:"prev_hash"` Time string `json:"time"` Validator string `json:"validator"` ValidatorAddr string `json:"validator_addr"` TxCount int `json:"tx_count"` TotalFees uint64 `json:"total_fees_ut"` Transactions []txSummary `json:"transactions"` } txs := make([]txSummary, len(b.Transactions)) for i, tx := range b.Transactions { txs[i] = txSummary{ ID: tx.ID, Type: tx.Type, Memo: txMemo(tx), From: tx.From, To: tx.To, Amount: tx.Amount, Fee: tx.Fee, } } jsonOK(w, blockDetail{ Index: b.Index, Hash: b.HashHex(), PrevHash: fmt.Sprintf("%x", b.PrevHash), Time: b.Timestamp.UTC().Format("2006-01-02T15:04:05Z"), Validator: b.Validator, ValidatorAddr: wallet.PubKeyToAddress(b.Validator), TxCount: len(b.Transactions), TotalFees: b.TotalFees, Transactions: txs, }) } } func apiTxByID(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { txID := strings.TrimPrefix(r.URL.Path, "/api/tx/") if txID == "" { jsonErr(w, fmt.Errorf("tx id required"), 400) return } rec, err := q.GetTx(txID) if err != nil { jsonErr(w, err, 500) return } if rec == nil { jsonErr(w, fmt.Errorf("transaction not found"), 404) return } type txDetail struct { ID string `json:"id"` Type blockchain.EventType `json:"type"` Memo string `json:"memo,omitempty"` From string `json:"from"` FromAddr string `json:"from_addr,omitempty"` To string `json:"to,omitempty"` ToAddr string `json:"to_addr,omitempty"` Amount uint64 `json:"amount_ut"` AmountDisp string `json:"amount"` Fee uint64 `json:"fee_ut"` FeeDisp string `json:"fee"` Time string `json:"time"` BlockIndex uint64 `json:"block_index"` BlockHash string `json:"block_hash"` BlockTime string `json:"block_time"` GasUsed uint64 `json:"gas_used,omitempty"` Payload any `json:"payload,omitempty"` PayloadHex string `json:"payload_hex,omitempty"` SignatureHex string `json:"signature_hex,omitempty"` } tx := rec.Tx payload, payloadHex := decodeTxPayload(tx.Payload) out := txDetail{ ID: tx.ID, Type: tx.Type, Memo: txMemo(tx), From: tx.From, To: tx.To, Amount: tx.Amount, AmountDisp: economy.FormatTokens(tx.Amount), Fee: tx.Fee, FeeDisp: economy.FormatTokens(tx.Fee), Time: tx.Timestamp.UTC().Format("2006-01-02T15:04:05Z"), BlockIndex: rec.BlockIndex, BlockHash: rec.BlockHash, BlockTime: rec.BlockTime.UTC().Format("2006-01-02T15:04:05Z"), GasUsed: rec.GasUsed, Payload: payload, PayloadHex: payloadHex, SignatureHex: hex.EncodeToString(tx.Signature), } if tx.From != "" { out.FromAddr = wallet.PubKeyToAddress(tx.From) } if tx.To != "" { out.ToAddr = wallet.PubKeyToAddress(tx.To) } jsonOK(w, out) } } func apiAddress(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { addr := strings.TrimPrefix(r.URL.Path, "/api/address/") if addr == "" { jsonErr(w, fmt.Errorf("address required"), 400) return } pubKey, err := resolveAccountID(q, addr) if err != nil { jsonErr(w, err, 404) return } limit := queryInt(r, "limit", 50) offset := queryIntMin0(r, "offset") bal, err := q.Balance(pubKey) if err != nil { jsonErr(w, err, 500) return } txs, err := q.TxsByAddress(pubKey, limit, offset) if err != nil { jsonErr(w, err, 500) return } type txEntry struct { ID string `json:"id"` Type blockchain.EventType `json:"type"` Memo string `json:"memo,omitempty"` From string `json:"from"` FromAddr string `json:"from_addr,omitempty"` To string `json:"to,omitempty"` ToAddr string `json:"to_addr,omitempty"` Amount uint64 `json:"amount_ut"` AmountDisp string `json:"amount"` Fee uint64 `json:"fee_ut"` Time string `json:"time"` BlockIndex uint64 `json:"block_index"` } entries := make([]txEntry, len(txs)) for i, rec := range txs { tx := rec.Tx entries[i] = txEntry{ ID: tx.ID, Type: tx.Type, Memo: txMemo(tx), From: tx.From, To: tx.To, Amount: tx.Amount, AmountDisp: economy.FormatTokens(tx.Amount), Fee: tx.Fee, Time: tx.Timestamp.UTC().Format("2006-01-02T15:04:05Z"), BlockIndex: rec.BlockIndex, } if tx.From != "" { entries[i].FromAddr = wallet.PubKeyToAddress(tx.From) } if tx.To != "" { entries[i].ToAddr = wallet.PubKeyToAddress(tx.To) } } type addrResp struct { Address string `json:"address"` PubKey string `json:"pub_key"` BalanceMicroT uint64 `json:"balance_ut"` Balance string `json:"balance"` TxCount int `json:"tx_count"` Offset int `json:"offset"` Limit int `json:"limit"` HasMore bool `json:"has_more"` NextOffset int `json:"next_offset"` Transactions []txEntry `json:"transactions"` } hasMore := len(entries) == limit jsonOK(w, addrResp{ Address: wallet.PubKeyToAddress(pubKey), PubKey: pubKey, BalanceMicroT: bal, Balance: economy.FormatTokens(bal), TxCount: len(entries), Offset: offset, Limit: limit, HasMore: hasMore, NextOffset: offset + len(entries), Transactions: entries, }) } } func apiNode(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { input := strings.TrimPrefix(r.URL.Path, "/api/node/") if input == "" { jsonErr(w, fmt.Errorf("node id required"), 400) return } pubKey, err := resolveAccountID(q, input) if err != nil { jsonErr(w, err, 404) return } rep, err := q.Reputation(pubKey) if err != nil { jsonErr(w, err, 500) return } nodeBal, err := q.Balance(pubKey) if err != nil { jsonErr(w, err, 500) return } walletPubKey, err := q.WalletBinding(pubKey) if err != nil { jsonErr(w, err, 500) return } var walletAddr string var walletBalance uint64 if walletPubKey != "" { walletAddr = wallet.PubKeyToAddress(walletPubKey) walletBalance, _ = q.Balance(walletPubKey) } window := queryInt(r, "window", 200) recentBlocks, err := q.RecentBlocks(window) if err != nil { jsonErr(w, err, 500) return } recentProduced := 0 var recentRewardsUT uint64 for _, b := range recentBlocks { if b.Validator != pubKey { continue } recentProduced++ recentRewardsUT += b.TotalFees } type nodeResp struct { QueryInput string `json:"query_input"` PubKey string `json:"pub_key"` Address string `json:"address"` NodeBalanceUT uint64 `json:"node_balance_ut"` NodeBalance string `json:"node_balance"` WalletBindingPubKey string `json:"wallet_binding_pub_key,omitempty"` WalletBindingAddress string `json:"wallet_binding_address,omitempty"` WalletBindingBalanceUT uint64 `json:"wallet_binding_balance_ut,omitempty"` WalletBindingBalance string `json:"wallet_binding_balance,omitempty"` ReputationScore int64 `json:"reputation_score"` ReputationRank string `json:"reputation_rank"` BlocksProduced uint64 `json:"blocks_produced"` RelayProofs uint64 `json:"relay_proofs"` SlashCount uint64 `json:"slash_count"` Heartbeats uint64 `json:"heartbeats"` LifetimeBaseRewardUT uint64 `json:"lifetime_base_reward_ut"` LifetimeBaseReward string `json:"lifetime_base_reward"` RecentWindowBlocks int `json:"recent_window_blocks"` RecentBlocksProduced int `json:"recent_blocks_produced"` RecentRewardsUT uint64 `json:"recent_rewards_ut"` RecentRewards string `json:"recent_rewards"` } jsonOK(w, nodeResp{ QueryInput: input, PubKey: pubKey, Address: wallet.PubKeyToAddress(pubKey), NodeBalanceUT: nodeBal, NodeBalance: economy.FormatTokens(nodeBal), WalletBindingPubKey: walletPubKey, WalletBindingAddress: walletAddr, WalletBindingBalanceUT: walletBalance, WalletBindingBalance: economy.FormatTokens(walletBalance), ReputationScore: rep.Score, ReputationRank: rep.Rank(), BlocksProduced: rep.BlocksProduced, RelayProofs: rep.RelayProofs, SlashCount: rep.SlashCount, Heartbeats: rep.Heartbeats, LifetimeBaseRewardUT: 0, LifetimeBaseReward: economy.FormatTokens(0), RecentWindowBlocks: len(recentBlocks), RecentBlocksProduced: recentProduced, RecentRewardsUT: recentRewardsUT, RecentRewards: economy.FormatTokens(recentRewardsUT), }) } } func apiRelays(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if q.RegisteredRelays == nil { jsonOK(w, []any{}) return } relays, err := q.RegisteredRelays() if err != nil { jsonErr(w, err, 500) return } if relays == nil { relays = []blockchain.RegisteredRelayInfo{} } jsonOK(w, relays) } } func apiValidators(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if q.ValidatorSet == nil { jsonOK(w, []any{}) return } validators, err := q.ValidatorSet() if err != nil { jsonErr(w, err, 500) return } type validatorEntry struct { PubKey string `json:"pub_key"` Address string `json:"address"` Staked uint64 `json:"staked_ut,omitempty"` } out := make([]validatorEntry, len(validators)) for i, pk := range validators { var staked uint64 if q.Stake != nil { staked, _ = q.Stake(pk) } out[i] = validatorEntry{ PubKey: pk, Address: wallet.PubKeyToAddress(pk), Staked: staked, } } jsonOK(w, map[string]any{ "count": len(out), "validators": out, }) } } func apiIdentity(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { input := strings.TrimPrefix(r.URL.Path, "/api/identity/") if input == "" { jsonErr(w, fmt.Errorf("pubkey or address required"), 400) return } if q.IdentityInfo == nil { jsonErr(w, fmt.Errorf("identity lookup not available"), 503) return } // Resolve DC address → pubkey if needed. pubKey, err := resolveAccountID(q, input) if err != nil { jsonErr(w, err, 404) return } info, err := q.IdentityInfo(pubKey) if err != nil { jsonErr(w, err, 500) return } jsonOK(w, info) } } func apiSubmitTx(q ExplorerQuery) http.HandlerFunc { // The returned handler is wrapped in withSubmitTxGuards() by the caller: // body size is capped at MaxTxRequestBytes and per-IP rate limiting is // applied upstream (see api_guards.go). This function therefore only // handles the semantics — shape, signature, timestamp window, dispatch. return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } var tx blockchain.Transaction if err := json.NewDecoder(r.Body).Decode(&tx); err != nil { jsonErr(w, fmt.Errorf("invalid JSON: %w", err), 400) return } // Reject txs with an obviously-bad clock value before we even verify // the signature — cheaper failure, and cuts a replay window against // long-rotated dedup caches. if err := ValidateTxTimestamp(&tx); err != nil { MetricTxSubmitRejected.Inc() jsonErr(w, fmt.Errorf("bad timestamp: %w", err), 400) return } if err := verifyTransactionSignature(&tx); err != nil { MetricTxSubmitRejected.Inc() jsonErr(w, err, 400) return } if err := q.SubmitTx(&tx); err != nil { MetricTxSubmitRejected.Inc() jsonErr(w, err, 500) return } MetricTxSubmitAccepted.Inc() jsonOK(w, map[string]string{"id": tx.ID, "status": "accepted"}) } }