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
184 lines
4.2 KiB
Go
184 lines
4.2 KiB
Go
package node
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"go-blockchain/blockchain"
|
|
"go-blockchain/wallet"
|
|
)
|
|
|
|
// GET /api/nfts — list all NFTs.
|
|
func apiNFTs(q ExplorerQuery) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if q.GetNFTs == nil {
|
|
jsonErr(w, fmt.Errorf("NFT query not available"), 503)
|
|
return
|
|
}
|
|
nfts, err := q.GetNFTs()
|
|
if err != nil {
|
|
jsonErr(w, err, 500)
|
|
return
|
|
}
|
|
if nfts == nil {
|
|
nfts = []blockchain.NFTRecord{}
|
|
}
|
|
jsonOK(w, map[string]any{
|
|
"count": len(nfts),
|
|
"nfts": nfts,
|
|
})
|
|
}
|
|
}
|
|
|
|
// GET /api/nfts/{id} — single NFT metadata
|
|
// GET /api/nfts/owner/{pubkey} — NFTs owned by address
|
|
func apiNFTByID(q ExplorerQuery) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/nfts/")
|
|
parts := strings.SplitN(path, "/", 2)
|
|
|
|
// /api/nfts/owner/{pubkey}
|
|
if parts[0] == "owner" {
|
|
if len(parts) < 2 || parts[1] == "" {
|
|
jsonErr(w, fmt.Errorf("pubkey required"), 400)
|
|
return
|
|
}
|
|
pubKey := parts[1]
|
|
if strings.HasPrefix(pubKey, "DC") && q.AddressToPubKey != nil {
|
|
if pk, err := q.AddressToPubKey(pubKey); err == nil && pk != "" {
|
|
pubKey = pk
|
|
}
|
|
}
|
|
if q.NFTsByOwner == nil {
|
|
jsonErr(w, fmt.Errorf("NFT query not available"), 503)
|
|
return
|
|
}
|
|
nfts, err := q.NFTsByOwner(pubKey)
|
|
if err != nil {
|
|
jsonErr(w, err, 500)
|
|
return
|
|
}
|
|
if nfts == nil {
|
|
nfts = []blockchain.NFTRecord{}
|
|
}
|
|
jsonOK(w, map[string]any{"count": len(nfts), "nfts": nfts})
|
|
return
|
|
}
|
|
|
|
// /api/nfts/{id}
|
|
nftID := parts[0]
|
|
if nftID == "" {
|
|
jsonErr(w, fmt.Errorf("NFT ID required"), 400)
|
|
return
|
|
}
|
|
if q.GetNFT == nil {
|
|
jsonErr(w, fmt.Errorf("NFT query not available"), 503)
|
|
return
|
|
}
|
|
rec, err := q.GetNFT(nftID)
|
|
if err != nil {
|
|
jsonErr(w, err, 500)
|
|
return
|
|
}
|
|
if rec == nil {
|
|
jsonErr(w, fmt.Errorf("NFT %s not found", nftID), 404)
|
|
return
|
|
}
|
|
// Attach owner address for convenience.
|
|
ownerAddr := ""
|
|
if rec.Owner != "" {
|
|
ownerAddr = wallet.PubKeyToAddress(rec.Owner)
|
|
}
|
|
jsonOK(w, map[string]any{
|
|
"nft": rec,
|
|
"owner_address": ownerAddr,
|
|
})
|
|
}
|
|
}
|
|
|
|
// GET /api/tokens — list all issued tokens.
|
|
func apiTokens(q ExplorerQuery) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if q.GetTokens == nil {
|
|
jsonErr(w, fmt.Errorf("token query not available"), 503)
|
|
return
|
|
}
|
|
tokens, err := q.GetTokens()
|
|
if err != nil {
|
|
jsonErr(w, err, 500)
|
|
return
|
|
}
|
|
if tokens == nil {
|
|
tokens = []blockchain.TokenRecord{}
|
|
}
|
|
jsonOK(w, map[string]any{
|
|
"count": len(tokens),
|
|
"tokens": tokens,
|
|
})
|
|
}
|
|
}
|
|
|
|
// GET /api/tokens/{id} — token metadata
|
|
// GET /api/tokens/{id}/balance/{pub} — token balance for a public key or DC address
|
|
func apiTokenByID(q ExplorerQuery) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/tokens/")
|
|
parts := strings.SplitN(path, "/", 3)
|
|
|
|
tokenID := parts[0]
|
|
if tokenID == "" {
|
|
jsonErr(w, fmt.Errorf("token ID required"), 400)
|
|
return
|
|
}
|
|
|
|
// /api/tokens/{id}/balance/{pub}
|
|
if len(parts) == 3 && parts[1] == "balance" {
|
|
pubOrAddr := parts[2]
|
|
if pubOrAddr == "" {
|
|
jsonErr(w, fmt.Errorf("pubkey or address required"), 400)
|
|
return
|
|
}
|
|
// Resolve DC address to pubkey if needed.
|
|
pubKey := pubOrAddr
|
|
if strings.HasPrefix(pubOrAddr, "DC") && q.AddressToPubKey != nil {
|
|
if pk, err := q.AddressToPubKey(pubOrAddr); err == nil && pk != "" {
|
|
pubKey = pk
|
|
}
|
|
}
|
|
if q.TokenBalance == nil {
|
|
jsonErr(w, fmt.Errorf("token balance query not available"), 503)
|
|
return
|
|
}
|
|
bal, err := q.TokenBalance(tokenID, pubKey)
|
|
if err != nil {
|
|
jsonErr(w, err, 500)
|
|
return
|
|
}
|
|
jsonOK(w, map[string]any{
|
|
"token_id": tokenID,
|
|
"pub_key": pubKey,
|
|
"address": wallet.PubKeyToAddress(pubKey),
|
|
"balance": bal,
|
|
})
|
|
return
|
|
}
|
|
|
|
// /api/tokens/{id}
|
|
if q.GetToken == nil {
|
|
jsonErr(w, fmt.Errorf("token query not available"), 503)
|
|
return
|
|
}
|
|
rec, err := q.GetToken(tokenID)
|
|
if err != nil {
|
|
jsonErr(w, err, 500)
|
|
return
|
|
}
|
|
if rec == nil {
|
|
jsonErr(w, fmt.Errorf("token %s not found", tokenID), 404)
|
|
return
|
|
}
|
|
jsonOK(w, rec)
|
|
}
|
|
}
|