package fetcher import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "strings" ) type AlchemyClient struct { apiKey string http *http.Client } func NewAlchemyClient(apiKey string) *AlchemyClient { return &AlchemyClient{apiKey: apiKey, http: &http.Client{}} } // FetchOwnerTokens returns tokenIDs (as strings) for the owner for a single contract. // network: e.g. "arb" (arbitrum one) -> uses arb-mainnet endpoint. func (c *AlchemyClient) FetchOwnerTokens(network, contract, owner string) ([]string, error) { if c.apiKey == "" { return nil, errors.New("missing ALCHEMY_API_KEY") } base := networkBase(network) if base == "" { return nil, fmt.Errorf("unsupported network: %s", network) } endpoint := fmt.Sprintf("%s/nft/v3/%s/getNFTsForOwner", base, c.apiKey) q := url.Values{} q.Set("owner", owner) q.Add("contractAddresses[]", contract) q.Set("withMetadata", "false") url := endpoint + "?" + q.Encode() resp, err := c.http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("alchemy error: %s", resp.Status) } var parsed struct { OwnedNfts []struct { Id struct { TokenId string `json:"tokenId"` } `json:"id"` } `json:"ownedNfts"` } if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { return nil, err } ids := make([]string, 0, len(parsed.OwnedNfts)) for _, n := range parsed.OwnedNfts { ids = append(ids, stripHexPrefix(n.Id.TokenId)) } return ids, nil } func stripHexPrefix(s string) string { s = strings.TrimPrefix(s, "0x") // Alchemy may return hex with leading zeros; keep as-is (frontend can parse) return s } func networkBase(network string) string { switch strings.ToLower(network) { case "arb", "arbitrum", "arbitrum-one": return "https://arb-mainnet.g.alchemy.com" case "arb-sepolia", "arbitrum-sepolia": return "https://arb-sepolia.g.alchemy.com" case "eth", "mainnet", "ethereum": return "https://eth-mainnet.g.alchemy.com" default: return "" } }