package token import ( "encoding/hex" "encoding/json" "errors" "fmt" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/guregu/null" "github.com/metarare/metarare_api/common" "github.com/metarare/metarare_api/helpers" "github.com/metarare/metarare_api/helpers/gauth" "github.com/metarare/metarare_api/helpers/gerror" "github.com/metarare/metarare_api/models" "github.com/metarare/metarare_api/util" "github.com/metarare/metarare_api/view" "gorm.io/gorm" ) type TokenV1Router struct { group *gin.RouterGroup mDB *gorm.DB rDB *gorm.DB awsConf util.AWSConfs assetsAwsConf util.AWSConfs } type TokenBaseInfo struct { Commission float64 `json:"commission"` Collection []CollectionData `json:"collection"` } type CollectionData struct { CollectionID uint64 `json:"collection_id"` ThumbnailImage string `json:"thumbnail_image"` Name string `json:"name"` IsOfficial bool `json:"is_official"` } type RegisterTokenData struct { CollectionID uint64 `json:"collection_id" binding:"required"` Name string `json:"name" binding:"required"` Description string `json:"description" binding:"required"` Type string `json:"type" binding:"required"` TotalCount int `json:"total_count" binding:"required"` Royalties uint32 `json:"royalties" binding:"required"` Sale RegisterSaleData `json:"sale" binding:"required"` Traits []Trait `json:"traits"` } type Trait struct { Key string `json:"key"` Value string `json:"value"` } type RegisterSaleData struct { SaleType string `json:"sale_type" binding:"required"` Price float64 `json:"price" binding:"required"` StartPrice float64 `json:"start_price" binding:"required"` StartAt time.Time `json:"start_at" binding:"required"` EndAt time.Time `json:"end_at" binding:"required"` Currency string `json:"currency" binding:"required"` } type TargetID struct { ID uint64 `json:"id"` } type Like struct { TokenID uint64 `json:"token_id" binding:"required"` IsLike bool `json:"is_like"` } func NewTokenV1Router(r common.Router, basePath string) TokenV1Router { t := TokenV1Router{ group: r.Version.Group(basePath), mDB: r.Db.MasterDB, rDB: r.Db.ReadDB, awsConf: util.AWSConfs{ Region: r.Env.GetString("storage.region"), BucketName: r.Env.GetString("storage.bucket_name"), Access_key_id: r.Env.GetString("storage.access_key_id"), Access_key: r.Env.GetString("storage.access_key"), }, assetsAwsConf: util.AWSConfs{ Region: r.Env.GetString("storage.region"), BucketName: r.Env.GetString("storage.assets_buket_name"), Access_key_id: r.Env.GetString("storage.access_key_id"), Access_key: r.Env.GetString("storage.access_key"), }, } t.group.GET("baseinfo", t.getTokenBaseInfo) t.group.PATCH("like", t.updateLikeCount) t.group.POST("", t.registerToken) t.group.GET("owner", t.getOwnerList) t.group.GET("log/:token_id", t.getTokenLog) t.group.GET("resaleinfo/:token_id", t.getTokenResaleInfo) t.group.GET(":token_id", t.getTokenDetailInfo) return t } // getTokenBaseInfo godoc // @Summary get token base info // @Description 아이템 생성 시 내가 만든 컬렉션 리스트와, 사이트 커미션 수수료 정보 가져오기 // @Schemes // @security ApiKeyAuth // @Tags token // @Accept json // @Produce json // @Param type query string true "erc721, erc1155" // @Success 200 {object} TokenBaseInfo // @Router /token/baseinfo [get] func (t TokenV1Router) getTokenBaseInfo(c *gin.Context) { //NOTE x 다이나믹으로 해당 컬렉션의 타입을 받아 분기시켜서 보내준다. userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } condition := c.Query("type") if condition != "erc721" && condition != "erc1155" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } wallet := models.UserWallet{} if err := t.rDB.Where("user_id = ?", userID).Find(&wallet).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } response := TokenBaseInfo{} if err := t.rDB.Select("commission").Table("setting").Find(&response.Commission).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } if err := t.rDB.Select("collection.id AS collection_id, collection.is_official, collection_profile.thumbnail_image, collection_profile.name").Table("collection"). Joins("INNER JOIN collection_profile ON collection_profile.collection_id = collection.id"). Where("(collection.is_official = 1 AND collection.type = ?) OR (collection.type = ? AND collection.owner_address = ?)", condition, condition, wallet.Address).Find(&response.Collection).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) return } // registerToken godoc // @Summary create NFT // @Description NFT 생성과 동시에 판매 등록 // @Schemes // @security ApiKeyAuth // @Tags token // @Accept multipart/form-data // @Produce json // @Param contentImage formData file true "content image" // @Param json formData common.SwagStruct true "object" // @Success 200 {string} OK // @Router /token [post] func (t TokenV1Router) registerToken(c *gin.Context) { userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } form, err := c.MultipartForm() if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MultipartError, nil, err) return } _request := form.Value["json"] if _request == nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } request := RegisterTokenData{} if err := json.Unmarshal([]byte(_request[0]), &request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, nil) return } //validate check if request.CollectionID == 0 || request.Name == "" || request.Type == "" || request.TotalCount > 20 || request.Royalties > 50 || request.Sale.SaleType == "" || request.Sale.Currency == "" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } contentImage := form.File["contentImage"] if contentImage == nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } wallet := models.UserWallet{} if err := t.rDB.Where("user_id = ?", userID).Find(&wallet).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidConnection, nil, err) return } collection := models.Collection{} _err := t.rDB.Where("id = ?", request.CollectionID).Find(&collection).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } setting := models.Setting{} _err = t.rDB.Find(&setting).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } tx := t.mDB.Begin() defer common.DBTransaction(tx) var contentImageURL string var assetsImageURL string ContentImagePath := fmt.Sprintf("token/content") for _, f := range contentImage { contentImageURL, err = util.UploadToS3(t.awsConf, ContentImagePath, f) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.Error3rdParty, nil, err) tx.Rollback() return } // NOTE x assets image data imageURL, err := util.UploadToS3(t.assetsAwsConf, "", f) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.Error3rdParty, nil, err) tx.Rollback() return } str := strings.Split(imageURL, "/") assetsImageURL = "https://assets.meta-rare.net/" + str[len(str)-1] } response := models.Sale{} tokenUID := common.GenerateTokenUID(tx, "token_") for idx := 1; idx <= int(request.TotalCount); idx++ { token := models.Token{ CollectionID: request.CollectionID, Name: request.Name, Description: request.Description, Royalties: request.Royalties, CreatorAddress: wallet.Address, OwnerAddress: wallet.Address, ContentURL: contentImageURL, AssetsImageURL: assetsImageURL, Type: request.Type, UID: tokenUID, } if request.Type == "erc1155" { token.TotalCount = null.IntFrom(int64(request.TotalCount)) token.Index = null.IntFrom(int64(idx)) } if err := tx.Save(&token).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } sale := models.Sale{ TokenID: token.ID, SaleType: request.Sale.SaleType, UID: common.GenerateSaleUID(tx, "sale_"), Currency: request.Sale.Currency, Status: "ongoing", TreasuryTax: setting.Commission, } if request.Sale.SaleType == "fixed" { sale.Price = null.FloatFrom(request.Sale.Price) } else if request.Sale.SaleType == "time" { sale.StartPrice = null.FloatFrom(request.Sale.StartPrice) sale.StartAt = null.TimeFrom(request.Sale.StartAt) sale.EndAt = null.TimeFrom(request.Sale.EndAt) } if request.Sale.SaleType == "fixed" { _v := common.MakeHashData{ OwnerAddress: wallet.Address, CollectionAddress: collection.ContractAddress, TokenType: request.Type, TokenID: int64(token.ID), Currency: request.Sale.Currency, Price: request.Sale.Price, Index: int64(idx), TotalIndex: int64(request.TotalCount), CreationTax: int64(request.Royalties * 100), PrevTreasuryTax: int64(setting.Commission * 100), TreasuryAddress: setting.TreasuryAddress, } _vv, err := common.MakeSining(tx, _v) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } _signature, _hashData, err := helpers.Signing(_vv, wallet.PrivateKey) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } sale.HashData = hex.EncodeToString(_hashData) sale.Signature = hex.EncodeToString(_signature) } if err := tx.Save(&sale).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } for _, item := range request.Traits { tr := models.Traits{ TokenID: token.ID, Key: item.Key, Value: item.Value, } if err := tx.Save(&tr).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } } if idx == 1 { response = sale } if request.Type == "erc721" { break } } if err := tx.Commit().Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) return } // updateLikeCount godoc // @Summary update token like count // @Description 토큰 좋아요 기능 // @Schemes // @security ApiKeyAuth // @Tags token // @Accept json // @Produce json // @Param Like body Like true "selected target id" // @Success 200 {string} OK // @Router /token/like [patch] func (t TokenV1Router) updateLikeCount(c *gin.Context) { userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } request := Like{} if err := c.ShouldBindJSON(&request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } token := models.Token{} _err := t.rDB.Where("id = ?", request.TokenID).Find(&token).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.NotFoundRecord, nil, err) return } userLike := models.UserLike{} if err := t.rDB.Where("user_id = ? AND token_id = ?", userID, token.ID).Find(&userLike).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } if (userLike.IsLike == 1 && request.IsLike) || (userLike.IsLike == 0 && !request.IsLike) { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } tx := t.mDB.Begin() defer common.DBTransaction(tx) if userLike.ID == 0 { userLike.UserID = userID userLike.TokenID = token.ID userLike.IsLike = 1 if err := tx.Save(&userLike).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } } else { if err := tx.Model(&userLike).Update("is_like", request.IsLike).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } } if request.IsLike { token.LikeCount += 1 } else { token.LikeCount -= 1 } if err := tx.Save(&token).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } if err := tx.Commit().Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, nil, nil) return } // getTokenLog godoc // @Summary get token log // @Description NFT 판매 페이지 기록 탭 // @Schemes // @Tags token // @Accept json // @Produce json // @Success 200 {object} common.ActivityItem // @Param token_id path string true "token id" // @Router /token/log/{token_id} [get] func (t TokenV1Router) getTokenLog(c *gin.Context) { tokenID := c.Param("token_id") if tokenID == "" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } condition := [...]string{"purchase", "create", "sell"} response := []common.ActivityItem{} err := view.GetActivityItemQuery(t.rDB).Where("log_relation.token_id = ? AND log.type in (?)", tokenID, condition).Find(&response).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) return } // getOwnerList godoc // @Summary get owner list // @Description NFT 1155타입의 소유자 탭 // @Schemes // @Tags token // @Accept json // @Produce json // @Success 200 {object} view.TokenOwnerList // @Param token_uid query string true "type" // @Router /token/owner [get] func (t TokenV1Router) getOwnerList(c *gin.Context) { tokenUID := c.Query("token_uid") if tokenUID == "" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } response := []view.TokenOwnerList{} err := view.GetOwnerList(t.rDB).Where("token.uid = ?", tokenUID).Order("token.id desc").Find(&response).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) return } // getTokenResaleInfo godoc // @Summary get resale data // @Description 토큰 재판매 시 설정해야하는 기본 데이터 // @Schemes // @security ApiKeyAuth // @Tags token // @Accept json // @Produce json // @Success 200 {object} view.TokenResaleData // @Param token_id path string true "token id" // @Router /token/resaleinfo/{token_id} [get] func (t TokenV1Router) getTokenResaleInfo(c *gin.Context) { userID, _err := gauth.GetCurrentUserIDToInt64(c) if _err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, _err) return } tokenID := c.Param("token_id") if tokenID == "" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } //NOTE x 토큰의 오너인지, 세일이 없는 아이인지 확인하기 _t := models.Token{} err := t.rDB.Where("id = ?", tokenID).Find(&_t).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } _uw := models.UserWallet{} err = t.rDB.Where("user_id = ?", userID).Find(&_uw).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } if _t.OwnerAddress != _uw.Address { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } _s := []models.Sale{} t.rDB.Where("token_id = ? AND status = 'ongoing'", tokenID).Find(&_s) if len(_s) > 0 { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } response := view.TokenResaleData{} err = view.GetResaleInfo(t.rDB).Where("token.id = ?", tokenID).Find(&response.Token).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } _setting := models.Setting{} err = t.rDB.Find(&_setting).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } response.Token.Commission = _setting.Commission t.rDB.Where("token_id = ?", tokenID).Find(&response.Traits) gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) return } // getTokenDetailInfo godoc // @Summary get token detail // @Description 토큰 상세페이지 // @Schemes // @Tags token // @Accept json // @Produce json // @Success 200 {object} view.TokenDetailData // @Param token_id path string true "token id" // @Router /token/{token_id} [get] func (t TokenV1Router) getTokenDetailInfo(c *gin.Context) { userID, _ := gauth.GetCurrentUserIDToInt64(c) tokenID := c.Param("token_id") if tokenID == "" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } _s := []models.Sale{} err := t.rDB.Where("token_id = ? AND status = 'ongoing'", tokenID).Find(&_s).Error if len(_s) > 0 { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } response := view.TokenDetailData{} err = view.GetTokenDetail(t.rDB, userID).Where("token.id = ?", tokenID).Find(&response.Token).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.NotFoundRecord, nil, err) return } err = t.rDB.Where("token_id = ?", tokenID).Find(&response.Traits).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.NotFoundRecord, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) }