2023-02-21 09:05:16 +00:00
|
|
|
package opensea
|
2021-08-20 19:53:24 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-02-28 03:10:38 +00:00
|
|
|
"net/url"
|
2021-08-30 07:50:18 +00:00
|
|
|
"strconv"
|
2022-08-03 07:42:56 +00:00
|
|
|
"strings"
|
2021-08-20 19:53:24 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
2023-03-20 13:02:09 +00:00
|
|
|
"github.com/ethereum/go-ethereum/event"
|
2023-02-28 03:10:38 +00:00
|
|
|
|
2023-04-18 14:33:59 +00:00
|
|
|
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
2023-07-10 09:02:17 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/connection"
|
2023-03-21 13:52:14 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/thirdparty"
|
2023-03-20 13:02:09 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/walletevent"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-07-31 23:34:53 +00:00
|
|
|
EventCollectibleStatusChanged walletevent.EventType = "wallet-collectible-opensea-v1-status-changed"
|
2021-08-20 19:53:24 +00:00
|
|
|
)
|
|
|
|
|
2023-07-31 23:34:53 +00:00
|
|
|
const OpenseaV1ID = "openseaV1"
|
|
|
|
|
2023-03-06 16:15:48 +00:00
|
|
|
const AssetLimit = 200
|
2021-08-20 19:53:24 +00:00
|
|
|
const CollectionLimit = 300
|
|
|
|
|
2023-03-27 18:50:19 +00:00
|
|
|
const RequestTimeout = 5 * time.Second
|
|
|
|
const GetRequestRetryMaxCount = 15
|
|
|
|
const GetRequestWaitTime = 300 * time.Millisecond
|
2023-02-28 03:10:38 +00:00
|
|
|
|
2023-07-12 17:43:22 +00:00
|
|
|
const ChainIDRequiringAPIKey = walletCommon.EthereumMainnet
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
type urlGetter func(walletCommon.ChainID, string) (string, error)
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func getBaseURL(chainID walletCommon.ChainID) (string, error) {
|
2023-07-18 15:01:53 +00:00
|
|
|
// v1 Endpoints only support L1 chain
|
|
|
|
switch uint64(chainID) {
|
2023-07-12 17:43:22 +00:00
|
|
|
case walletCommon.EthereumMainnet:
|
2023-03-27 18:50:19 +00:00
|
|
|
return "https://api.opensea.io/api/v1", nil
|
2023-07-12 17:43:22 +00:00
|
|
|
case walletCommon.EthereumGoerli:
|
2023-03-27 18:50:19 +00:00
|
|
|
return "https://testnets-api.opensea.io/api/v1", nil
|
|
|
|
}
|
2023-01-17 09:56:16 +00:00
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
return "", thirdparty.ErrChainIDNotSupported
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Client) ID() string {
|
2023-07-31 23:34:53 +00:00
|
|
|
return OpenseaV1ID
|
2023-07-31 19:41:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
|
|
|
|
_, err := getBaseURL(chainID)
|
|
|
|
return err == nil
|
2021-09-20 16:24:07 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func getURL(chainID walletCommon.ChainID, path string) (string, error) {
|
2023-07-31 19:41:14 +00:00
|
|
|
baseURL, err := getBaseURL(chainID)
|
2023-07-18 15:01:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s/%s", baseURL, path), nil
|
|
|
|
}
|
|
|
|
|
2023-02-21 09:05:16 +00:00
|
|
|
type Client struct {
|
2023-07-10 09:02:17 +00:00
|
|
|
client *HTTPClient
|
|
|
|
apiKey string
|
|
|
|
connectionStatus *connection.Status
|
2023-07-18 15:01:53 +00:00
|
|
|
urlGetter urlGetter
|
2021-08-20 19:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// new opensea client.
|
2023-07-10 09:02:17 +00:00
|
|
|
func NewClient(apiKey string, feed *event.Feed) *Client {
|
|
|
|
return &Client{
|
|
|
|
client: newHTTPClient(),
|
|
|
|
apiKey: apiKey,
|
|
|
|
connectionStatus: connection.NewStatus(EventCollectibleStatusChanged, feed),
|
2023-07-18 15:01:53 +00:00
|
|
|
urlGetter: getURL,
|
2023-03-27 18:50:19 +00:00
|
|
|
}
|
2023-07-10 09:02:17 +00:00
|
|
|
}
|
2023-03-27 18:50:19 +00:00
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Client) FetchAllCollectionsByOwner(chainID walletCommon.ChainID, owner common.Address) ([]OwnedCollection, error) {
|
2023-07-10 09:02:17 +00:00
|
|
|
offset := 0
|
|
|
|
var collections []OwnedCollection
|
2023-01-17 09:56:16 +00:00
|
|
|
|
2021-08-20 19:53:24 +00:00
|
|
|
for {
|
2023-07-18 15:01:53 +00:00
|
|
|
path := fmt.Sprintf("collections?asset_owner=%s&offset=%d&limit=%d", owner, offset, CollectionLimit)
|
|
|
|
url, err := o.urlGetter(chainID, path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-03-27 18:50:19 +00:00
|
|
|
body, err := o.client.doGetRequest(url, o.apiKey)
|
2021-08-20 19:53:24 +00:00
|
|
|
if err != nil {
|
2023-07-10 09:02:17 +00:00
|
|
|
o.connectionStatus.SetIsConnected(false)
|
2021-08-20 19:53:24 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2023-07-10 09:02:17 +00:00
|
|
|
o.connectionStatus.SetIsConnected(true)
|
2021-08-20 19:53:24 +00:00
|
|
|
|
2023-03-20 15:53:39 +00:00
|
|
|
// if Json is not returned there must be an error
|
|
|
|
if !json.Valid(body) {
|
|
|
|
return nil, fmt.Errorf("invalid json: %s", string(body))
|
|
|
|
}
|
|
|
|
|
2023-02-28 03:10:38 +00:00
|
|
|
var tmp []OwnedCollection
|
2021-08-20 19:53:24 +00:00
|
|
|
err = json.Unmarshal(body, &tmp)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
collections = append(collections, tmp...)
|
|
|
|
|
|
|
|
if len(tmp) < CollectionLimit {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return collections, nil
|
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
2023-02-28 03:10:38 +00:00
|
|
|
queryParams := url.Values{
|
|
|
|
"owner": {owner.String()},
|
|
|
|
"collection": {collectionSlug},
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:15:48 +00:00
|
|
|
if len(cursor) > 0 {
|
|
|
|
queryParams["cursor"] = []string{cursor}
|
|
|
|
}
|
|
|
|
|
2023-07-10 09:02:17 +00:00
|
|
|
return o.fetchAssets(chainID, queryParams, limit)
|
2023-03-06 16:15:48 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
2023-03-17 21:20:31 +00:00
|
|
|
queryParams := url.Values{
|
|
|
|
"owner": {owner.String()},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, contractAddress := range contractAddresses {
|
|
|
|
queryParams.Add("asset_contract_addresses", contractAddress.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(cursor) > 0 {
|
|
|
|
queryParams["cursor"] = []string{cursor}
|
|
|
|
}
|
|
|
|
|
2023-07-10 09:02:17 +00:00
|
|
|
return o.fetchAssets(chainID, queryParams, limit)
|
2023-03-17 21:20:31 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Client) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
2023-03-06 16:15:48 +00:00
|
|
|
queryParams := url.Values{
|
|
|
|
"owner": {owner.String()},
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(cursor) > 0 {
|
|
|
|
queryParams["cursor"] = []string{cursor}
|
|
|
|
}
|
|
|
|
|
2023-07-10 09:02:17 +00:00
|
|
|
return o.fetchAssets(chainID, queryParams, limit)
|
2023-02-28 03:10:38 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Client) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
2023-02-28 03:10:38 +00:00
|
|
|
queryParams := url.Values{}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
ret := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
|
2023-07-18 15:01:53 +00:00
|
|
|
|
|
|
|
idsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(uniqueIDs)
|
|
|
|
for chainID, ids := range idsPerChainID {
|
|
|
|
for _, id := range ids {
|
|
|
|
queryParams.Add("token_ids", id.TokenID.String())
|
2023-07-31 19:41:14 +00:00
|
|
|
queryParams.Add("asset_contract_addresses", id.ContractID.Address.String())
|
2023-07-18 15:01:53 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 23:34:53 +00:00
|
|
|
data, err := o.fetchAssets(chainID, queryParams, thirdparty.FetchNoLimit)
|
2023-07-18 15:01:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
ret = append(ret, data.Items...)
|
2023-02-28 03:10:38 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Client) fetchAssets(chainID walletCommon.ChainID, queryParams url.Values, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
|
|
|
assets := new(thirdparty.FullCollectibleDataContainer)
|
2023-07-18 15:01:53 +00:00
|
|
|
|
|
|
|
if len(queryParams["cursor"]) > 0 {
|
|
|
|
assets.PreviousCursor = queryParams["cursor"][0]
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpLimit := AssetLimit
|
2023-07-31 23:34:53 +00:00
|
|
|
if limit > thirdparty.FetchNoLimit && limit < tmpLimit {
|
2023-07-18 15:01:53 +00:00
|
|
|
tmpLimit = limit
|
|
|
|
}
|
|
|
|
|
|
|
|
queryParams["limit"] = []string{strconv.Itoa(tmpLimit)}
|
|
|
|
for {
|
|
|
|
path := "assets?" + queryParams.Encode()
|
|
|
|
url, err := o.urlGetter(chainID, path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := o.client.doGetRequest(url, o.apiKey)
|
|
|
|
if err != nil {
|
|
|
|
o.connectionStatus.SetIsConnected(false)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
o.connectionStatus.SetIsConnected(true)
|
|
|
|
|
|
|
|
// if Json is not returned there must be an error
|
|
|
|
if !json.Valid(body) {
|
|
|
|
return nil, fmt.Errorf("invalid json: %s", string(body))
|
|
|
|
}
|
|
|
|
|
|
|
|
container := AssetContainer{}
|
|
|
|
err = json.Unmarshal(body, &container)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, asset := range container.Assets {
|
2023-07-31 19:41:14 +00:00
|
|
|
assets.Items = append(assets.Items, asset.toCommon())
|
2023-07-18 15:01:53 +00:00
|
|
|
}
|
|
|
|
assets.NextCursor = container.NextCursor
|
|
|
|
|
|
|
|
if len(assets.NextCursor) == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
queryParams["cursor"] = []string{assets.NextCursor}
|
|
|
|
|
2023-07-31 23:34:53 +00:00
|
|
|
if limit > thirdparty.FetchNoLimit && len(assets.Items) >= limit {
|
2023-07-18 15:01:53 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return assets, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only here for compatibility with mobile app, to be removed
|
|
|
|
func (o *Client) FetchAllOpenseaAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*AssetContainer, error) {
|
|
|
|
queryParams := url.Values{
|
|
|
|
"owner": {owner.String()},
|
|
|
|
"collection": {collectionSlug},
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(cursor) > 0 {
|
|
|
|
queryParams["cursor"] = []string{cursor}
|
|
|
|
}
|
|
|
|
|
|
|
|
return o.fetchOpenseaAssets(chainID, queryParams, limit)
|
2023-02-28 03:10:38 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Client) fetchOpenseaAssets(chainID walletCommon.ChainID, queryParams url.Values, limit int) (*AssetContainer, error) {
|
2023-03-06 16:15:48 +00:00
|
|
|
assets := new(AssetContainer)
|
|
|
|
|
|
|
|
if len(queryParams["cursor"]) > 0 {
|
|
|
|
assets.PreviousCursor = queryParams["cursor"][0]
|
|
|
|
}
|
|
|
|
|
2023-03-17 21:20:31 +00:00
|
|
|
tmpLimit := AssetLimit
|
|
|
|
if limit > 0 && limit < tmpLimit {
|
2023-06-15 13:51:55 +00:00
|
|
|
tmpLimit = limit
|
2023-03-06 16:15:48 +00:00
|
|
|
}
|
2023-02-28 03:10:38 +00:00
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
baseURL, err := getBaseURL(chainID)
|
2023-07-10 09:02:17 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:15:48 +00:00
|
|
|
queryParams["limit"] = []string{strconv.Itoa(tmpLimit)}
|
2021-08-20 19:53:24 +00:00
|
|
|
for {
|
2023-07-10 09:02:17 +00:00
|
|
|
url := baseURL + "/assets?" + queryParams.Encode()
|
2023-02-28 03:10:38 +00:00
|
|
|
|
2023-03-27 18:50:19 +00:00
|
|
|
body, err := o.client.doGetRequest(url, o.apiKey)
|
2021-08-20 19:53:24 +00:00
|
|
|
if err != nil {
|
2023-07-10 09:02:17 +00:00
|
|
|
o.connectionStatus.SetIsConnected(false)
|
2021-08-20 19:53:24 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2023-07-10 09:02:17 +00:00
|
|
|
o.connectionStatus.SetIsConnected(true)
|
2021-08-20 19:53:24 +00:00
|
|
|
|
2023-03-20 15:53:39 +00:00
|
|
|
// if Json is not returned there must be an error
|
|
|
|
if !json.Valid(body) {
|
|
|
|
return nil, fmt.Errorf("invalid json: %s", string(body))
|
|
|
|
}
|
|
|
|
|
2023-02-21 09:05:16 +00:00
|
|
|
container := AssetContainer{}
|
2021-08-20 19:53:24 +00:00
|
|
|
err = json.Unmarshal(body, &container)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-08-03 07:42:56 +00:00
|
|
|
for _, asset := range container.Assets {
|
|
|
|
for i := range asset.Traits {
|
2022-11-29 12:28:37 +00:00
|
|
|
asset.Traits[i].TraitType = strings.Replace(asset.Traits[i].TraitType, "_", " ", 1)
|
2022-08-03 07:42:56 +00:00
|
|
|
asset.Traits[i].Value = TraitValue(strings.Title(string(asset.Traits[i].Value)))
|
|
|
|
}
|
2023-03-06 16:15:48 +00:00
|
|
|
assets.Assets = append(assets.Assets, asset)
|
2022-08-03 07:42:56 +00:00
|
|
|
}
|
2023-03-06 16:15:48 +00:00
|
|
|
assets.NextCursor = container.NextCursor
|
2021-08-20 19:53:24 +00:00
|
|
|
|
2023-03-06 16:15:48 +00:00
|
|
|
if len(assets.NextCursor) == 0 {
|
2023-02-28 03:10:38 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:15:48 +00:00
|
|
|
queryParams["cursor"] = []string{assets.NextCursor}
|
2023-02-28 03:10:38 +00:00
|
|
|
|
2023-03-17 21:20:31 +00:00
|
|
|
if limit > 0 && len(assets.Assets) >= limit {
|
2021-08-20 19:53:24 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 09:56:16 +00:00
|
|
|
|
2021-08-20 19:53:24 +00:00
|
|
|
return assets, nil
|
|
|
|
}
|