Files
assetx/webapp-back/lending/helpers.go
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

246 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package lending
import (
"context"
"encoding/hex"
"fmt"
"log"
"math/big"
"time"
"github.com/ethereum/go-ethereum"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
appcfg "github.com/gothinkster/golang-gin-realworld-example-app/config"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"github.com/gothinkster/golang-gin-realworld-example-app/models"
)
// TokenInfo represents token contract information
type TokenInfo struct {
Symbol string
Name string
Decimals int
ContractAddress string
ChainID int
AssetCode string
}
// getRPCURL returns the RPC URL for a given chain ID from config
func getRPCURL(chainId int) string {
cfg := appcfg.AppConfig
switch chainId {
case 97: // BSC Testnet
if cfg != nil && cfg.BSCTestnetRPC != "" {
return cfg.BSCTestnetRPC
}
case 421614: // Arbitrum Sepolia
if cfg != nil && cfg.ArbSepoliaRPC != "" {
return cfg.ArbSepoliaRPC
}
}
return ""
}
// getContractAddressByChain returns the contract address from TokenInfo (chain-agnostic now)
func getContractAddressByChain(info TokenInfo, chainId int) string {
return info.ContractAddress
}
// fetchDecimalsOnChain calls decimals() on an ERC20 contract via RPC
// Returns error if chain call fails so caller can fall back to DB
func fetchDecimalsOnChain(contractAddress, rpcURL string) (int, error) {
if rpcURL == "" || contractAddress == "" {
return 0, fmt.Errorf("missing rpcURL or contractAddress")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, err := ethclient.DialContext(ctx, rpcURL)
if err != nil {
return 0, fmt.Errorf("dial rpc: %w", err)
}
defer client.Close()
// decimals() selector = keccak256("decimals()")[0:4] = 313ce567
data, _ := hex.DecodeString("313ce567")
addr := ethcommon.HexToAddress(contractAddress)
result, err := client.CallContract(ctx, ethereum.CallMsg{
To: &addr,
Data: data,
}, nil)
if err != nil {
return 0, fmt.Errorf("call contract: %w", err)
}
if len(result) < 1 {
return 0, fmt.Errorf("empty result")
}
decimals := new(big.Int).SetBytes(result[len(result)-1:])
return int(decimals.Int64()), nil
}
// resolveDecimals tries chain first (if chainId != 0), falls back to dbDecimals
func resolveDecimals(contractAddress string, chainId, dbDecimals int) int {
if chainId == 0 {
return dbDecimals
}
rpcURL := getRPCURL(chainId)
if rpcURL == "" {
return dbDecimals
}
decimals, err := fetchDecimalsOnChain(contractAddress, rpcURL)
if err != nil {
log.Printf("⚠️ [Lending] 链上获取精度失败 %s (chain %d): %v使用数据库配置 %d", contractAddress, chainId, err, dbDecimals)
return dbDecimals
}
return decimals
}
// GetTokenInfoFromDB queries any token from assets table by asset_code
func GetTokenInfoFromDB(assetCode string) (*TokenInfo, error) {
db := common.GetDB()
var asset models.Asset
if err := db.Where("asset_code = ? AND is_active = ?", assetCode, true).First(&asset).Error; err != nil {
return nil, err
}
return &TokenInfo{
Symbol: asset.TokenSymbol,
Name: asset.Name,
Decimals: asset.Decimals,
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
AssetCode: asset.AssetCode,
}, nil
}
// GetUSDCInfo returns USDC token information.
// chainId != 0: tries on-chain decimals() first, falls back to DB.
// chainId == 0: DB only.
func GetUSDCInfo(chainId int) TokenInfo {
info, err := GetTokenInfoFromDB("USDC")
if err != nil {
log.Printf("⚠️ [Lending] USDC 不在 assets 表: %v使用默认值", err)
return TokenInfo{AssetCode: "USDC", Symbol: "USDC", Name: "USD Coin", Decimals: 18}
}
contractAddr := getContractAddressByChain(*info, chainId)
info.Decimals = resolveDecimals(contractAddr, chainId, info.Decimals)
return *info
}
// GetYTTokenInfo returns YT token information.
// chainId != 0: tries on-chain decimals() first, falls back to DB.
func GetYTTokenInfo(assetCode string, chainId int) (*TokenInfo, error) {
db := common.GetDB()
var asset models.Asset
if err := db.Where("asset_code = ? AND is_active = ?", assetCode, true).First(&asset).Error; err != nil {
return nil, err
}
info := &TokenInfo{
Symbol: asset.TokenSymbol,
Name: asset.Name,
Decimals: asset.Decimals,
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
AssetCode: asset.AssetCode,
}
info.Decimals = resolveDecimals(info.ContractAddress, info.ChainID, info.Decimals)
return info, nil
}
// GetAllStablecoins returns all active stablecoin tokens (token_role = 'stablecoin').
func GetAllStablecoins(chainId int) ([]TokenInfo, error) {
db := common.GetDB()
var assets []models.Asset
if err := db.Where("token_role = ? AND is_active = ?", "stablecoin", true).Find(&assets).Error; err != nil {
return nil, err
}
tokens := make([]TokenInfo, 0, len(assets))
for _, asset := range assets {
info := TokenInfo{
Symbol: asset.TokenSymbol,
Name: asset.Name,
Decimals: asset.Decimals,
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
AssetCode: asset.AssetCode,
}
info.Decimals = resolveDecimals(info.ContractAddress, chainId, info.Decimals)
tokens = append(tokens, info)
}
return tokens, nil
}
// GetAllYTTokens returns all active YT tokens.
// chainId != 0: tries on-chain decimals() first, falls back to DB.
func GetAllYTTokens(chainId int) ([]TokenInfo, error) {
db := common.GetDB()
var assets []models.Asset
if err := db.Where("token_role = ? AND is_active = ?", "yt_token", true).Find(&assets).Error; err != nil {
return nil, err
}
tokens := make([]TokenInfo, 0, len(assets))
for _, asset := range assets {
info := TokenInfo{
Symbol: asset.TokenSymbol,
Name: asset.Name,
Decimals: asset.Decimals,
ContractAddress: asset.ContractAddress,
ChainID: asset.ChainID,
AssetCode: asset.AssetCode,
}
info.Decimals = resolveDecimals(info.ContractAddress, info.ChainID, info.Decimals)
tokens = append(tokens, info)
}
return tokens, nil
}
// ValidateYTToken checks if the given asset code is a valid YT token
func ValidateYTToken(assetCode string) bool {
db := common.GetDB()
var count int64
db.Model(&models.Asset{}).
Where("asset_code = ? AND token_role = ? AND is_active = ?", assetCode, "yt_token", true).
Count(&count)
return count > 0
}
// GetLendingMarketConfig returns lending market configuration
func GetLendingMarketConfig() map[string]interface{} {
return map[string]interface{}{
"market_name": "AssetX Lending Market",
"base_supply_apy": 6.1,
"base_borrow_apy": 9.1,
"borrow_collateral_factor": 0.7,
"liquidate_collateral_factor": 0.75,
"liquidation_penalty": 0.1,
"kink_rate": 0.8,
}
}
// CalculateHealthFactor calculates health factor
func CalculateHealthFactor(collateralValueUSD, borrowedValueUSD float64) float64 {
if borrowedValueUSD == 0 {
return 999999.0
}
config := GetLendingMarketConfig()
liquidationThreshold := config["liquidate_collateral_factor"].(float64)
return (collateralValueUSD * liquidationThreshold) / borrowedValueUSD
}
// CalculateLTV calculates Loan-to-Value ratio
func CalculateLTV(collateralValueUSD, borrowedValueUSD float64) float64 {
if collateralValueUSD == 0 {
return 0
}
return (borrowedValueUSD / collateralValueUSD) * 100
}
// GetContractAddress returns contract address for the given chain
func GetContractAddress(token TokenInfo, chainID int) string {
return getContractAddressByChain(token, chainID)
}