package lending import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/gothinkster/golang-gin-realworld-example-app/common" "github.com/gothinkster/golang-gin-realworld-example-app/models" ) // GetUserPosition returns user's lending position // GET /api/lending/position/:address func GetUserPosition(c *gin.Context) { address := c.Param("address") if address == "" { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "wallet address is required", }) return } config := GetLendingMarketConfig() position := &UserPosition{ UserAddress: address, WalletAddress: address, SuppliedBalance: "0", SuppliedBalanceUSD: 0, BorrowedBalance: "0", BorrowedBalanceUSD: 0, CollateralBalances: map[string]CollateralInfo{}, HealthFactor: 0, LTV: 0, SupplyAPY: config["base_supply_apy"].(float64), BorrowAPY: config["base_borrow_apy"].(float64), } c.JSON(http.StatusOK, gin.H{ "success": true, "data": position, }) } // GetLendingStats returns lending market statistics // GET /api/lending/stats func GetLendingStats(c *gin.Context) { config := GetLendingMarketConfig() stats := &LendingStats{ TotalSuppliedUSD: 0, TotalBorrowedUSD: 0, TotalCollateralUSD: 0, UtilizationRate: 0, AvgSupplyAPY: config["base_supply_apy"].(float64), AvgBorrowAPY: config["base_borrow_apy"].(float64), TotalUsers: 0, ActiveBorrowers: 0, TotalTVL: 0, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": stats, }) } // GetLendingMarkets returns lending market configuration. // Contract addresses come from system_contracts; static config from GetLendingMarketConfig. // GET /api/lending/markets func GetLendingMarkets(c *gin.Context) { db := common.GetDB() var lc struct { Address string `gorm:"column:address"` ChainID int `gorm:"column:chain_id"` } db.Table("system_contracts"). Where("name = ? AND is_active = ?", "lendingProxy", true). Select("address, chain_id"). First(&lc) config := GetLendingMarketConfig() config["contract_address"] = lc.Address config["chain_id"] = lc.ChainID c.JSON(http.StatusOK, gin.H{ "success": true, "data": []interface{}{config}, }) } // SupplyUSDC handles USDC supply (deposit) transaction // POST /api/lending/supply func SupplyUSDC(c *gin.Context) { var req SupplyRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } // TODO: Validate transaction on blockchain // TODO: Update user position // TODO: Record transaction in database c.JSON(http.StatusOK, SupplyResponse{ Success: true, Message: "USDC supplied successfully", }) } // WithdrawUSDC handles USDC withdrawal transaction // POST /api/lending/withdraw func WithdrawUSDC(c *gin.Context) { var req WithdrawRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } // TODO: Validate transaction on blockchain // TODO: Update user position // TODO: Record transaction in database c.JSON(http.StatusOK, WithdrawResponse{ Success: true, Message: "USDC withdrawn successfully", }) } // SupplyCollateral handles collateral supply transaction // POST /api/lending/supply-collateral func SupplyCollateral(c *gin.Context) { var req SupplyCollateralRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } // Validate asset type - check against assets table if !ValidateYTToken(req.Asset) { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "invalid collateral asset, must be a valid YT token (YT-A, YT-B, YT-C)", }) return } // Get token info from database tokenInfo, err := GetYTTokenInfo(req.Asset, 0) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "failed to fetch token information", }) return } _ = tokenInfo // Will be used for blockchain validation // TODO: Validate transaction on blockchain using tokenInfo.ContractAddress // TODO: Update user collateral position // TODO: Record transaction in database c.JSON(http.StatusOK, SupplyCollateralResponse{ Success: true, Message: "Collateral supplied successfully", }) } // WithdrawCollateral handles collateral withdrawal transaction // POST /api/lending/withdraw-collateral func WithdrawCollateral(c *gin.Context) { var req WithdrawCollateralRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } // Validate asset type - check against assets table if !ValidateYTToken(req.Asset) { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "invalid collateral asset, must be a valid YT token (YT-A, YT-B, YT-C)", }) return } // Get token info from database tokenInfo, err := GetYTTokenInfo(req.Asset, 0) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "failed to fetch token information", }) return } _ = tokenInfo // Will be used for blockchain validation // TODO: Validate transaction on blockchain using tokenInfo.ContractAddress // TODO: Check if withdrawal is safe (health factor) // TODO: Update user collateral position // TODO: Record transaction in database c.JSON(http.StatusOK, WithdrawCollateralResponse{ Success: true, Message: "Collateral withdrawn successfully", }) } // BorrowUSDC handles USDC borrow transaction // POST /api/lending/borrow func BorrowUSDC(c *gin.Context) { var req BorrowRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } // TODO: Validate transaction on blockchain // TODO: Check collateral sufficiency // TODO: Update user borrow position // TODO: Record transaction in database c.JSON(http.StatusOK, BorrowResponse{ Success: true, Message: "USDC borrowed successfully", }) } // RepayUSDC handles USDC repayment transaction // POST /api/lending/repay func RepayUSDC(c *gin.Context) { var req RepayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } // TODO: Validate transaction on blockchain // TODO: Update user borrow position // TODO: Record transaction in database c.JSON(http.StatusOK, RepayResponse{ Success: true, Message: "USDC repaid successfully", }) } // GetLendingAPYHistory returns historical supply/borrow APY snapshots // GET /api/lending/apy-history?period=1W&chain_id=97 func GetLendingAPYHistory(c *gin.Context) { period := c.DefaultQuery("period", "1W") chainId := parseChainID(c) database := common.GetDB() var usdcAsset models.Asset if err := database.Where("asset_code = ?", "USDC").First(&usdcAsset).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "USDC asset not configured"}) return } now := time.Now() var since time.Time switch period { case "1M": since = now.AddDate(0, -1, 0) case "1Y": since = now.AddDate(-1, 0, 0) default: // 1W since = now.AddDate(0, 0, -7) } query := database.Where("asset_id = ? AND snapshot_time >= ?", usdcAsset.ID, since). Order("snapshot_time ASC") if chainId != 0 { query = query.Where("chain_id = ?", chainId) } var snapshots []models.APYSnapshot if err := query.Find(&snapshots).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "failed to fetch APY history"}) return } type DataPoint struct { Time string `json:"time"` SupplyAPY float64 `json:"supply_apy"` BorrowAPY float64 `json:"borrow_apy"` } points := make([]DataPoint, 0, len(snapshots)) for _, s := range snapshots { points = append(points, DataPoint{ Time: s.SnapshotTime.UTC().Format(time.RFC3339), SupplyAPY: s.SupplyAPY, BorrowAPY: s.BorrowAPY, }) } var currentSupplyAPY, currentBorrowAPY float64 if len(snapshots) > 0 { last := snapshots[len(snapshots)-1] currentSupplyAPY = last.SupplyAPY currentBorrowAPY = last.BorrowAPY } // APY change vs first point in period var apyChange float64 if len(snapshots) > 1 { apyChange = snapshots[len(snapshots)-1].SupplyAPY - snapshots[0].SupplyAPY } c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "history": points, "current_supply_apy": currentSupplyAPY, "current_borrow_apy": currentBorrowAPY, "apy_change": apyChange, "period": period, }, }) }