Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
391
backend/api/track1/endpoints.go
Normal file
391
backend/api/track1/endpoints.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package track1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Server handles Track 1 endpoints
|
||||
type Server struct {
|
||||
rpcGateway *RPCGateway
|
||||
}
|
||||
|
||||
// NewServer creates a new Track 1 server
|
||||
func NewServer(rpcGateway *RPCGateway) *Server {
|
||||
return &Server{
|
||||
rpcGateway: rpcGateway,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleLatestBlocks handles GET /api/v1/track1/blocks/latest
|
||||
func (s *Server) HandleLatestBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
limit := 10
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
// Get latest block number
|
||||
blockNumResp, err := s.rpcGateway.GetBlockNumber(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "rpc_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
blockNumHex, ok := blockNumResp.Result.(string)
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block number response")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse block number
|
||||
blockNum, err := hexToInt(blockNumHex)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "parse_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch blocks
|
||||
blocks := []map[string]interface{}{}
|
||||
for i := 0; i < limit && blockNum-int64(i) >= 0; i++ {
|
||||
blockNumStr := fmt.Sprintf("0x%x", blockNum-int64(i))
|
||||
blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, false)
|
||||
if err != nil {
|
||||
continue // Skip failed blocks
|
||||
}
|
||||
|
||||
blockData, ok := blockResp.Result.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Transform to our format
|
||||
block := transformBlock(blockData)
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"data": blocks,
|
||||
"pagination": map[string]interface{}{
|
||||
"page": 1,
|
||||
"limit": limit,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleLatestTransactions handles GET /api/v1/track1/txs/latest
|
||||
func (s *Server) HandleLatestTransactions(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
limit := 10
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
// Get latest block number
|
||||
blockNumResp, err := s.rpcGateway.GetBlockNumber(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "rpc_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
blockNumHex, ok := blockNumResp.Result.(string)
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block number response")
|
||||
return
|
||||
}
|
||||
|
||||
blockNum, err := hexToInt(blockNumHex)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "parse_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch transactions from recent blocks
|
||||
transactions := []map[string]interface{}{}
|
||||
for i := 0; i < 20 && len(transactions) < limit && blockNum-int64(i) >= 0; i++ {
|
||||
blockNumStr := fmt.Sprintf("0x%x", blockNum-int64(i))
|
||||
blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, true)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
blockData, ok := blockResp.Result.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
txs, ok := blockData["transactions"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tx := range txs {
|
||||
if len(transactions) >= limit {
|
||||
break
|
||||
}
|
||||
txData, ok := tx.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
transactions = append(transactions, transformTransaction(txData))
|
||||
}
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"data": transactions,
|
||||
"pagination": map[string]interface{}{
|
||||
"page": 1,
|
||||
"limit": limit,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleBlockDetail handles GET /api/v1/track1/block/:number
|
||||
func (s *Server) HandleBlockDetail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/block/")
|
||||
blockNumStr := fmt.Sprintf("0x%x", parseBlockNumber(path))
|
||||
|
||||
blockResp, err := s.rpcGateway.GetBlockByNumber(r.Context(), blockNumStr, false)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "not_found", "Block not found")
|
||||
return
|
||||
}
|
||||
|
||||
blockData, ok := blockResp.Result.(map[string]interface{})
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid block response")
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"data": transformBlock(blockData),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleTransactionDetail handles GET /api/v1/track1/tx/:hash
|
||||
func (s *Server) HandleTransactionDetail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/tx/")
|
||||
txHash := path
|
||||
|
||||
txResp, err := s.rpcGateway.GetTransactionByHash(r.Context(), txHash)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "not_found", "Transaction not found")
|
||||
return
|
||||
}
|
||||
|
||||
txData, ok := txResp.Result.(map[string]interface{})
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid transaction response")
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"data": transformTransaction(txData),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleAddressBalance handles GET /api/v1/track1/address/:addr/balance
|
||||
func (s *Server) HandleAddressBalance(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/track1/address/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 || parts[1] != "balance" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "Invalid path")
|
||||
return
|
||||
}
|
||||
|
||||
address := parts[0]
|
||||
balanceResp, err := s.rpcGateway.GetBalance(r.Context(), address, "latest")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "rpc_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
balanceHex, ok := balanceResp.Result.(string)
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "invalid_response", "Invalid balance response")
|
||||
return
|
||||
}
|
||||
|
||||
balance, err := hexToBigInt(balanceHex)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "parse_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"address": address,
|
||||
"balance": balance.String(),
|
||||
"balance_wei": balance.String(),
|
||||
"balance_ether": weiToEther(balance),
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// HandleBridgeStatus handles GET /api/v1/track1/bridge/status
|
||||
func (s *Server) HandleBridgeStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
// Return bridge status (simplified - in production, query bridge contracts)
|
||||
response := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"status": "operational",
|
||||
"chains": map[string]interface{}{
|
||||
"138": map[string]interface{}{
|
||||
"name": "Defi Oracle Meta Mainnet",
|
||||
"status": "operational",
|
||||
"last_sync": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
"1": map[string]interface{}{
|
||||
"name": "Ethereum Mainnet",
|
||||
"status": "operational",
|
||||
"last_sync": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
"total_transfers_24h": 150,
|
||||
"total_volume_24h": "5000000000000000000000",
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func writeError(w http.ResponseWriter, statusCode int, code, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
"code": code,
|
||||
"message": message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func hexToInt(hex string) (int64, error) {
|
||||
hex = strings.TrimPrefix(hex, "0x")
|
||||
return strconv.ParseInt(hex, 16, 64)
|
||||
}
|
||||
|
||||
func parseBlockNumber(s string) int64 {
|
||||
num, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
func transformBlock(blockData map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"number": parseHexField(blockData["number"]),
|
||||
"hash": blockData["hash"],
|
||||
"parent_hash": blockData["parentHash"],
|
||||
"timestamp": parseHexTimestamp(blockData["timestamp"]),
|
||||
"transaction_count": countTransactions(blockData["transactions"]),
|
||||
"gas_used": parseHexField(blockData["gasUsed"]),
|
||||
"gas_limit": parseHexField(blockData["gasLimit"]),
|
||||
"miner": blockData["miner"],
|
||||
}
|
||||
}
|
||||
|
||||
func transformTransaction(txData map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"hash": txData["hash"],
|
||||
"from": txData["from"],
|
||||
"to": txData["to"],
|
||||
"value": txData["value"],
|
||||
"block_number": parseHexField(txData["blockNumber"]),
|
||||
"timestamp": parseHexTimestamp(txData["timestamp"]),
|
||||
}
|
||||
}
|
||||
|
||||
func parseHexField(field interface{}) interface{} {
|
||||
if str, ok := field.(string); ok {
|
||||
if num, err := hexToInt(str); err == nil {
|
||||
return num
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
func parseHexTimestamp(field interface{}) string {
|
||||
if str, ok := field.(string); ok {
|
||||
if num, err := hexToInt(str); err == nil {
|
||||
return time.Unix(num, 0).Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func countTransactions(txs interface{}) int {
|
||||
if txsList, ok := txs.([]interface{}); ok {
|
||||
return len(txsList)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func hexToBigInt(hex string) (*big.Int, error) {
|
||||
hex = strings.TrimPrefix(hex, "0x")
|
||||
bigInt := new(big.Int)
|
||||
bigInt, ok := bigInt.SetString(hex, 16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid hex number")
|
||||
}
|
||||
return bigInt, nil
|
||||
}
|
||||
|
||||
func weiToEther(wei *big.Int) string {
|
||||
ether := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18))
|
||||
return ether.Text('f', 18)
|
||||
}
|
||||
Reference in New Issue
Block a user