chore: initial commit for v0.0.1

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
This commit is contained in:
vsecoder
2026-04-17 14:16:44 +03:00
commit 7e7393e4f8
196 changed files with 55947 additions and 0 deletions

183
node/api_tokens.go Normal file
View File

@@ -0,0 +1,183 @@
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)
}
}