209 lines
4.7 KiB
Go

package helius
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
rpcURL string
apiKey string
http *http.Client
nftCache *Cache[NFTResult]
tokCache *Cache[float64]
}
type NFTResult struct {
HasTeamNFT bool
HasCommunityNFT bool
Count int
}
func NewClient(apiKey, rpcURL string) *Client {
return &Client{
rpcURL: rpcURL,
apiKey: apiKey,
http: &http.Client{Timeout: 15 * time.Second},
nftCache: NewCache[NFTResult](5 * time.Minute),
tokCache: NewCache[float64](5 * time.Minute),
}
}
// CheckNFTs checks if a wallet holds DeBros Team (100) or Community (700) NFTs.
func (c *Client) CheckNFTs(wallet, teamCollection, communityCollection string) (NFTResult, error) {
cacheKey := "nft:" + wallet
if cached, ok := c.nftCache.Get(cacheKey); ok {
return cached, nil
}
result := NFTResult{}
// Check team NFTs
teamCount, err := c.searchAssetsByCollection(wallet, teamCollection)
if err != nil {
return result, fmt.Errorf("failed to check team NFTs: %w", err)
}
// Check community NFTs
commCount, err := c.searchAssetsByCollection(wallet, communityCollection)
if err != nil {
return result, fmt.Errorf("failed to check community NFTs: %w", err)
}
result.HasTeamNFT = teamCount > 0
result.HasCommunityNFT = commCount > 0
result.Count = teamCount + commCount
c.nftCache.Set(cacheKey, result)
return result, nil
}
// GetTokenBalance returns the token balance for a specific mint.
func (c *Client) GetTokenBalance(wallet, mint string) (float64, error) {
cacheKey := "token:" + wallet + ":" + mint
if cached, ok := c.tokCache.Get(cacheKey); ok {
return cached, nil
}
body := map[string]any{
"jsonrpc": "2.0",
"id": "token-balance",
"method": "getTokenAccountsByOwner",
"params": []any{
wallet,
map[string]string{"mint": mint},
map[string]string{"encoding": "jsonParsed"},
},
}
resp, err := c.rpcCall(body)
if err != nil {
return 0, err
}
result, ok := resp["result"].(map[string]any)
if !ok {
return 0, nil
}
value, ok := result["value"].([]any)
if !ok || len(value) == 0 {
c.tokCache.Set(cacheKey, 0)
return 0, nil
}
// Parse the first token account
account, ok := value[0].(map[string]any)
if !ok {
return 0, nil
}
balance := extractUIAmount(account)
c.tokCache.Set(cacheKey, balance)
return balance, nil
}
// GetTokenBalanceFresh bypasses cache (used for claims).
func (c *Client) GetTokenBalanceFresh(wallet, mint string) (float64, error) {
cacheKey := "token:" + wallet + ":" + mint
c.tokCache.Delete(cacheKey)
return c.GetTokenBalance(wallet, mint)
}
func (c *Client) searchAssetsByCollection(owner, collectionAddr string) (int, error) {
body := map[string]any{
"jsonrpc": "2.0",
"id": "search-assets",
"method": "searchAssets",
"params": map[string]any{
"ownerAddress": owner,
"grouping": []any{"collection", collectionAddr},
"page": 1,
"limit": 1000,
},
}
resp, err := c.rpcCall(body)
if err != nil {
return 0, err
}
result, ok := resp["result"].(map[string]any)
if !ok {
return 0, nil
}
total, ok := result["total"].(float64)
if ok {
return int(total), nil
}
items, ok := result["items"].([]any)
if !ok {
return 0, nil
}
return len(items), nil
}
func (c *Client) rpcCall(body map[string]any) (map[string]any, error) {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
resp, err := c.http.Post(c.rpcURL, "application/json", bytes.NewReader(jsonBody))
if err != nil {
return nil, fmt.Errorf("helius request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("helius returned status %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]any
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if errObj, ok := result["error"]; ok {
return nil, fmt.Errorf("helius RPC error: %v", errObj)
}
return result, nil
}
func extractUIAmount(account map[string]any) float64 {
data, _ := account["account"].(map[string]any)
if data == nil {
return 0
}
dataInner, _ := data["data"].(map[string]any)
if dataInner == nil {
return 0
}
parsed, _ := dataInner["parsed"].(map[string]any)
if parsed == nil {
return 0
}
info, _ := parsed["info"].(map[string]any)
if info == nil {
return 0
}
tokenAmount, _ := info["tokenAmount"].(map[string]any)
if tokenAmount == nil {
return 0
}
uiAmount, _ := tokenAmount["uiAmount"].(float64)
return uiAmount
}