From 8f9c644daeaa057a1291f31c667e6a8ea2393aec Mon Sep 17 00:00:00 2001 From: Anthony Laibe Date: Fri, 20 Aug 2021 21:53:24 +0200 Subject: [PATCH] feat: fetch assets from opensea (#2320) --- services/wallet/api.go | 10 +++ services/wallet/opensea.go | 134 ++++++++++++++++++++++++++++++++ services/wallet/opensea_test.go | 63 +++++++++++++++ services/wallet/service.go | 2 + 4 files changed, 209 insertions(+) create mode 100644 services/wallet/opensea.go create mode 100644 services/wallet/opensea_test.go diff --git a/services/wallet/api.go b/services/wallet/api.go index e48302d58..e5828ef64 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -269,3 +269,13 @@ func (api *API) GetCachedBalances(ctx context.Context, addresses []common.Addres return blocksToViews(result), nil } + +func (api *API) GetOpenseaCollectionsByOwner(ctx context.Context, owner common.Address) ([]OpenseaCollection, error) { + log.Debug("call to get opensea collections") + return api.s.opensea.fetchAllCollectionsByOwner(owner) +} + +func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, owner common.Address, collectionSlug string, limit int) ([]OpenseaAsset, error) { + log.Debug("call to get opensea assets") + return api.s.opensea.fetchAllAssetsByOwnerAndCollection(owner, collectionSlug, limit) +} diff --git a/services/wallet/opensea.go b/services/wallet/opensea.go new file mode 100644 index 000000000..f217581e2 --- /dev/null +++ b/services/wallet/opensea.go @@ -0,0 +1,134 @@ +package wallet + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +const AssetLimit = 50 +const CollectionLimit = 300 + +type OpenseaAssetContainer struct { + Assets []OpenseaAsset `json:"assets"` +} + +type OpenseaAssetCollection struct { + Name string `json:"name"` +} + +type OpenseaContract struct { + Address string `json:"address"` +} + +type OpenseaAsset struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Permalink string `json:"permalink"` + ImageThumbnailURL string `json:"image_thumbnail_url"` + ImageURL string `json:"image_url"` + Contract OpenseaContract `json:"asset_contract"` + Collection OpenseaAssetCollection `json:"collection"` +} + +type OpenseaCollection struct { + Name string `json:"name"` + Slug string `json:"slug"` + ImageURL string `json:"image_url"` + OwnedAssetCount int `json:"owned_asset_count"` +} + +type OpenseaClient struct { + client *http.Client + url string +} + +// new opensea client. +func newOpenseaClient() *OpenseaClient { + client := &http.Client{ + Timeout: time.Second * 5, + } + + return &OpenseaClient{client: client, url: "https://api.opensea.io/api/v1"} +} + +func (o *OpenseaClient) fetchAllCollectionsByOwner(owner common.Address) ([]OpenseaCollection, error) { + offset := 0 + var collections []OpenseaCollection + for { + url := fmt.Sprintf("%s/collections?asset_owner=%s&offset=%d&limit=%d", o.url, owner, offset, CollectionLimit) + body, err := o.doOpenseaRequest(url) + if err != nil { + return nil, err + } + + var tmp []OpenseaCollection + err = json.Unmarshal(body, &tmp) + if err != nil { + return nil, err + } + + collections = append(collections, tmp...) + + if len(tmp) < CollectionLimit { + break + } + } + return collections, nil +} + +func (o *OpenseaClient) fetchAllAssetsByOwnerAndCollection(owner common.Address, collectionSlug string, limit int) ([]OpenseaAsset, error) { + offset := 0 + var assets []OpenseaAsset + for { + url := fmt.Sprintf("%s/assets?owner=%s&collection=%s&offset=%d&limit=%d", o.url, owner, collectionSlug, offset, AssetLimit) + body, err := o.doOpenseaRequest(url) + if err != nil { + return nil, err + } + + container := OpenseaAssetContainer{} + err = json.Unmarshal(body, &container) + if err != nil { + return nil, err + } + + assets = append(assets, container.Assets...) + + if len(container.Assets) < AssetLimit { + break + } + + if len(assets) >= limit { + break + } + } + return assets, nil +} + +func (o *OpenseaClient) doOpenseaRequest(url string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := o.client.Do(req) + if err != nil { + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Error("failed to close opensea request body", "err", err) + } + }() + + body, err := ioutil.ReadAll(resp.Body) + return body, err +} diff --git a/services/wallet/opensea_test.go b/services/wallet/opensea_test.go new file mode 100644 index 000000000..91676ff44 --- /dev/null +++ b/services/wallet/opensea_test.go @@ -0,0 +1,63 @@ +package wallet + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ethereum/go-ethereum/common" +) + +func TestFetchAllCollectionsByOwner(t *testing.T) { + expected := []OpenseaCollection{OpenseaCollection{Name: "Rocky", Slug: "rocky", ImageURL: "ImageUrl", OwnedAssetCount: 1}} + response, _ := json.Marshal(expected) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, err := w.Write(response) + if err != nil { + return + } + })) + defer srv.Close() + + opensea := &OpenseaClient{ + client: srv.Client(), + url: srv.URL, + } + res, err := opensea.fetchAllCollectionsByOwner(common.Address{1}) + assert.Equal(t, expected, res) + assert.Nil(t, err) +} + +func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) { + expected := []OpenseaAsset{OpenseaAsset{ + ID: 1, + Name: "Rocky", + Description: "Rocky Balboa", + Permalink: "permalink", + ImageThumbnailURL: "ImageThumbnailURL", + ImageURL: "ImageUrl", + Contract: OpenseaContract{Address: "1"}, + Collection: OpenseaAssetCollection{Name: "Rocky"}, + }} + response, _ := json.Marshal(OpenseaAssetContainer{Assets: expected}) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, err := w.Write(response) + if err != nil { + return + } + })) + defer srv.Close() + + opensea := &OpenseaClient{ + client: srv.Client(), + url: srv.URL, + } + res, err := opensea.fetchAllAssetsByOwnerAndCollection(common.Address{1}, "rocky", 200) + assert.Equal(t, expected, res) + assert.Nil(t, err) +} diff --git a/services/wallet/service.go b/services/wallet/service.go index f748e3d22..1048bf5a2 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -25,6 +25,7 @@ func NewService(db *Database, accountsFeed *event.Feed) *Service { publisher: feed, }, accountsFeed: accountsFeed, + opensea: newOpenseaClient(), } } @@ -37,6 +38,7 @@ type Service struct { client *walletClient cryptoOnRampManager *CryptoOnRampManager started bool + opensea *OpenseaClient group *Group accountsFeed *event.Feed