Files
assetx/webapp-back/lending/helpers.go

246 lines
7.6 KiB
Go
Raw Normal View History

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)
}