Files
a301_server/internal/chain/handler.go
tolelom f8b23e93bf
All checks were successful
Server CI/CD / deploy (push) Successful in 7s
feat: 블록체인(chain) 통합 및 내부 API 추가
- internal/chain 패키지 추가 (client, handler, service, repository, model)
- 체인 연동 엔드포인트: 지갑 조회, 잔액, 자산, 인벤토리, 마켓 등
- 관리자 전용 체인 엔드포인트: 민팅, 보상, 템플릿 등록
- 게임 서버용 내부 API (/api/internal/chain/*) + ServerAuth 미들웨어
- 회원가입 시 블록체인 월렛 자동 생성
- 체인 관련 환경변수 및 InternalAPIKey 설정 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:18:15 +09:00

376 lines
13 KiB
Go

package chain
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/tolelom/tolchain/core"
)
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
// ---- Query Handlers ----
func (h *Handler) GetWalletInfo(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
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 := c.Locals("userID").(uint)
result, err := h.svc.GetBalance(userID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(result)
}
func (h *Handler) GetAssets(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
offset, _ := strconv.Atoi(c.Query("offset", "0"))
limit, _ := strconv.Atoi(c.Query("limit", "50"))
result, err := h.svc.GetAssets(userID, offset, limit)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
c.Set("Content-Type", "application/json")
return c.Send(result)
}
func (h *Handler) GetAsset(c *fiber.Ctx) error {
assetID := c.Params("id")
if assetID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "asset id is required"})
}
result, err := h.svc.GetAsset(assetID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
c.Set("Content-Type", "application/json")
return c.Send(result)
}
func (h *Handler) GetInventory(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
result, err := h.svc.GetInventory(userID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
c.Set("Content-Type", "application/json")
return c.Send(result)
}
func (h *Handler) GetMarketListings(c *fiber.Ctx) error {
offset, _ := strconv.Atoi(c.Query("offset", "0"))
limit, _ := strconv.Atoi(c.Query("limit", "50"))
result, err := h.svc.GetMarketListings(offset, limit)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
c.Set("Content-Type", "application/json")
return c.Send(result)
}
func (h *Handler) GetMarketListing(c *fiber.Ctx) error {
listingID := c.Params("id")
if listingID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listing id is required"})
}
result, err := h.svc.GetListing(listingID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
c.Set("Content-Type", "application/json")
return c.Send(result)
}
// ---- User Transaction Handlers ----
func (h *Handler) Transfer(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
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 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(result)
}
func (h *Handler) TransferAsset(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
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 req.AssetID == "" || 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(result)
}
func (h *Handler) ListOnMarket(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
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 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(result)
}
func (h *Handler) BuyFromMarket(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
var req struct {
ListingID string `json:"listingId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.ListingID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"})
}
result, err := h.svc.BuyFromMarket(userID, req.ListingID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(result)
}
func (h *Handler) CancelListing(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
var req struct {
ListingID string `json:"listingId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.ListingID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"})
}
result, err := h.svc.CancelListing(userID, req.ListingID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(result)
}
func (h *Handler) EquipItem(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
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 req.AssetID == "" || 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(result)
}
func (h *Handler) UnequipItem(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
var req struct {
AssetID string `json:"assetId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.AssetID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId는 필수입니다"})
}
result, err := h.svc.UnequipItem(userID, req.AssetID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 req.TemplateID == "" || 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 req.ID == "" || 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 req.TemplateID == "" || 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 username == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"})
}
result, err := h.svc.GetBalanceByUsername(username)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 username == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"})
}
offset, _ := strconv.Atoi(c.Query("offset", "0"))
limit, _ := strconv.Atoi(c.Query("limit", "50"))
result, err := h.svc.GetAssetsByUsername(username, offset, limit)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
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 username == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"})
}
result, err := h.svc.GetInventoryByUsername(username)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
c.Set("Content-Type", "application/json")
return c.Send(result)
}