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/: // "" → contract info // "/state/" → state value // "/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 }