Files
dchain/node/api_routes.go
vsecoder 88848efa63 feat(chain): remove channels, add social feed (Phase A of v2.0.0)
Replaces the channel/membership model with a VK/Twitter-style feed:
public posts, follow graph, likes. Views are deliberately off-chain
(counted by the hosting relay, Phase B).

Removed
  - EventCreateChannel, EventAddMember
  - CreateChannelPayload, AddMemberPayload, ChannelMember
  - prefixChannel, prefixChanMember
  - chain.Channel(), chain.ChannelMembers()
  - node/api_channels.go
  - GetChannel, GetChannelMembers on ExplorerQuery

Added
  - Events: CREATE_POST, DELETE_POST, FOLLOW, UNFOLLOW, LIKE_POST, UNLIKE_POST
  - Payloads: CreatePostPayload, DeletePostPayload, FollowPayload,
    UnfollowPayload, LikePostPayload, UnlikePostPayload
  - Stored shape: PostRecord (author, size, hash, hosting relay, timestamp,
    reply/quote refs, soft-delete flag, fee paid)
  - State prefixes: post:, postbyauthor:, follow:, followin:, like:, likecount:
  - Queries: Post(), PostsByAuthor(), Following(), Followers(),
    LikeCount(), HasLiked()
  - Cached like counter via bumpLikeCount helper

Pricing
  - BasePostFee = 1000 µT (aligned with MinFee block-validation floor)
  - PostByteFee = 1 µT/byte of compressed content
  - Total fee credited in full to HostingRelay pub (storage compensation)
  - MaxPostSize = 256 KiB

Integrity
  - CREATE_POST validates content_hash length (32 B) and size range
  - DELETE_POST restricted to post.Author
  - Duplicate FOLLOW / LIKE rejected
  - reply_to and quote_of mutually exclusive

Tests
  - TestFeedCreatePost: post stored, indexed, host credited
  - TestFeedInsufficientFee: underpaid post is skipped
  - TestFeedFollowUnfollow: follow graph round-trips via forward + inbound indices
  - TestFeedLikeUnlike: like toggles with dedup, counter stays accurate
  - TestFeedDeletePostByOther: non-author deletion rejected

This is Phase A (chain-layer). Phase B adds the relay feed-mailbox
(post bodies + gossipsub) and HTTP endpoints. Phase C adds the client
Feed tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:36:00 +03:00

264 lines
11 KiB
Go

