package sale import ( "encoding/hex" "errors" "fmt" "math/big" "net/http" "strconv" "time" "github.com/ethereum/go-ethereum/core/types" "github.com/gin-gonic/gin" "github.com/guregu/null" "github.com/metarare/metarare_api/api/constant" "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 SaleV1Router struct { group *gin.RouterGroup mDB *gorm.DB rDB *gorm.DB awsConf util.AWSConfs metaAwsConf util.AWSConfs } type TargetID struct { ID uint64 `json:"id"` } type BidData struct { SaleID uint64 `json:"sale_id"` BidPrice float64 `json:"bid_price"` Currency string `json:"currency"` } type RegisterResaleData struct { TokenID uint64 `json:"token_id" binding:"required"` SaleType string `json:"sale_type" binding:"required"` Price float64 `json:"price"` StartPrice float64 `json:"start_price"` StartAt time.Time `json:"start_at"` EndAt time.Time `json:"end_at"` Currency string `json:"currency" binding:"required"` } func NewSaleV1Router(r common.Router, basePath string) SaleV1Router { s := SaleV1Router{ 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.assets_buket_name"), Access_key_id: r.Env.GetString("storage.access_key_id"), Access_key: r.Env.GetString("storage.access_key"), }, metaAwsConf: util.AWSConfs{ Region: r.Env.GetString("storage.region"), BucketName: r.Env.GetString("storage.nftinfo_buket_name"), Access_key_id: r.Env.GetString("storage.access_key_id"), Access_key: r.Env.GetString("storage.access_key"), }, } s.group.GET(":sale_id", s.getDetailSaleInfo) s.group.PATCH("status", s.updateStatus) s.group.POST("bid", s.bidAuction) s.group.PATCH("bid", s.cancelBid) s.group.GET("bid/:sale_id", s.getBidHistory) s.group.POST("buy", s.buyNow) s.group.POST("auction", s.sellHighestPrice) s.group.POST("resale", s.registerResale) s.group.GET("ranking", s.rankningSale) return s } // getDetailSaleInfo godoc // @Summary get detail sale data // @Description NFT 판매 상세페이지 데이터 가져오기 // @Schemes // @Tags sale // @name get-string-by-int // @Accept json // @Produce json // @Success 200 {obejct} view.DetailSaleData // @Param sale_id path string true "sale id" // @Router /sale/{sale_id} [get] func (s SaleV1Router) getDetailSaleInfo(c *gin.Context) { userID, _ := gauth.GetCurrentUserIDToInt64(c) saleID := c.Param("sale_id") intScaleID, _ := strconv.Atoi(saleID) if helpers.Contains(constant.HideFromListIds, intScaleID) { return } if saleID == "" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } wallet := models.UserWallet{} if userID != 0 { if err := s.rDB.Where("user_id = ?", userID).Find(&wallet).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } } response := view.DetailSaleData{} if err := view.GetSaleData(s.rDB, wallet.Address, wallet.UserID, saleID).Where("sale.id = ?", saleID).Find(&response.Sale).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } // t := []models.Traits{} if err := s.rDB.Select("traits.*").Table("sale"). Joins("INNER JOIN traits ON traits.token_id = sale.token_id"). Where("sale.id = ?", saleID).Find(&response.Traits).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) return } // updateStatus godoc // @Summary update sale statue for cancel // @Description 등록한 오너인 경우에만 실행가능, 판매하지 않고 판매를 강제 종료시킨다. // @Schemes // @security ApiKeyAuth // @Tags sale // @Accept json // @Produce json // @Param TargetID body TargetID true "selected target id" // @Success 200 {string} OK // @Router /sale/status [patch] func (s SaleV1Router) updateStatus(c *gin.Context) { userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } request := TargetID{} if err := c.ShouldBindJSON(&request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } isOwner := view.Owner{} if err := view.IsOwner(s.rDB, userID).Where("sale.id = ?", request.ID).Find(&isOwner).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } else if !isOwner.IsOwner { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, errors.New("permission not found ")) return } //NOTE x fixed, time인경우 취소가능, time인 경우 경매기록이 있으면 취소가 안된다. sale := models.Sale{} _err := s.rDB.Where("id = ?", request.ID).Find(&sale).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } //NOTE x auction인경우 판매취소 불가, time인 경우 비딩 되어있는 값이 있으면 불가 if sale.SaleType == "auction" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("can't cancel the sale")) return } else if sale.SaleType == "time" { bidLog := []models.BidLog{} _err := s.rDB.Where("sale_id = ?", sale.ID).Find(&bidLog).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } if len(bidLog) > 0 { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("can't cancel the sale")) return } } if err := s.mDB.Model(&models.Sale{}).Where("id = ?", request.ID).Update("status", "cancel").Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, nil, nil) return } // bidAuction godoc // @Summary create bid // @Description 입찰하기 // @Schemes // @security ApiKeyAuth // @Tags sale // @Accept json // @Produce json // @Param BidData body BidData true "body struct" // @Success 200 {string} OK // @Router /sale/bid [post] func (s SaleV1Router) bidAuction(c *gin.Context) { userID, _err := gauth.GetCurrentUserIDToInt64(c) if _err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, _err) return } request := BidData{} if err := c.ShouldBindJSON(&request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } sale := models.Sale{} err := s.rDB.Where("id = ?", request.SaleID).Find(&sale).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } if sale.SaleType == "time" && sale.StartPrice.Float64 >= request.BidPrice { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("please check the minimum bid amount")) return } wallet := models.UserWallet{} err = s.rDB.Where("user_id = ?", userID).Find(&wallet).Error if errors.Is(err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } bidLog := models.BidLog{} if err := s.rDB.Where("sale_id = ? AND address = ? AND is_cancel = 0", request.SaleID, wallet.Address).Find(&bidLog).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } else if bidLog.ID != 0 { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, errors.New("already participating in the bidding")) return } setting := models.Setting{} if err := s.rDB.Find(&setting).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } else if setting.ID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, errors.New("not found recored")) return } //NOTE x 사용 가능 금액 검증 if request.Currency == "eth" { _, availableBalance := helpers.GetSelectedBalance(s.rDB, userID, request.Currency, wallet.Address) if (request.BidPrice + setting.GasDeposit) > availableBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } } else { _, availableBalance := helpers.GetSelectedBalance(s.rDB, userID, request.Currency, wallet.Address) if request.BidPrice > availableBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } _, availableEthBalance := helpers.GetSelectedBalance(s.rDB, userID, "eth", wallet.Address) if setting.GasDeposit > availableEthBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } } tx := s.mDB.Begin() defer common.DBTransaction(tx) _bidLog := models.BidLog{ SaleID: request.SaleID, IsCancel: 0, Price: request.BidPrice, Address: wallet.Address, } if err := tx.Save(&_bidLog).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } deposit := models.Deposit{ UserID: userID, SaleID: request.SaleID, Type: request.Currency, EthPrice: null.FloatFrom(setting.GasDeposit), Status: "deposit", } if request.Currency == "eth" { deposit.EthPrice = null.FloatFrom(deposit.EthPrice.Float64 + request.BidPrice) } else if request.Currency == "mr" { deposit.MrPrice = null.FloatFrom(request.BidPrice) } else if request.Currency == "mf1" { deposit.Mf1Price = null.FloatFrom(request.BidPrice) } if err := tx.Save(&deposit).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 } // cancelBid godoc // @Summary cancel bid // @Description 입찰 취소하기, 입찰을 다시하려면 취소 후 다시 해야한다. // @Schemes // @security ApiKeyAuth // @Tags sale // @Accept json // @Produce json // @Param TargetID body TargetID true "body struct" // @Success 200 {string} OK // @Router /sale/bid [patch] func (s SaleV1Router) cancelBid(c *gin.Context) { userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } request := TargetID{} if err := c.ShouldBindJSON(&request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } wallet := models.UserWallet{} if err := s.rDB.Where("user_id = ?", userID).Find(&wallet).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } else if wallet.ID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, errors.New("not found recored")) return } bidLog := models.BidLog{} if err := s.rDB.Where("sale_id = ? AND address = ? AND is_cancel = 0", request.ID, wallet.Address).Find(&bidLog).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } else if bidLog.ID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, errors.New("not found recored")) return } deposit := models.Deposit{} if err := s.rDB.Where("user_id = ? AND sale_id = ? AND status = 'deposit'", userID, request.ID).Find(&deposit).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } else if deposit.ID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, errors.New("not found recored")) return } tx := s.mDB.Begin() defer common.DBTransaction(tx) if err := tx.Model(&bidLog).Update("is_cancel", 1).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } if err := tx.Model(&deposit).Update("status", "withdrawal").Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, 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 } // getBidHistory godoc // @Summary get bid history // @Description NFT 판매 상세페이지 경매 기록 탭 // @Schemes // @security ApiKeyAuth // @Tags sale // @name get-string-by-int // @Accept json // @Produce json // @Success 200 {obejct} []view.BidList // @Param sale_id path string true "sale id" // @Router /sale/bid/{sale_id} [get] func (s SaleV1Router) getBidHistory(c *gin.Context) { saleID := c.Param("sale_id") if saleID == "" { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("invalid parameter value")) return } response := []view.BidList{} if err := view.GetBidList(s.rDB).Where("sale.id = ?", saleID).Order("bid_log.id desc").Find(&response).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, response, nil) return } // buyNow godoc // @Summary buy now item // @Description 아이템 즉시 구매 // @Schemes // @security ApiKeyAuth // @Tags sale // @Accept json // @Produce json // @Param BidData body BidData true "body struct" // @Success 200 {string} OK // @Router /sale/buy [post] func (s SaleV1Router) buyNow(c *gin.Context) { userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } request := BidData{} if err := c.ShouldBindJSON(&request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } sale := models.Sale{} if err := s.rDB.Where("id = ?", request.SaleID).Find(&sale).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } else if sale.ID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } //NOTE x 요청 한 값과 실제 NFT 가격이 맞는지 비교 (필요한가 ..?) if request.BidPrice != sale.Price.Float64 { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } wallet := models.UserWallet{} if err := s.rDB.Where("user_id = ?", userID).Find(&wallet).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } else if wallet.ID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } token := models.Token{} if err := s.rDB.Where("id = ?", sale.TokenID).Find(&token).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } else if token.ID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } var firstOrder bool if sale.IsResale == 1 && (token.LastestCurrency.String != "") { firstOrder = false } else if sale.IsResale == 0 || (sale.IsResale == 1 && token.LastestCurrency.String == "") { firstOrder = true } var _official bool ownerWallet := models.UserWallet{} _err := s.rDB.Where("address = ?", token.OwnerAddress).Find(&ownerWallet).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } collection := models.Collection{} _err = s.rDB.Where("id = ?", token.CollectionID).Find(&collection).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } setting := models.Setting{} _err = s.rDB.Find(&setting).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusNotFound, gerror.NotFoundRecord, nil, err) return } if collection.IsOfficial == 1 { _official = true } else { _official = false } fmt.Println("---------------------------------------------------------") fmt.Printf("first_order: %t\n", firstOrder) fmt.Printf("official: %t\n", _official) //NOTE x 사용 가능 금액 검증 if request.Currency == "eth" { _, availableBalance := helpers.GetSelectedBalance(s.rDB, userID, request.Currency, wallet.Address) if (sale.Price.Float64 + setting.GasDeposit) > availableBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } } else { _, availableBalance := helpers.GetSelectedBalance(s.rDB, userID, request.Currency, wallet.Address) if sale.Price.Float64 > availableBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } _, availableEthBalance := helpers.GetSelectedBalance(s.rDB, userID, "eth", wallet.Address) if setting.GasDeposit > availableEthBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } var allowanceAmount *big.Int var err error var _currency helpers.ContractType if request.Currency == "mf" { _currency = helpers.MetaFinance allowanceAmount, err = helpers.GetAllowance(helpers.MetaFinance, wallet.Address) } else if request.Currency == "mr" { _currency = helpers.MetaRare allowanceAmount, err = helpers.GetAllowance(helpers.MetaRare, wallet.Address) } if request.BidPrice > helpers.TypeCastingToFloat64(allowanceAmount) { fmt.Println("excution approve") _, _, _, err = helpers.ApproveForERC20(_currency, wallet.PrivateKey, common.MaxBig256) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } } } tx := s.mDB.Begin() defer common.DBTransaction(tx) if err := tx.Model(&sale).Update("status", "end").Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } if err := tx.Model(&token).Updates(models.Token{OwnerAddress: wallet.Address, LastestPrice: null.FloatFrom(request.BidPrice), LastestCurrency: null.StringFrom(request.Currency)}).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } _v := common.MakeHashData{ OwnerAddress: ownerWallet.Address, CollectionAddress: collection.ContractAddress, TokenType: token.Type, TokenID: int64(token.ID), Currency: sale.Currency, Price: sale.Price.Float64, Index: int64(token.Index.Int64), TotalIndex: int64(token.TotalCount.Int64), CreationTax: int64(token.Royalties * 100), PrevTreasuryTax: int64(sale.TreasuryTax * 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 } _sigData, _hashData, err := helpers.Signing(_vv, ownerWallet.PrivateKey) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } fmt.Printf("sig_data: %s \n", hex.EncodeToString(_sigData)) fmt.Printf("hash_data: %s \n", hex.EncodeToString(_hashData)) var txHash *types.Transaction _hash, _ := hex.DecodeString(sale.HashData) _sig, _ := hex.DecodeString(sale.Signature) var _hashedData [32]byte var _signature [65]byte copy(_hashedData[:], _hash) copy(_signature[:], _sig) //NOTE x resale인 경우와 첫 판매인 경우 분기 if sale.IsResale == 1 && !firstOrder { if token.Type == "erc721" { _txHash, _, err := helpers.Order721OnlyTransfer(_vv, _hashedData, wallet.PrivateKey, _signature) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } else if token.Type == "erc1155" { _txHash, _, err := helpers.Order1155OnlyTransfer(_vv, _hashedData, wallet.PrivateKey, _signature) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } } else if sale.IsResale == 0 || firstOrder { fmt.Println("excution minting function") if token.Type == "erc721" { fmt.Println("excution Order721WithMinting") _txHash, _, err := helpers.Order721WithMinting(_vv, _hashedData, wallet.PrivateKey, _signature, _official) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } else if token.Type == "erc1155" { fmt.Println("excution Order1155WithMinting") _txHash, _, err := helpers.Order1155WithMinting(_vv, _hashedData, wallet.PrivateKey, _signature, _official) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } //NOTE x 메타데이터 넣어주는 작업 ( 최초 한번만 넣어 준다 ) err = util.MakeMetaData(s.metaAwsConf, tx, sale.TokenID, txHash.Hash().String(), token.Name, token.Description, token.AssetsImageURL) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.GoContextError, nil, err) tx.Rollback() return } fmt.Println("finish") } else { fmt.Println("else") gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.GoContextError, nil, err) tx.Rollback() return } fmt.Println("commit") if err := tx.Commit().Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } // HAVE TO DISCUS WITH S.M Park tx = s.mDB.Begin() defer common.DBTransaction(tx) // index 0 upper row : seller // index 1 lower row : purchaser log := []models.Log{} if err := s.rDB.Where("sale_uid =(?) AND type in (?)", sale.UID, []string{"purchase", "sell"}).Order("id desc").Limit(2).Find(&log).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } lr := []models.LogRelation{} if err := s.rDB.Where("id in (?)", []uint64{log[0].LogRelationID, log[1].LogRelationID}).Order("id desc").Find(&lr).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) return } log[0].FromAddress = null.StringFrom(wallet.Address) log[0].Price = null.FloatFrom(request.BidPrice) log[0].ToAddress = null.StringFrom(ownerWallet.Address) log[1].FromAddress = null.StringFrom(ownerWallet.Address) lr[1].UserID = null.IntFrom(int64(userID)) log[1].Price = null.FloatFrom(request.BidPrice) log[1].ToAddress = null.StringFrom(wallet.Address) if err := tx.Updates(&log[0]).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } if err := tx.Updates(&log[1]).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } if err := tx.Updates(&lr[1]).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, 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, txHash.Hash().String(), nil) return } // sellHighestPrice godoc // @Summary highest price sell // @Description 최고가 판매 // @Schemes // @security ApiKeyAuth // @Tags sale // @Accept json // @Produce json // @Param BidData body BidData true "body struct" // @Success 200 {string} OK // @Router /sale/auction [post] func (s SaleV1Router) sellHighestPrice(c *gin.Context) { userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } request := BidData{} if err := c.ShouldBindJSON(&request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } wallet := models.UserWallet{} _err := s.rDB.Where("user_id = ?", userID).Find(&wallet).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.NotFoundRecord, nil, err) return } sale := models.Sale{} _err = s.rDB.Where("id = ?", request.SaleID).Find(&sale).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.NotFoundRecord, nil, err) return } token := models.Token{} _err = s.rDB.Where("id = ?", sale.TokenID).Find(&token).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.NotFoundRecord, nil, err) return } var firstOrder bool if sale.IsResale == 1 && (token.LastestCurrency.String != "") { firstOrder = false } else if sale.IsResale == 0 || (sale.IsResale == 1 && token.LastestCurrency.String == "") { firstOrder = true } collection := models.Collection{} _err = s.rDB.Where("id = ?", token.CollectionID).Find(&collection).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.NotFoundRecord, nil, err) return } setting := models.Setting{} _err = s.rDB.Find(&setting).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.NotFoundRecord, nil, err) return } var _official bool if collection.IsOfficial == 1 { _official = true } else { _official = false } //NOTE 자기가 판매하는 물품이 맞는건지 확인 if wallet.Address != token.OwnerAddress { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("no matching rights")) return } highestBid := models.BidLog{} if err := s.rDB.Where("sale_id = ? AND is_cancel = 0", request.SaleID).Order("price desc").Limit(1).Find(&highestBid).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } buyUser := models.UserWallet{} if err := s.rDB.Where("address = ?", highestBid.Address).Find(&buyUser).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } UserDepositList := []models.Deposit{} if err := s.rDB.Where("sale_id = ? AND status = 'deposit'", request.SaleID).Find(&UserDepositList).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } //NOTE x 사용 가능 금액 검증 (구매자의 정보를 가져와서 금액이 있는지 확인한다) if request.Currency == "eth" { _, availableBalance := helpers.GetSelectedBalance(s.rDB, buyUser.UserID, request.Currency, buyUser.Address) if (highestBid.Price + setting.GasDeposit) > availableBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } } else { _, availableBalance := helpers.GetSelectedBalance(s.rDB, userID, request.Currency, buyUser.Address) if highestBid.Price > availableBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } _, availableEthBalance := helpers.GetSelectedBalance(s.rDB, userID, "eth", buyUser.Address) if setting.GasDeposit > availableEthBalance { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, errors.New("lack of holding balance")) return } var allowanceAmount *big.Int var err error var _currency helpers.ContractType if request.Currency == "mf" { _currency = helpers.MetaFinance allowanceAmount, err = helpers.GetAllowance(helpers.MetaFinance, buyUser.Address) } else if request.Currency == "mr" { _currency = helpers.MetaRare allowanceAmount, err = helpers.GetAllowance(helpers.MetaRare, buyUser.Address) } if request.BidPrice > helpers.TypeCastingToFloat64(allowanceAmount) { _, _, _, err = helpers.ApproveForERC20(_currency, buyUser.PrivateKey, common.MaxBig256) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } } } tx := s.mDB.Begin() defer common.DBTransaction(tx) if err := tx.Model(&sale).Update("status", "end").Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } if err := tx.Model(&token).Updates(models.Token{OwnerAddress: buyUser.Address, LastestPrice: null.FloatFrom(highestBid.Price), LastestCurrency: null.StringFrom(request.Currency)}).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } //FIXME a 해당 경매에 묶여있는 모든 deposit을 제거해줘야 한다! for idx, _ := range UserDepositList { if err := tx.Model(&UserDepositList[idx]).Updates(models.Deposit{Status: "withdrawal", DeletedAt: null.TimeFrom(time.Now())}).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } } /* NOTE x sale 의 hashdata, signature 를 만들어주고 업데이트, 이후 바로 동일하게 다시 만들어서 검증 */ _v := common.MakeHashData{ OwnerAddress: wallet.Address, CollectionAddress: collection.ContractAddress, TokenType: token.Type, TokenID: int64(token.ID), Currency: sale.Currency, Price: highestBid.Price, Index: int64(token.Index.Int64), TotalIndex: int64(token.TotalCount.Int64), CreationTax: int64(token.Royalties * 100), PrevTreasuryTax: int64(sale.TreasuryTax * 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 } _sigData, _hashData, err := helpers.Signing(_vv, wallet.PrivateKey) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } if err := tx.Model(&sale).Updates(&models.Sale{HashData: hex.EncodeToString(_hashData), Signature: hex.EncodeToString(_sigData)}).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.InvalidParameterValue, nil, err) tx.Rollback() return } var txHash *types.Transaction _hash, _ := hex.DecodeString(sale.HashData) _sig, _ := hex.DecodeString(sale.Signature) var _hashedData [32]byte var _signature [65]byte copy(_hashedData[:], _hash) copy(_signature[:], _sig) if sale.IsResale == 1 && !firstOrder { if token.Type == "erc721" { _txHash, _, err := helpers.Order721OnlyTransfer(_vv, _hashedData, buyUser.PrivateKey, _signature) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } else if token.Type == "erc1155" { _txHash, _, err := helpers.Order1155OnlyTransfer(_vv, _hashedData, buyUser.PrivateKey, _signature) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } } else if sale.IsResale == 0 || firstOrder { if token.Type == "erc721" { _txHash, _, err := helpers.Order721WithMinting(_vv, _hashedData, buyUser.PrivateKey, _signature, _official) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } else if token.Type == "erc1155" { _txHash, _, err := helpers.Order1155WithMinting(_vv, _hashedData, buyUser.PrivateKey, _signature, _official) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) tx.Rollback() return } txHash = _txHash } //NOTE x 메타데이터 넣어주는 작업 ( 최초 한번만 넣어 준다 ) err = util.MakeMetaData(s.metaAwsConf, tx, sale.TokenID, txHash.Hash().String(), token.Name, token.Description, token.AssetsImageURL) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.GoContextError, nil, err) tx.Rollback() return } } else { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.GoContextError, 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, txHash.Hash().String(), nil) return } // registerResale godoc // @Summary create resale // @Description NFT 구입 후 재등록 API // @Schemes // @security ApiKeyAuth // @Tags sale // @Accept json // @Produce json // @Param RegisterResaleData body RegisterResaleData true "body struct" // @Success 200 {string} OK // @Router /sale/resale [post] func (s SaleV1Router) registerResale(c *gin.Context) { userID, err := gauth.GetCurrentUserIDToInt64(c) if err != nil || userID == 0 { gerror.IntegratedResponseToRequest(c, http.StatusUnauthorized, gerror.Unauthorized, nil, err) return } request := RegisterResaleData{} if err := c.ShouldBindJSON(&request); err != nil { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, err) return } wallet := models.UserWallet{} _err := s.rDB.Where("user_id = ?", userID).Find(&wallet).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } token := models.Token{} _err = s.rDB.Where("id = ?", request.TokenID).Find(&token).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } if wallet.Address != token.OwnerAddress { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, c, errors.New("no maching rights")) } collection := models.Collection{} _err = s.rDB.Where("id = ?", token.CollectionID).Find(&collection).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } setting := models.Setting{} _err = s.rDB.Find(&setting).Error if errors.Is(_err, gorm.ErrRecordNotFound) { gerror.IntegratedResponseToRequest(c, http.StatusBadRequest, gerror.InvalidParameterValue, nil, _err) return } sale := models.Sale{ TokenID: request.TokenID, IsResale: 1, SaleType: request.SaleType, UID: common.GenerateSaleUID(s.mDB, "sale_"), Currency: request.Currency, Status: "ongoing", TreasuryTax: setting.Commission, } if request.SaleType == "fixed" { sale.Price = null.FloatFrom(request.Price) _v := common.MakeHashData{ OwnerAddress: wallet.Address, CollectionAddress: collection.ContractAddress, TokenType: token.Type, TokenID: int64(token.ID), Currency: sale.Currency, Price: request.Price, Index: int64(token.Index.Int64), TotalIndex: int64(token.TotalCount.Int64), CreationTax: int64(token.Royalties * 100), PrevTreasuryTax: int64(setting.Commission * 100), TreasuryAddress: setting.TreasuryAddress, } _vv, err := common.MakeSining(s.rDB, _v) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) return } _signature, _hashData, err := helpers.Signing(_vv, wallet.PrivateKey) if err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) return } sale.HashData = hex.EncodeToString(_hashData) sale.Signature = hex.EncodeToString(_signature) } else if request.SaleType == "time" { sale.StartPrice = null.FloatFrom(request.StartPrice) sale.StartAt = null.TimeFrom(request.StartAt) sale.EndAt = null.TimeFrom(request.EndAt) } if err := s.mDB.Save(&sale).Error; err != nil { gerror.IntegratedResponseToRequest(c, http.StatusInternalServerError, gerror.MysqlSaveError, nil, err) return } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, sale, nil) } func getQuery(c *gin.Context, name string, def interface{}) (r interface{}) { g, has := c.GetQuery(name) if has { r = g } else { r = def } return } func (s SaleV1Router) rankningSale(c *gin.Context) { ranking := []common.TopSales{} start, _ := strconv.Atoi(fmt.Sprintf("%v", getQuery(c, "start", 0))) end, _ := strconv.Atoi(fmt.Sprintf("%v", getQuery(c, "end", 0))) limited, _ := strconv.Atoi(fmt.Sprintf("%v", getQuery(c, "limit", 20))) if start == 0 || end == 0 { ranking = view.GetRankingSaleByArtist(s.rDB, limited) } else { startdate := time.UnixMilli(int64(start)) endDate := time.UnixMilli(int64(end)) ranking = view.GetRankingSaleByArtist(s.rDB, limited, startdate, endDate) } gerror.IntegratedResponseToRequest(c, http.StatusOK, gerror.OK, ranking, nil) }