2023-07-31 23:34:53 +00:00
|
|
|
package opensea
|
|
|
|
|
|
|
|
import (
|
2023-11-14 17:16:39 +00:00
|
|
|
"context"
|
2023-07-31 23:34:53 +00:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2024-10-28 20:54:17 +00:00
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
|
|
"github.com/status-im/status-go/logutils"
|
2023-07-31 23:34:53 +00:00
|
|
|
)
|
|
|
|
|
2023-08-17 18:10:13 +00:00
|
|
|
const requestTimeout = 5 * time.Second
|
|
|
|
const getRequestRetryMaxCount = 15
|
|
|
|
const getRequestWaitTime = 300 * time.Millisecond
|
|
|
|
|
2023-07-31 23:34:53 +00:00
|
|
|
type HTTPClient struct {
|
|
|
|
client *http.Client
|
|
|
|
getRequestLock sync.RWMutex
|
|
|
|
}
|
|
|
|
|
2023-08-17 18:10:13 +00:00
|
|
|
func NewHTTPClient() *HTTPClient {
|
2023-07-31 23:34:53 +00:00
|
|
|
return &HTTPClient{
|
|
|
|
client: &http.Client{
|
2023-08-17 18:10:13 +00:00
|
|
|
Timeout: requestTimeout,
|
2023-07-31 23:34:53 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:16:39 +00:00
|
|
|
func (o *HTTPClient) doGetRequest(ctx context.Context, url string, apiKey string) ([]byte, error) {
|
2023-07-31 23:34:53 +00:00
|
|
|
// Ensure only one thread makes a request at a time
|
|
|
|
o.getRequestLock.Lock()
|
|
|
|
defer o.getRequestLock.Unlock()
|
|
|
|
|
|
|
|
retryCount := 0
|
|
|
|
statusCode := http.StatusOK
|
|
|
|
|
|
|
|
// Try to do the request without an apiKey first
|
|
|
|
tmpAPIKey := ""
|
|
|
|
|
|
|
|
for {
|
2023-11-14 17:16:39 +00:00
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
2023-07-31 23:34:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0")
|
|
|
|
if len(tmpAPIKey) > 0 {
|
|
|
|
req.Header.Set("X-API-KEY", tmpAPIKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := o.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
if err := resp.Body.Close(); err != nil {
|
2024-10-28 20:54:17 +00:00
|
|
|
logutils.ZapLogger().Error("failed to close opensea request body", zap.Error(err))
|
2023-07-31 23:34:53 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
statusCode = resp.StatusCode
|
|
|
|
switch resp.StatusCode {
|
|
|
|
case http.StatusOK:
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
return body, err
|
2023-08-23 19:29:09 +00:00
|
|
|
case http.StatusBadRequest:
|
|
|
|
// The OpenSea v2 API will return error 400 if the account holds no collectibles on
|
|
|
|
// the requested chain. This shouldn't be treated as an error, return an empty body.
|
|
|
|
return nil, nil
|
2023-07-31 23:34:53 +00:00
|
|
|
case http.StatusTooManyRequests:
|
2023-08-17 18:10:13 +00:00
|
|
|
if retryCount < getRequestRetryMaxCount {
|
2023-07-31 23:34:53 +00:00
|
|
|
// sleep and retry
|
2023-08-17 18:10:13 +00:00
|
|
|
time.Sleep(getRequestWaitTime)
|
2023-07-31 23:34:53 +00:00
|
|
|
retryCount++
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// break and error
|
|
|
|
case http.StatusForbidden:
|
|
|
|
// Request requires an apiKey, set it and retry
|
|
|
|
if tmpAPIKey == "" && apiKey != "" {
|
|
|
|
tmpAPIKey = apiKey
|
|
|
|
// sleep and retry
|
2023-08-17 18:10:13 +00:00
|
|
|
time.Sleep(getRequestWaitTime)
|
2023-07-31 23:34:53 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
// break and error
|
|
|
|
default:
|
|
|
|
// break and error
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsuccessful request: %d %s", statusCode, http.StatusText(statusCode))
|
|
|
|
}
|