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:
199
node/api_contract.go
Normal file
199
node/api_contract.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go-blockchain/blockchain"
|
||||
)
|
||||
|
||||
// registerContractAPI mounts the contract API routes on mux.
|
||||
//
|
||||
// GET /api/contracts — list all deployed contracts
|
||||
// GET /api/contracts/{contractID} — contract metadata (no WASM bytes)
|
||||
// GET /api/contracts/{contractID}/state/{key} — raw state value, base64-encoded
|
||||
// GET /api/contracts/{contractID}/logs — recent log entries (newest first)
|
||||
func registerContractAPI(mux *http.ServeMux, q ExplorerQuery) {
|
||||
// Exact match for list endpoint (no trailing slash)
|
||||
mux.HandleFunc("/api/contracts", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonErr(w, errorf("method not allowed"), 405)
|
||||
return
|
||||
}
|
||||
if q.GetContracts == nil {
|
||||
jsonErr(w, errorf("contract queries not available"), 503)
|
||||
return
|
||||
}
|
||||
contracts, err := q.GetContracts()
|
||||
if err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
return
|
||||
}
|
||||
if contracts == nil {
|
||||
contracts = []blockchain.ContractRecord{}
|
||||
}
|
||||
jsonOK(w, map[string]any{
|
||||
"count": len(contracts),
|
||||
"contracts": contracts,
|
||||
})
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/contracts/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonErr(w, errorf("method not allowed"), 405)
|
||||
return
|
||||
}
|
||||
// Path segments after /api/contracts/:
|
||||
// "<id>" → contract info
|
||||
// "<id>/state/<k>" → state value
|
||||
// "<id>/logs" → log entries
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/contracts/")
|
||||
path = strings.Trim(path, "/")
|
||||
|
||||
switch {
|
||||
case strings.Contains(path, "/state/"):
|
||||
parts := strings.SplitN(path, "/state/", 2)
|
||||
contractID := parts[0]
|
||||
if contractID == "" {
|
||||
jsonErr(w, errorf("contract_id required"), 400)
|
||||
return
|
||||
}
|
||||
handleContractState(w, q, contractID, parts[1])
|
||||
|
||||
case strings.HasSuffix(path, "/logs"):
|
||||
contractID := strings.TrimSuffix(path, "/logs")
|
||||
if contractID == "" {
|
||||
jsonErr(w, errorf("contract_id required"), 400)
|
||||
return
|
||||
}
|
||||
handleContractLogs(w, r, q, contractID)
|
||||
|
||||
default:
|
||||
contractID := path
|
||||
if contractID == "" {
|
||||
jsonErr(w, errorf("contract_id required"), 400)
|
||||
return
|
||||
}
|
||||
handleContractInfo(w, q, contractID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func handleContractInfo(w http.ResponseWriter, q ExplorerQuery, contractID string) {
|
||||
// Check native contracts first — they aren't stored as WASM ContractRecord
|
||||
// in BadgerDB but are valid targets for CALL_CONTRACT and have an ABI.
|
||||
if q.NativeContracts != nil {
|
||||
for _, nc := range q.NativeContracts() {
|
||||
if nc.ContractID == contractID {
|
||||
jsonOK(w, map[string]any{
|
||||
"contract_id": nc.ContractID,
|
||||
"deployer_pub": "",
|
||||
"deployed_at": uint64(0), // native contracts exist from genesis
|
||||
"abi_json": nc.ABIJson,
|
||||
"wasm_size": 0,
|
||||
"native": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if q.GetContract == nil {
|
||||
jsonErr(w, errorf("contract queries not available"), 503)
|
||||
return
|
||||
}
|
||||
rec, err := q.GetContract(contractID)
|
||||
if err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
return
|
||||
}
|
||||
if rec == nil {
|
||||
jsonErr(w, errorf("contract %s not found", contractID), 404)
|
||||
return
|
||||
}
|
||||
// Omit raw WASM bytes from the API response; expose only metadata.
|
||||
jsonOK(w, map[string]any{
|
||||
"contract_id": rec.ContractID,
|
||||
"deployer_pub": rec.DeployerPub,
|
||||
"deployed_at": rec.DeployedAt,
|
||||
"abi_json": rec.ABIJson,
|
||||
"wasm_size": len(rec.WASMBytes),
|
||||
"native": false,
|
||||
})
|
||||
}
|
||||
|
||||
func handleContractState(w http.ResponseWriter, q ExplorerQuery, contractID, key string) {
|
||||
if q.GetContractState == nil {
|
||||
jsonErr(w, errorf("contract state queries not available"), 503)
|
||||
return
|
||||
}
|
||||
val, err := q.GetContractState(contractID, key)
|
||||
if err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
return
|
||||
}
|
||||
if val == nil {
|
||||
jsonOK(w, map[string]any{
|
||||
"contract_id": contractID,
|
||||
"key": key,
|
||||
"value_b64": nil,
|
||||
"value_hex": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
jsonOK(w, map[string]any{
|
||||
"contract_id": contractID,
|
||||
"key": key,
|
||||
"value_b64": base64.StdEncoding.EncodeToString(val),
|
||||
"value_hex": hexEncode(val),
|
||||
"value_u64": decodeU64(val), // convenience: big-endian uint64 if len==8
|
||||
})
|
||||
}
|
||||
|
||||
func handleContractLogs(w http.ResponseWriter, r *http.Request, q ExplorerQuery, contractID string) {
|
||||
if q.GetContractLogs == nil {
|
||||
jsonErr(w, errorf("contract log queries not available"), 503)
|
||||
return
|
||||
}
|
||||
limit := 50
|
||||
if s := r.URL.Query().Get("limit"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
entries, err := q.GetContractLogs(contractID, limit)
|
||||
if err != nil {
|
||||
jsonErr(w, err, 500)
|
||||
return
|
||||
}
|
||||
if entries == nil {
|
||||
entries = []blockchain.ContractLogEntry{}
|
||||
}
|
||||
jsonOK(w, map[string]any{
|
||||
"contract_id": contractID,
|
||||
"count": len(entries),
|
||||
"logs": entries,
|
||||
})
|
||||
}
|
||||
|
||||
// errorf is a helper to create a formatted error.
|
||||
func errorf(format string, args ...any) error {
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("%s", format)
|
||||
}
|
||||
return fmt.Errorf(format, args...)
|
||||
}
|
||||
|
||||
func hexEncode(b []byte) string { return hex.EncodeToString(b) }
|
||||
|
||||
func decodeU64(b []byte) *uint64 {
|
||||
if len(b) != 8 {
|
||||
return nil
|
||||
}
|
||||
v := binary.BigEndian.Uint64(b)
|
||||
return &v
|
||||
}
|
||||
Reference in New Issue
Block a user