mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-27 17:54:13 +00:00
209 lines
4.7 KiB
Go
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
|
|
}
|