package chain import ( "log" "strconv" "github.com/gofiber/fiber/v2" "github.com/tolelom/tolchain/core" ) const maxLimit = 200 const maxIDLength = 256 // max length for string IDs (assetId, listingId, etc.) type Handler struct { svc *Service } func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } func getUserID(c *fiber.Ctx) (uint, error) { uid, ok := c.Locals("userID").(uint) if !ok { return 0, fiber.NewError(fiber.StatusUnauthorized, "인증이 필요합니다") } return uid, nil } func parsePagination(c *fiber.Ctx) (int, int) { offset, _ := strconv.Atoi(c.Query("offset", "0")) limit, _ := strconv.Atoi(c.Query("limit", "50")) if offset < 0 { offset = 0 } if limit <= 0 { limit = 50 } else if limit > maxLimit { limit = maxLimit } return offset, limit } func validID(s string) bool { return s != "" && len(s) <= maxIDLength } func chainError(c *fiber.Ctx, status int, userMsg string, err error) error { log.Printf("chain error: %s: %v", userMsg, err) return c.Status(status).JSON(fiber.Map{"error": userMsg}) } // ---- Query Handlers ---- func (h *Handler) GetWalletInfo(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } w, err := h.svc.GetWallet(userID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "지갑을 찾을 수 없습니다"}) } return c.JSON(fiber.Map{ "address": w.Address, "pubKeyHex": w.PubKeyHex, }) } func (h *Handler) GetBalance(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } result, err := h.svc.GetBalance(userID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "잔액 조회에 실패했습니다", err) } return c.JSON(result) } func (h *Handler) GetAssets(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } offset, limit := parsePagination(c) result, err := h.svc.GetAssets(userID, offset, limit) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } func (h *Handler) GetAsset(c *fiber.Ctx) error { assetID := c.Params("id") if !validID(assetID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효한 asset id가 필요합니다"}) } result, err := h.svc.GetAsset(assetID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } func (h *Handler) GetInventory(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } result, err := h.svc.GetInventory(userID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "인벤토리 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } func (h *Handler) GetMarketListings(c *fiber.Ctx) error { offset, limit := parsePagination(c) result, err := h.svc.GetMarketListings(offset, limit) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } func (h *Handler) GetMarketListing(c *fiber.Ctx) error { listingID := c.Params("id") if !validID(listingID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효한 listing id가 필요합니다"}) } result, err := h.svc.GetListing(listingID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // ---- User Transaction Handlers ---- func (h *Handler) Transfer(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { To string `json:"to"` Amount uint64 `json:"amount"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.To) || req.Amount == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "to와 amount는 필수입니다"}) } result, err := h.svc.Transfer(userID, req.To, req.Amount) if err != nil { return chainError(c, fiber.StatusInternalServerError, "전송에 실패했습니다", err) } return c.JSON(result) } func (h *Handler) TransferAsset(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` To string `json:"to"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) || !validID(req.To) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 to는 필수입니다"}) } result, err := h.svc.TransferAsset(userID, req.AssetID, req.To) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 전송에 실패했습니다", err) } return c.JSON(result) } func (h *Handler) ListOnMarket(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` Price uint64 `json:"price"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) || req.Price == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 price는 필수입니다"}) } result, err := h.svc.ListOnMarket(userID, req.AssetID, req.Price) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 등록에 실패했습니다", err) } return c.JSON(result) } func (h *Handler) BuyFromMarket(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { ListingID string `json:"listingId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.ListingID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"}) } result, err := h.svc.BuyFromMarket(userID, req.ListingID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 구매에 실패했습니다", err) } return c.JSON(result) } func (h *Handler) CancelListing(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { ListingID string `json:"listingId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.ListingID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"}) } result, err := h.svc.CancelListing(userID, req.ListingID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 취소에 실패했습니다", err) } return c.JSON(result) } func (h *Handler) EquipItem(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` Slot string `json:"slot"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) || !validID(req.Slot) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 slot은 필수입니다"}) } result, err := h.svc.EquipItem(userID, req.AssetID, req.Slot) if err != nil { return chainError(c, fiber.StatusInternalServerError, "장착에 실패했습니다", err) } return c.JSON(result) } func (h *Handler) UnequipItem(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId는 필수입니다"}) } result, err := h.svc.UnequipItem(userID, req.AssetID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "장착 해제에 실패했습니다", err) } return c.JSON(result) } // ---- Operator (Admin) Transaction Handlers ---- func (h *Handler) MintAsset(c *fiber.Ctx) error { var req struct { TemplateID string `json:"templateId"` OwnerPubKey string `json:"ownerPubKey"` Properties map[string]any `json:"properties"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.TemplateID) || !validID(req.OwnerPubKey) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "templateId와 ownerPubKey는 필수입니다"}) } result, err := h.svc.MintAsset(req.TemplateID, req.OwnerPubKey, req.Properties) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 발행에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } func (h *Handler) GrantReward(c *fiber.Ctx) error { var req struct { RecipientPubKey string `json:"recipientPubKey"` TokenAmount uint64 `json:"tokenAmount"` Assets []core.MintAssetPayload `json:"assets"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.RecipientPubKey) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "recipientPubKey는 필수입니다"}) } result, err := h.svc.GrantReward(req.RecipientPubKey, req.TokenAmount, req.Assets) if err != nil { return chainError(c, fiber.StatusInternalServerError, "보상 지급에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } func (h *Handler) RegisterTemplate(c *fiber.Ctx) error { var req struct { ID string `json:"id"` Name string `json:"name"` Schema map[string]any `json:"schema"` Tradeable bool `json:"tradeable"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.ID) || !validID(req.Name) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id와 name은 필수입니다"}) } result, err := h.svc.RegisterTemplate(req.ID, req.Name, req.Schema, req.Tradeable) if err != nil { return chainError(c, fiber.StatusInternalServerError, "템플릿 등록에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // ---- Internal Handlers (game server, username-based) ---- // InternalGrantReward grants reward by username. For game server use. func (h *Handler) InternalGrantReward(c *fiber.Ctx) error { var req struct { Username string `json:"username"` TokenAmount uint64 `json:"tokenAmount"` Assets []core.MintAssetPayload `json:"assets"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.Username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } result, err := h.svc.GrantRewardByUsername(req.Username, req.TokenAmount, req.Assets) if err != nil { return chainError(c, fiber.StatusInternalServerError, "보상 지급에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // InternalMintAsset mints an asset by username. For game server use. func (h *Handler) InternalMintAsset(c *fiber.Ctx) error { var req struct { TemplateID string `json:"templateId"` Username string `json:"username"` Properties map[string]any `json:"properties"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.TemplateID) || !validID(req.Username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "templateId와 username은 필수입니다"}) } result, err := h.svc.MintAssetByUsername(req.TemplateID, req.Username, req.Properties) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 발행에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // InternalGetBalance returns balance by username. For game server use. func (h *Handler) InternalGetBalance(c *fiber.Ctx) error { username := c.Query("username") if !validID(username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } result, err := h.svc.GetBalanceByUsername(username) if err != nil { return chainError(c, fiber.StatusInternalServerError, "잔액 조회에 실패했습니다", err) } return c.JSON(result) } // InternalGetAssets returns assets by username. For game server use. func (h *Handler) InternalGetAssets(c *fiber.Ctx) error { username := c.Query("username") if !validID(username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } offset, limit := parsePagination(c) result, err := h.svc.GetAssetsByUsername(username, offset, limit) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // InternalGetInventory returns inventory by username. For game server use. func (h *Handler) InternalGetInventory(c *fiber.Ctx) error { username := c.Query("username") if !validID(username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } result, err := h.svc.GetInventoryByUsername(username) if err != nil { return chainError(c, fiber.StatusInternalServerError, "인벤토리 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) }