You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
572 lines
17 KiB
572 lines
17 KiB
// Package api provides HTTP API handlers
|
|
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"spiderman/internal/fabric"
|
|
"spiderman/internal/logger"
|
|
"spiderman/pkg/models"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Handler handles HTTP requests and communicates with Fabric
|
|
type Handler struct {
|
|
fabricClient *fabric.Client
|
|
}
|
|
|
|
// NewHandler creates a new API handler
|
|
func NewHandler(fabricClient *fabric.Client) *Handler {
|
|
logger.Logger.Info("API handler initialized")
|
|
return &Handler{
|
|
fabricClient: fabricClient,
|
|
}
|
|
}
|
|
|
|
// writeJSONResponse writes a JSON response to the HTTP response writer
|
|
func writeJSONResponse(w http.ResponseWriter, statusCode int, response models.Response) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// writeErrorResponse writes an error response to the HTTP response writer
|
|
func writeErrorResponse(w http.ResponseWriter, statusCode int, message string) {
|
|
writeJSONResponse(w, statusCode, models.Response{
|
|
Success: false,
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
// HealthCheck godoc
|
|
//
|
|
// @Summary Health check
|
|
// @Description Check the health status of the API
|
|
// @Tags health
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} models.HealthResponse
|
|
// @Router /health [get]
|
|
func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
requestLogger.Debug("Processing health check request")
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(models.HealthResponse{Status: "healthy"})
|
|
|
|
requestLogger.Info("Health check completed successfully")
|
|
}
|
|
|
|
// InitLedger godoc
|
|
//
|
|
// @Summary Initialize ledger
|
|
// @Description Initialize the blockchain ledger with a set of assets
|
|
// @Tags ledger
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} models.Response
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /ledger/init [post]
|
|
func (h *Handler) InitLedger(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
requestLogger.Info("Starting ledger initialization")
|
|
|
|
start := time.Now()
|
|
err := h.fabricClient.InitLedger()
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to initialize ledger")
|
|
|
|
writeErrorResponse(w, http.StatusInternalServerError,
|
|
fmt.Sprintf("Failed to initialize ledger: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Ledger initialized successfully")
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Message: "Ledger initialized successfully",
|
|
})
|
|
}
|
|
|
|
// GetAllAssets godoc
|
|
//
|
|
// @Summary Get all assets
|
|
// @Description Retrieve all assets from the blockchain ledger
|
|
// @Tags assets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} models.Response{data=[]models.Asset}
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /assets [get]
|
|
func (h *Handler) GetAllAssets(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
requestLogger.Info("Retrieving all assets")
|
|
|
|
start := time.Now()
|
|
assets, err := h.fabricClient.GetAllAssets()
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to retrieve assets")
|
|
|
|
writeErrorResponse(w, http.StatusInternalServerError,
|
|
fmt.Sprintf("Failed to get assets: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"asset_count": len(assets),
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Assets retrieved successfully")
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Data: assets,
|
|
})
|
|
}
|
|
|
|
// CreateAsset godoc
|
|
//
|
|
// @Summary Create a new asset
|
|
// @Description Create a new asset on the blockchain ledger with auto-generated UUID if ID not provided
|
|
// @Tags assets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param asset body models.CreateAssetRequest true "Asset data (ID is optional and will be auto-generated)"
|
|
// @Success 201 {object} models.Response{data=object{asset=models.Asset,transactionId=string}}
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /assets [post]
|
|
func (h *Handler) CreateAsset(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
requestLogger.Info("Creating new asset")
|
|
|
|
var req models.CreateAssetRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
requestLogger.WithError(err).Warn("Invalid request body for asset creation")
|
|
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
// Auto-generate UUID if ID is not provided
|
|
if req.ID == "" {
|
|
req.ID = uuid.New().String()
|
|
requestLogger.WithField("generated_id", req.ID).Info("Auto-generated UUID for asset")
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"asset_id": req.ID,
|
|
"owner": req.Owner,
|
|
"color": req.Color,
|
|
"size": req.Size,
|
|
"value": req.AppraisedValue,
|
|
}).Debug("Asset creation request details")
|
|
|
|
// Convert request to asset model
|
|
asset := models.Asset{
|
|
ID: req.ID,
|
|
Color: req.Color,
|
|
Size: req.Size,
|
|
Owner: req.Owner,
|
|
AppraisedValue: req.AppraisedValue,
|
|
}
|
|
|
|
start := time.Now()
|
|
transactionResult, err := h.fabricClient.CreateAsset(asset)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"asset_id": req.ID,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to create asset")
|
|
|
|
writeErrorResponse(w, http.StatusInternalServerError,
|
|
fmt.Sprintf("Failed to create asset: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"asset_id": req.ID,
|
|
"transaction_result": transactionResult,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Asset created successfully")
|
|
|
|
// Create response data that includes both asset and transaction ID
|
|
responseData := map[string]interface{}{
|
|
"asset": asset,
|
|
"transactionId": transactionResult,
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusCreated, models.Response{
|
|
Success: true,
|
|
Message: "Asset created successfully",
|
|
Data: responseData,
|
|
})
|
|
}
|
|
|
|
// GetAssetByID godoc
|
|
//
|
|
// @Summary Get asset by ID
|
|
// @Description Retrieve a specific asset by its ID from the blockchain ledger
|
|
// @Tags assets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Asset ID"
|
|
// @Success 200 {object} models.Response{data=models.Asset}
|
|
// @Failure 404 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /assets/{id} [get]
|
|
func (h *Handler) GetAssetByID(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
vars := mux.Vars(r)
|
|
assetID := vars["id"]
|
|
|
|
requestLogger.WithField("asset_id", assetID).Info("Retrieving asset by ID")
|
|
|
|
start := time.Now()
|
|
asset, err := h.fabricClient.ReadAssetByID(assetID)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"asset_id": assetID,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to retrieve asset")
|
|
|
|
statusCode := http.StatusInternalServerError
|
|
if err.Error() == "asset not found" {
|
|
statusCode = http.StatusNotFound
|
|
}
|
|
|
|
writeErrorResponse(w, statusCode,
|
|
fmt.Sprintf("Failed to get asset: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"asset_id": assetID,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Asset retrieved successfully")
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Data: asset,
|
|
})
|
|
}
|
|
|
|
// TransferAsset godoc
|
|
//
|
|
// @Summary Transfer asset ownership
|
|
// @Description Transfer ownership of an asset to a new owner
|
|
// @Tags assets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Asset ID"
|
|
// @Param request body models.TransferAssetRequest true "Transfer request"
|
|
// @Success 200 {object} models.Response
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 404 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /assets/{id}/transfer [put]
|
|
func (h *Handler) TransferAsset(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
vars := mux.Vars(r)
|
|
assetID := vars["id"]
|
|
|
|
requestLogger.WithField("asset_id", assetID).Info("Transferring asset")
|
|
|
|
var req models.TransferAssetRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
requestLogger.WithError(err).Warn("Invalid request body for asset transfer")
|
|
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"asset_id": assetID,
|
|
"new_owner": req.NewOwner,
|
|
}).Debug("Asset transfer request details")
|
|
|
|
start := time.Now()
|
|
err := h.fabricClient.TransferAsset(assetID, req.NewOwner)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"asset_id": assetID,
|
|
"new_owner": req.NewOwner,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to transfer asset")
|
|
|
|
statusCode := http.StatusInternalServerError
|
|
if err.Error() == "asset not found" {
|
|
statusCode = http.StatusNotFound
|
|
}
|
|
|
|
writeErrorResponse(w, statusCode,
|
|
fmt.Sprintf("Failed to transfer asset: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"asset_id": assetID,
|
|
"new_owner": req.NewOwner,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Asset transferred successfully")
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Message: "Asset transferred successfully",
|
|
})
|
|
}
|
|
|
|
// UpdateAsset godoc
|
|
//
|
|
// @Summary Update an existing asset
|
|
// @Description Update an existing asset's information on the blockchain
|
|
// @Tags assets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Asset ID"
|
|
// @Param asset body models.CreateAssetRequest true "Updated asset data"
|
|
// @Success 200 {object} models.Response{data=models.Asset}
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /assets/{id} [put]
|
|
func (h *Handler) UpdateAsset(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
assetID := vars["id"]
|
|
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr).
|
|
WithField("asset_id", assetID)
|
|
requestLogger.Info("Updating asset")
|
|
|
|
var req models.CreateAssetRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
requestLogger.WithError(err).Warn("Invalid request body for asset update")
|
|
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
// Set the ID from the URL
|
|
req.ID = assetID
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"owner": req.Owner,
|
|
"color": req.Color,
|
|
"size": req.Size,
|
|
"value": req.AppraisedValue,
|
|
}).Debug("Asset update request details")
|
|
|
|
// Convert request to asset model
|
|
asset := models.Asset{
|
|
ID: req.ID,
|
|
Color: req.Color,
|
|
Size: req.Size,
|
|
Owner: req.Owner,
|
|
AppraisedValue: req.AppraisedValue,
|
|
}
|
|
|
|
start := time.Now()
|
|
err := h.fabricClient.UpdateAsset(asset)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to update asset")
|
|
|
|
writeErrorResponse(w, http.StatusInternalServerError,
|
|
fmt.Sprintf("Failed to update asset: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Asset updated successfully")
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Message: "Asset updated successfully",
|
|
Data: asset,
|
|
})
|
|
}
|
|
|
|
// GetBlockHeight godoc
|
|
//
|
|
// @Summary Get block height
|
|
// @Description Get the current block height of the blockchain
|
|
// @Tags blockchain
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} models.Response{data=models.BlockHeightResponse}
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /blockchain/height [get]
|
|
func (h *Handler) GetBlockHeight(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
requestLogger.Info("Getting block height")
|
|
|
|
start := time.Now()
|
|
height, err := h.fabricClient.GetBlockHeight()
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to get block height")
|
|
|
|
writeErrorResponse(w, http.StatusInternalServerError,
|
|
fmt.Sprintf("Failed to get block height: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"height": height,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Block height retrieved successfully")
|
|
|
|
response := models.BlockHeightResponse{
|
|
Height: height,
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Message: "Block height retrieved successfully",
|
|
Data: response,
|
|
})
|
|
}
|
|
|
|
// GetChainInfo godoc
|
|
//
|
|
// @Summary Get chain information
|
|
// @Description Get detailed information about the blockchain including block height
|
|
// @Tags blockchain
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} models.Response{data=models.ChainInfoResponse}
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /blockchain/info [get]
|
|
func (h *Handler) GetChainInfo(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
requestLogger.Info("Getting chain information")
|
|
|
|
start := time.Now()
|
|
height, err := h.fabricClient.GetBlockHeight()
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to get chain information")
|
|
|
|
writeErrorResponse(w, http.StatusInternalServerError,
|
|
fmt.Sprintf("Failed to get chain information: %s", fabric.HandleFabricError(err)))
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"height": height,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Chain information retrieved successfully")
|
|
|
|
response := models.ChainInfoResponse{
|
|
Height: height,
|
|
ChainName: h.fabricClient.GetChannelName(),
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Message: "Chain information retrieved successfully",
|
|
Data: response,
|
|
})
|
|
}
|
|
|
|
// GetTransactionByID godoc
|
|
//
|
|
// @Summary Get transaction details by ID
|
|
// @Description Retrieve detailed information about a specific transaction by its ID
|
|
// @Tags transactions
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param txid path string true "Transaction ID"
|
|
// @Success 200 {object} models.Response{data=models.TransactionDetail}
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 404 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
// @Router /transactions/{txid} [get]
|
|
func (h *Handler) GetTransactionByID(w http.ResponseWriter, r *http.Request) {
|
|
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
|
|
vars := mux.Vars(r)
|
|
txID := vars["txid"]
|
|
|
|
if txID == "" {
|
|
requestLogger.Warn("Missing transaction ID in request")
|
|
writeErrorResponse(w, http.StatusBadRequest, "Transaction ID is required")
|
|
return
|
|
}
|
|
|
|
requestLogger.WithField("transaction_id", txID).Info("Getting transaction details")
|
|
|
|
start := time.Now()
|
|
txDetail, err := h.fabricClient.GetTransactionByID(txID)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"error": err.Error(),
|
|
"transaction_id": txID,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Error("Failed to get transaction details")
|
|
|
|
statusCode := http.StatusInternalServerError
|
|
message := fmt.Sprintf("Failed to get transaction details: %s", fabric.HandleFabricError(err))
|
|
|
|
// Check if it's a "not found" error
|
|
if strings.Contains(err.Error(), "not found") ||
|
|
strings.Contains(err.Error(), "does not exist") ||
|
|
strings.Contains(err.Error(), "no such transaction ID") {
|
|
statusCode = http.StatusNotFound
|
|
message = "Transaction not found"
|
|
}
|
|
|
|
writeErrorResponse(w, statusCode, message)
|
|
return
|
|
}
|
|
|
|
requestLogger.WithFields(logrus.Fields{
|
|
"transaction_id": txID,
|
|
"block_number": txDetail.BlockNumber,
|
|
"duration_ms": duration.Milliseconds(),
|
|
}).Info("Transaction details retrieved successfully")
|
|
|
|
writeJSONResponse(w, http.StatusOK, models.Response{
|
|
Success: true,
|
|
Message: "Transaction details retrieved successfully",
|
|
Data: txDetail,
|
|
})
|
|
}
|
|
|