// Package node - chain explorer HTTP API and minimal web UI.
package node
import (
"net/http"
"go-blockchain/blockchain"
)
// ConnectedPeerRef is an entry returned by /api/peers — one currently-connected
// libp2p peer. Mirrors p2p.ConnectedConnectedPeerRef but kept in the node package
// so api routes don't pull the p2p package directly.
type ConnectedPeerRef struct {
ID string `json:"id"`
Addrs []string `json:"addrs"`
// Version is the peer's last-seen version announce (from gossipsub topic
// dchain/version/v1). Empty when the peer hasn't announced yet — either
// it's running an older binary without gossip, or it hasn't reached its
// first publish tick (up to 60s after connect).
Version *PeerVersionRef `json:"version,omitempty"`
}
// PeerVersionRef mirrors p2p.PeerVersion in a package-local type so
// api_routes doesn't import p2p directly.
type PeerVersionRef struct {
Tag string `json:"tag"`
Commit string `json:"commit"`
ProtocolVersion int `json:"protocol_version"`
Timestamp int64 `json:"timestamp"`
ReceivedAt string `json:"received_at,omitempty"`
}
// NativeContractInfo is the shape ExplorerQuery.NativeContracts returns.
// Passed from main.go (which has the blockchain package imported) to the
// well-known endpoint below so it can merge native contracts with WASM ones.
type NativeContractInfo struct {
ContractID string
ABIJson string
}
// ExplorerQuery holds all functions the explorer API needs to read chain state
// and accept new transactions.
type ExplorerQuery struct {
GetBlock func(index uint64) (*blockchain.Block, error)
GetTx func(txID string) (*blockchain.TxRecord, error)
AddressToPubKey func(addr string) (string, error)
Balance func(pubKey string) (uint64, error)
Reputation func(pubKey string) (blockchain.RepStats, error)
WalletBinding func(pubKey string) (string, error)
TxsByAddress func(pubKey string, limit, offset int) ([]*blockchain.TxRecord, error)
RecentBlocks func(limit int) ([]*blockchain.Block, error)
RecentTxs func(limit int) ([]*blockchain.TxRecord, error)
NetStats func() (blockchain.NetStats, error)
RegisteredRelays func() ([]blockchain.RegisteredRelayInfo, error)
IdentityInfo func(pubKeyOrAddr string) (*blockchain.IdentityInfo, error)
ValidatorSet func() ([]string, error)
SubmitTx func(tx *blockchain.Transaction) error
// ConnectedPeers (optional) returns the local libp2p view of currently
// connected peers. Used by /api/peers and /api/network-info so new nodes
// can bootstrap from any existing peer's view of the network. May be nil
// if the node binary is built without p2p (tests).
ConnectedPeers func() []ConnectedPeerRef
// ChainID (optional) returns a stable identifier for this chain so a
// joiner can sanity-check it's syncing from the right network.
ChainID func() string
// NativeContracts (optional) returns the list of built-in Go contracts
// registered on this node. These appear in /api/well-known-contracts
// alongside WASM contracts, so the client doesn't need to distinguish.
NativeContracts func() []NativeContractInfo
GetContract func(contractID string) (*blockchain.ContractRecord, error)
GetContracts func() ([]blockchain.ContractRecord, error)
GetContractState func(contractID, key string) ([]byte, error)
GetContractLogs func(contractID string, limit int) ([]blockchain.ContractLogEntry, error)
Stake func(pubKey string) (uint64, error)
GetToken func(tokenID string) (*blockchain.TokenRecord, error)
GetTokens func() ([]blockchain.TokenRecord, error)
TokenBalance func(tokenID, pubKey string) (uint64, error)
GetNFT func(nftID string) (*blockchain.NFTRecord, error)
GetNFTs func() ([]blockchain.NFTRecord, error)
NFTsByOwner func(ownerPub string) ([]blockchain.NFTRecord, error)
// Events is the SSE hub for the live event stream. Optional — if nil the
// /api/events endpoint returns 501 Not Implemented.
Events *SSEHub
// WS is the websocket hub for low-latency push to mobile/desktop clients.
// Optional — if nil the /api/ws endpoint returns 501.
WS *WSHub
}
// ExplorerRouteFlags toggles the optional HTML frontend surfaces. API
// endpoints (/api/*) always register — these flags only affect static pages.
type ExplorerRouteFlags struct {
// DisableUI suppresses the embedded block explorer at `/`, `/address`,
// `/tx`, `/node`, `/relays`, `/validators`, `/contract`, `/tokens`,
// `/token`, and their `/assets/explorer/*.js|css` dependencies. Useful
// for JSON-API-only deployments (headless nodes, mobile-backend nodes).
DisableUI bool
// DisableSwagger suppresses `/swagger` and `/swagger/openapi.json`.
// Useful for hardened private deployments where even API documentation
// shouldn't be exposed. Does NOT affect the JSON API itself.
DisableSwagger bool
}
// RegisterExplorerRoutes adds all explorer API, chain API and docs routes to mux.
// The variadic flags parameter is optional — passing none (or an empty struct)
// registers the full surface (UI + Swagger + JSON API) for backwards compatibility.
func RegisterExplorerRoutes(mux *http.ServeMux, q ExplorerQuery, flags ...ExplorerRouteFlags) {
var f ExplorerRouteFlags
if len(flags) > 0 {
f = flags[0]
}
if !f.DisableUI {
registerExplorerPages(mux)
}
registerExplorerAPI(mux, q)
registerChainAPI(mux, q)
registerContractAPI(mux, q)
registerWellKnownAPI(mux, q)
registerWellKnownVersionAPI(mux, q)
registerUpdateCheckAPI(mux, q)
registerOnboardingAPI(mux, q)
registerTokenAPI(mux, q)
if !f.DisableSwagger {
registerSwaggerRoutes(mux)
}
}
// RegisterRelayRoutes adds relay mailbox HTTP endpoints to mux.
// Call this after RegisterExplorerRoutes with the relay config.
func RegisterRelayRoutes(mux *http.ServeMux, rc RelayConfig) {
if rc.Mailbox == nil {
return
}
registerRelayRoutes(mux, rc)
}
func registerExplorerPages(mux *http.ServeMux) {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
serveExplorerIndex(w, r)
})
mux.HandleFunc("/address", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/address" {
http.NotFound(w, r)
return
}
serveExplorerAddressPage(w, r)
})
mux.HandleFunc("/tx", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tx" {
http.NotFound(w, r)
return
}
serveExplorerTxPage(w, r)
})
mux.HandleFunc("/node", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/node" {
http.NotFound(w, r)
return
}
serveExplorerNodePage(w, r)
})
mux.HandleFunc("/relays", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/relays" {
http.NotFound(w, r)
return
}
serveExplorerRelaysPage(w, r)
})
mux.HandleFunc("/validators", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/validators" {
http.NotFound(w, r)
return
}
serveExplorerValidatorsPage(w, r)
})
mux.HandleFunc("/assets/explorer/style.css", serveExplorerCSS)
mux.HandleFunc("/assets/explorer/common.js", serveExplorerCommonJS)
mux.HandleFunc("/assets/explorer/app.js", serveExplorerJS)
mux.HandleFunc("/assets/explorer/address.js", serveExplorerAddressJS)
mux.HandleFunc("/assets/explorer/tx.js", serveExplorerTxJS)
mux.HandleFunc("/assets/explorer/node.js", serveExplorerNodeJS)
mux.HandleFunc("/assets/explorer/relays.js", serveExplorerRelaysJS)
mux.HandleFunc("/assets/explorer/validators.js", serveExplorerValidatorsJS)
mux.HandleFunc("/contract", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/contract" {
http.NotFound(w, r)
return
}
serveExplorerContractPage(w, r)
})
mux.HandleFunc("/assets/explorer/contract.js", serveExplorerContractJS)
mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tokens" {
http.NotFound(w, r)
return
}
serveExplorerTokensPage(w, r)
})
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/token" {
http.NotFound(w, r)
return
}
serveExplorerTokenPage(w, r)
})
mux.HandleFunc("/assets/explorer/tokens.js", serveExplorerTokensJS)
mux.HandleFunc("/assets/explorer/token.js", serveExplorerTokenJS)
}
func registerExplorerAPI(mux *http.ServeMux, q ExplorerQuery) {
mux.HandleFunc("/api/netstats", apiNetStats(q))
mux.HandleFunc("/api/blocks", apiRecentBlocks(q))
mux.HandleFunc("/api/txs/recent", apiRecentTxs(q)) // GET /api/txs/recent?limit=20
mux.HandleFunc("/api/block/", apiBlock(q)) // GET /api/block/{index}
mux.HandleFunc("/api/tx/", apiTxByID(q)) // GET /api/tx/{txid}
mux.HandleFunc("/api/address/", apiAddress(q)) // GET /api/address/{addr}
mux.HandleFunc("/api/node/", apiNode(q)) // GET /api/node/{pubkey|DC...}
mux.HandleFunc("/api/relays", apiRelays(q)) // GET /api/relays
mux.HandleFunc("/api/identity/", apiIdentity(q)) // GET /api/identity/{pubkey|addr}
mux.HandleFunc("/api/validators", apiValidators(q))// GET /api/validators
mux.HandleFunc("/api/tx", withWriteTokenGuard(withSubmitTxGuards(apiSubmitTx(q)))) // POST /api/tx (body size + per-IP rate limit + optional token gate)
// Live event stream (SSE) — GET /api/events
mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
if q.Events == nil {
http.Error(w, "event stream not available", http.StatusNotImplemented)
return
}
q.Events.ServeHTTP(w, r)
})
// WebSocket gateway — GET /api/ws (upgrades to ws://). Low-latency push
// for clients that would otherwise poll balance/inbox/contacts.
mux.HandleFunc("/api/ws", func(w http.ResponseWriter, r *http.Request) {
if q.WS == nil {
http.Error(w, "websocket not available", http.StatusNotImplemented)
return
}
q.WS.ServeHTTP(w, r)
})
// Prometheus exposition endpoint — scraped by a Prometheus server. Has
// no auth; operators running this in public should put the node behind
// a reverse proxy that restricts /metrics to trusted scrapers only.
mux.HandleFunc("/metrics", metricsHandler)
}
func registerTokenAPI(mux *http.ServeMux, q ExplorerQuery) {
mux.HandleFunc("/api/tokens", apiTokens(q)) // GET /api/tokens
mux.HandleFunc("/api/tokens/", apiTokenByID(q)) // GET /api/tokens/{id} and /api/tokens/{id}/balance/{pubkey}
mux.HandleFunc("/api/nfts", apiNFTs(q)) // GET /api/nfts
mux.HandleFunc("/api/nfts/", apiNFTByID(q)) // GET /api/nfts/{id} and /api/nfts/owner/{pubkey}
}
func registerChainAPI(mux *http.ServeMux, q ExplorerQuery) {
// Similar to blockchain APIs, but only with features supported by this chain.
mux.HandleFunc("/v2/chain/accounts/", apiV2ChainAccountTransactions(q)) // GET /v2/chain/accounts/{account_id}/transactions
mux.HandleFunc("/v2/chain/transactions/", apiV2ChainTxByID(q)) // GET /v2/chain/transactions/{tx_id}
mux.HandleFunc("/v2/chain/transactions/draft", apiV2ChainDraftTx()) // POST /v2/chain/transactions/draft
mux.HandleFunc("/v2/chain/transactions", withWriteTokenGuard(withSubmitTxGuards(apiV2ChainSendTx(q)))) // POST /v2/chain/transactions (body size + rate limit + optional token gate)
}