343 lines
8.6 KiB
Go
343 lines
8.6 KiB
Go
|
|
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,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|