DChain single-node blockchain + React Native messenger client. Core: - PBFT consensus with multi-sig validator admission + equivocation slashing - BadgerDB + schema migration scaffold (CurrentSchemaVersion=0) - libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1) - Native Go contracts (username_registry) alongside WASM (wazero) - WebSocket gateway with topic-based fanout + Ed25519-nonce auth - Relay mailbox with NaCl envelope encryption (X25519 + Ed25519) - Prometheus /metrics, per-IP rate limit, body-size cap Deployment: - Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus - 3-node dev compose (docker-compose.yml) with mocked internet topology - 3-validator prod compose (deploy/prod/) for federation - Auto-update from Gitea via /api/update-check + systemd timer - Build-time version injection (ldflags → node --version) - UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER) Client (client-app/): - Expo / React Native / NativeWind - E2E NaCl encryption, typing indicator, contact requests - Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch Documentation: - README.md, CHANGELOG.md, CONTEXT.md - deploy/single/README.md with 6 operator scenarios - deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design - docs/contracts/*.md per contract
271 lines
12 KiB
Go
271 lines
12 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)
|
|
|
|
// Channel group-messaging lookups (R1). GetChannel returns metadata;
|
|
// GetChannelMembers returns the Ed25519 pubkey of every current member.
|
|
// Both may be nil on nodes that don't expose channel state (tests).
|
|
GetChannel func(channelID string) (*blockchain.CreateChannelPayload, error)
|
|
GetChannelMembers func(channelID string) ([]string, 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)
|
|
registerChannelAPI(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)
|
|
}
|