init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
208
webapp-back/lending/snapshot.go
Normal file
208
webapp-back/lending/snapshot.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package lending
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
appcfg "github.com/gothinkster/golang-gin-realworld-example-app/config"
|
||||
db "github.com/gothinkster/golang-gin-realworld-example-app/common"
|
||||
"github.com/gothinkster/golang-gin-realworld-example-app/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const lendingSnapshotInterval = 15 * time.Minute
|
||||
|
||||
// Only the 4 view functions needed for APY snapshot
|
||||
const lendingRateABIJSON = `[
|
||||
{"inputs":[],"name":"getSupplyRate","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},
|
||||
{"inputs":[],"name":"getBorrowRate","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},
|
||||
{"inputs":[],"name":"getTotalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
|
||||
{"inputs":[],"name":"getTotalBorrow","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}
|
||||
]`
|
||||
|
||||
// StartLendingAPYSnapshot starts the hourly lending APY snapshot background service.
|
||||
// Call as: go lending.StartLendingAPYSnapshot(cfg)
|
||||
func StartLendingAPYSnapshot(cfg *appcfg.Config) {
|
||||
log.Println("=== Lending APY Snapshot Service Started (interval: 15min) ===")
|
||||
runLendingSnapshot(cfg)
|
||||
ticker := time.NewTicker(lendingSnapshotInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
runLendingSnapshot(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func runLendingSnapshot(cfg *appcfg.Config) {
|
||||
start := time.Now()
|
||||
log.Printf("[LendingSnapshot] Starting at %s", start.Format("2006-01-02 15:04:05"))
|
||||
|
||||
database := db.GetDB()
|
||||
|
||||
// Load lendingProxy contracts from system_contracts
|
||||
var lendingContracts []struct {
|
||||
Name string `gorm:"column:name"`
|
||||
ChainID int `gorm:"column:chain_id"`
|
||||
Address string `gorm:"column:address"`
|
||||
}
|
||||
if err := database.Table("system_contracts").
|
||||
Where("name = ? AND is_active = ?", "lendingProxy", true).
|
||||
Select("name, chain_id, address").
|
||||
Find(&lendingContracts).Error; err != nil {
|
||||
log.Printf("[LendingSnapshot] Load lendingProxy error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(lendingContracts) == 0 {
|
||||
log.Println("[LendingSnapshot] No lendingProxy in system_contracts, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
var usdcAsset models.Asset
|
||||
if err := database.Where("asset_code = ?", "USDC").First(&usdcAsset).Error; err != nil {
|
||||
log.Printf("[LendingSnapshot] USDC asset not found in assets table: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(lendingRateABIJSON))
|
||||
if err != nil {
|
||||
log.Printf("[LendingSnapshot] Parse ABI error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, lc := range lendingContracts {
|
||||
if lc.Address == "" {
|
||||
log.Printf("[LendingSnapshot] lendingProxy chain_id=%d has no address, skipping", lc.ChainID)
|
||||
continue
|
||||
}
|
||||
rpcURL := getRPCURL(lc.ChainID)
|
||||
if rpcURL == "" {
|
||||
log.Printf("[LendingSnapshot] lendingProxy unsupported chain_id=%d, skipping", lc.ChainID)
|
||||
continue
|
||||
}
|
||||
if err := snapshotLendingMarket(database, parsedABI, rpcURL, lc.Address, lc.ChainID, usdcAsset); err != nil {
|
||||
log.Printf("[LendingSnapshot] lendingProxy chain_id=%d error: %v", lc.ChainID, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[LendingSnapshot] Done in %v", time.Since(start))
|
||||
}
|
||||
|
||||
func snapshotLendingMarket(
|
||||
database *gorm.DB,
|
||||
parsedABI abi.ABI,
|
||||
rpcURL, contractAddr string,
|
||||
chainID int,
|
||||
usdcAsset models.Asset,
|
||||
) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client, err := ethclient.DialContext(ctx, rpcURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial rpc: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
addr := ethcommon.HexToAddress(contractAddr)
|
||||
|
||||
// Generic call helper: packs, calls, unpacks, returns *big.Int
|
||||
call := func(name string) (*big.Int, error) {
|
||||
data, err := parsedABI.Pack(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pack %s: %w", name, err)
|
||||
}
|
||||
result, err := client.CallContract(ctx, ethereum.CallMsg{To: &addr, Data: data}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("call %s: %w", name, err)
|
||||
}
|
||||
decoded, err := parsedABI.Unpack(name, result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unpack %s: %w", name, err)
|
||||
}
|
||||
if len(decoded) == 0 {
|
||||
return nil, fmt.Errorf("%s: empty result", name)
|
||||
}
|
||||
switch v := decoded[0].(type) {
|
||||
case uint64:
|
||||
return new(big.Int).SetUint64(v), nil
|
||||
case *big.Int:
|
||||
return v, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%s: unexpected type %T", name, decoded[0])
|
||||
}
|
||||
}
|
||||
|
||||
supplyRateRaw, err := call("getSupplyRate")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
borrowRateRaw, err := call("getBorrowRate")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalSupplyRaw, err := call("getTotalSupply")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalBorrowRaw, err := call("getTotalBorrow")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// APR% = rate / 1e18 * 100
|
||||
// This contract's getSupplyRate/getBorrowRate already return annualized rate (APR × 1e18),
|
||||
// NOT per-second rate. Do NOT multiply by secondsPerYear.
|
||||
toAPRPct := func(raw *big.Int) float64 {
|
||||
f, _ := new(big.Float).SetPrec(256).Quo(
|
||||
new(big.Float).SetPrec(256).SetInt(raw),
|
||||
new(big.Float).SetPrec(256).SetFloat64(1e18),
|
||||
).Float64()
|
||||
return f * 100
|
||||
}
|
||||
|
||||
usdcDec := int64(usdcAsset.Decimals)
|
||||
totalSupply := lendingBigIntToFloat(totalSupplyRaw, usdcDec)
|
||||
totalBorrow := lendingBigIntToFloat(totalBorrowRaw, usdcDec)
|
||||
supplyAPR := toAPRPct(supplyRateRaw)
|
||||
borrowAPR := toAPRPct(borrowRateRaw)
|
||||
|
||||
log.Printf("[LendingSnapshot] supply=%.4f USDC, borrow=%.4f USDC, utilization=%.2f%%, supplyAPR=%.4f%%, borrowAPR=%.4f%%",
|
||||
totalSupply, totalBorrow,
|
||||
func() float64 {
|
||||
if totalSupply == 0 {
|
||||
return 0
|
||||
}
|
||||
return totalBorrow / totalSupply * 100
|
||||
}(),
|
||||
supplyAPR, borrowAPR,
|
||||
)
|
||||
|
||||
snap := models.APYSnapshot{
|
||||
AssetID: usdcAsset.ID,
|
||||
ChainID: chainID,
|
||||
ContractAddress: contractAddr,
|
||||
SupplyAPY: supplyAPR,
|
||||
BorrowAPY: borrowAPR,
|
||||
TotalAssets: totalSupply, // total USDC deposited
|
||||
TotalSupply: totalBorrow, // total USDC borrowed
|
||||
SnapshotTime: time.Now(),
|
||||
}
|
||||
return database.Create(&snap).Error
|
||||
}
|
||||
|
||||
// lendingBigIntToFloat converts *big.Int to float64 by dividing by 10^decimals
|
||||
func lendingBigIntToFloat(n *big.Int, decimals int64) float64 {
|
||||
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(decimals), nil)
|
||||
f, _ := new(big.Float).SetPrec(256).Quo(
|
||||
new(big.Float).SetPrec(256).SetInt(n),
|
||||
new(big.Float).SetPrec(256).SetInt(divisor),
|
||||
).Float64()
|
||||
return f
|
||||
}
|
||||
Reference in New Issue
Block a user