package fetcher import ( "bytes" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" ) type RPCClient struct { // Map of network key -> RPC URL, e.g. {"eth": "https://mainnet.infura.io/v3/...", "arb": "https://arb1...", "base": "https://base-mainnet..."} RPC map[string]string http *http.Client rateLimiter chan struct{} // Rate limiter: 5 TPS } func NewRPCClient(rpc map[string]string) *RPCClient { client := &RPCClient{ RPC: rpc, http: &http.Client{}, rateLimiter: make(chan struct{}, 5), // 5 TPS rate limiter } // Fill the rate limiter initially for i := 0; i < 5; i++ { client.rateLimiter <- struct{}{} } // Refill rate limiter at 5 TPS (200ms intervals) go func() { ticker := time.NewTicker(200 * time.Millisecond) defer ticker.Stop() for range ticker.C { select { case client.rateLimiter <- struct{}{}: default: // Channel is full, skip } } }() return client } // FetchOwnerTokens iterates through tokenIds 0,1,2,... up to maxTokenId // and calls ownerOf for each to find which ones belong to owner. // network: key like "eth", "arb", "base". // contract: 0x... // owner: 0x... // maxTokenId: maximum tokenId to check // FetchAllTokenOwners scans all tokens 0..maxTokenId and returns ownership map func (c *RPCClient) FetchAllTokenOwners(network, contract string, maxTokenId int, debug bool) (map[string]string, error) { rpcURL := c.RPC[strings.ToLower(network)] if rpcURL == "" { return nil, fmt.Errorf("missing RPC for network %s", network) } contract = strings.ToLower(contract) owners := make(map[string]string) consecutiveErrors := 0 maxConsecutiveErrors := 50 // Stop if 50 consecutive tokens don't exist // Check each tokenId from 0 to maxTokenId for i := 0; i < maxTokenId; i++ { tokenOwner, err := c.getOwnerOf(rpcURL, contract, i, debug) if err != nil { // If we hit the first invalid token ID, stop immediately as requested if strings.Contains(strings.ToLower(err.Error()), "invalid token id") { if debug { fmt.Printf("DEBUG: Stopping at first invalid token ID %d due to error: %v\n", i, err) } break } consecutiveErrors++ if debug { fmt.Printf("DEBUG: Token %d error: %v (consecutive errors: %d)\n", i, err, consecutiveErrors) } // Early termination if too many consecutive errors if consecutiveErrors >= maxConsecutiveErrors { if debug { fmt.Printf("DEBUG: Stopping scan at token %d due to %d consecutive errors\n", i, consecutiveErrors) } break } continue } consecutiveErrors = 0 // Reset on successful call // Store tokenId -> canonicalized owner canonicalOwner := canonicalizeAddr(tokenOwner) owners[strconv.Itoa(i)] = canonicalOwner // Log specific tokens for debugging if debug && (i == 103 || i == 0 || i == 1 || i%100 == 0 || len(owners) <= 50) { fmt.Printf("DEBUG: Token %d owner: %s (canonical: %s)\n", i, tokenOwner, canonicalOwner) } // Always log token 103 regardless of debug mode if i == 103 { fmt.Printf("ALWAYS: Token 103 owner: %s (canonical: %s)\n", tokenOwner, canonicalOwner) } } fmt.Printf("DEBUG: Scanned up to token %d, found %d with owners\n", maxTokenId, len(owners)) return owners, nil } func (c *RPCClient) FetchOwnerTokens(network, contract, owner string, maxTokenId int) ([]string, error) { rpcURL := c.RPC[strings.ToLower(network)] if rpcURL == "" { return nil, fmt.Errorf("missing RPC for network %s", network) } contract = strings.ToLower(contract) owner = strings.ToLower(owner) var owned []string // Check each tokenId from 0 to maxTokenId for i := 0; i < maxTokenId; i++ { tokenOwner, err := c.getOwnerOf(rpcURL, contract, i, false) // No debug for this method if err != nil { // Token might not exist, skip continue } if strings.EqualFold(tokenOwner, owner) { owned = append(owned, strconv.Itoa(i)) } } return owned, nil } // canonicalizeAddr normalizes address to 0x + 40 lowercase hex func canonicalizeAddr(addr string) string { x := strings.ToLower(strings.TrimSpace(addr)) if strings.HasPrefix(x, "0x") { x = x[2:] } if len(x) > 40 { x = x[len(x)-40:] } if len(x) < 40 { x = strings.Repeat("0", 40-len(x)) + x } return "0x" + x } // getOwnerOf calls ownerOf(tokenId) on the contract with rate limiting and retry logic func (c *RPCClient) getOwnerOf(rpcURL, contract string, tokenId int, debug bool) (string, error) { maxRetries := 10 baseDelay := 100 * time.Millisecond for attempt := 0; attempt <= maxRetries; attempt++ { // Wait for rate limiter <-c.rateLimiter if debug && attempt > 0 { fmt.Printf("DEBUG: Retry attempt %d for token %d\n", attempt, tokenId) } result, err := c.makeRPCCall(rpcURL, contract, tokenId, debug) if err != nil { // Check if it's a 429 error if strings.Contains(err.Error(), "429") || strings.Contains(err.Error(), "Too Many Requests") { if attempt < maxRetries { // Exponential backoff: 100ms, 200ms, 400ms, 800ms, etc. delay := time.Duration(1<= 42 { owner := "0x" + result.Result[len(result.Result)-40:] if debug { fmt.Printf("DEBUG: Extracted owner tokenId=%d owner=%s\n", tokenId, owner) } return owner, nil } if debug { fmt.Printf("DEBUG: Invalid response length tokenId=%d result=%s\n", tokenId, result.Result) } return "", fmt.Errorf("invalid ownerOf response") }