Repository: trustwallet/blockatlas (go)

Issue:
Add support for BSC collectibles
https://docs.bounce.finance/nft-issuance/nft-api-reference

Pull request:
[Collection] Add bounce collection api
Fixes #1313 

Patch:
diff --git a/config.yml b/config.yml
index 72640d5..69d0bdc 100644
--- a/config.yml
+++ b/config.yml
@@ -214,9 +214,11 @@ elrond:
 filecoin:
   api: https://api.filscan.io:8700/rpc/v1
   explorer: https://filfox.info
-  # bsc:
-  # api: <blockbook api>
-  # rpc: <rpc>
+
+# smartchain:
+#   api: <blockbook api>
+#   rpc: https://bsc-dataseed.nariox.org
+#   collections_api: https://nftview.bounce.finance
 
 sentry:
   dsn: ""
diff --git a/config/configuration.go b/config/configuration.go
index 3015365..175ba4e 100755
--- a/config/configuration.go
+++ b/config/configuration.go
@@ -68,12 +68,14 @@ type Configuration struct {
 		RPC string `mapstructure:"rpc"`
 	} `mapstructure:"classic"`
 	Smartchain struct {
-		API string `mapstructure:"api"`
-		RPC string `mapstructure:"rpc"`
+		API            string `mapstructure:"api"`
+		RPC            string `mapstructure:"rpc"`
+		CollectionsAPI string `mapstructure:"collections_api"`
 	} `mapstructure:"smartchain"`
 	BSC struct {
-		API string `mapstructure:"api"`
-		RPC string `mapstructure:"rpc"`
+		API            string `mapstructure:"api"`
+		RPC            string `mapstructure:"rpc"`
+		CollectionsAPI string `mapstructure:"collections_api"`
 	} `mapstructure:"bsc"`
 	Poa struct {
 		API string `mapstructure:"api"`
diff --git a/pkg/blockatlas/collectible.go b/pkg/blockatlas/collectible.go
new file mode 100644
index 0000000..4d478a4
--- /dev/null
+++ b/pkg/blockatlas/collectible.go
@@ -0,0 +1,7 @@
+package blockatlas
+
+import "fmt"
+
+func GenCollectibleId(contract, tokenId string) string {
+	return fmt.Sprintf("%s-%s", contract, tokenId)
+}
diff --git a/platform/ethereum/base.go b/platform/ethereum/base.go
index c2b528b..3cf04c2 100644
--- a/platform/ethereum/base.go
+++ b/platform/ethereum/base.go
@@ -2,7 +2,8 @@ package ethereum
 
 import (
 	"github.com/trustwallet/blockatlas/platform/bitcoin/blockbook"
-	"github.com/trustwallet/blockatlas/platform/ethereum/collection"
+	"github.com/trustwallet/blockatlas/platform/ethereum/bounce"
+	"github.com/trustwallet/blockatlas/platform/ethereum/opensea"
 	"github.com/trustwallet/blockatlas/platform/ethereum/trustray"
 	"github.com/trustwallet/golibs/client"
 	"github.com/trustwallet/golibs/coin"
@@ -13,7 +14,7 @@ type Platform struct {
 	CoinIndex   uint
 	RpcURL      string
 	client      EthereumClient
-	collectible collection.Client
+	collectible CollectibleClient
 }
 
 func Init(coinType uint, api, rpc string) *Platform {
@@ -32,10 +33,15 @@ func InitWithBlockbook(coinType uint, blockbookApi, rpc string) *Platform {
 	}
 }
 
-func InitWithCollection(coinType uint, rpc, blockbookApi, collectionApi, collectionKey string) *Platform {
+func InitWithOpenSea(coinType uint, rpc, blockbookApi, collectionApi, collectionKey string) *Platform {
 	platform := InitWithBlockbook(coinType, blockbookApi, rpc)
-	platform.collectible = collection.Client{Request: client.InitClient(collectionApi, middleware.SentryErrorHandler)}
-	platform.collectible.Headers["X-API-KEY"] = collectionKey
+	platform.collectible = opensea.InitClient(collectionApi, collectionKey)
+	return platform
+}
+
+func InitWithBounce(coinType uint, rpc, blockbookApi, collectionApi string) *Platform {
+	platform := InitWithBlockbook(coinType, blockbookApi, rpc)
+	platform.collectible = bounce.InitClient(collectionApi)
 	return platform
 }
 
diff --git a/platform/ethereum/block.go b/platform/ethereum/block.go
new file mode 100644
index 0000000..b886009
--- /dev/null
+++ b/platform/ethereum/block.go
@@ -0,0 +1,11 @@
+package ethereum
+
+import "github.com/trustwallet/golibs/types"
+
+func (p *Platform) CurrentBlockNumber() (int64, error) {
+	return p.client.GetCurrentBlockNumber()
+}
+
+func (p *Platform) GetBlockByNumber(num int64) (*types.Block, error) {
+	return p.client.GetBlockByNumber(num, p.CoinIndex)
+}
diff --git a/platform/ethereum/bounce/client.go b/platform/ethereum/bounce/client.go
new file mode 100644
index 0000000..04fb8cd
--- /dev/null
+++ b/platform/ethereum/bounce/client.go
@@ -0,0 +1,88 @@
+package bounce
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/trustwallet/golibs/client"
+	"github.com/trustwallet/golibs/network/middleware"
+)
+
+const (
+	httpScheme = "http"
+	ipfsScheme = "ipfs"
+)
+
+type Client struct {
+	client.Request
+}
+
+func InitClient(url string) *Client {
+	c := Client{client.InitClient(url, middleware.SentryErrorHandler)}
+	return &c
+}
+
+func (c Client) getCollections(owner string, chainId int) ([]Collection, error) {
+	query := url.Values{
+		"address":  {owner},
+		"chain_id": {strconv.Itoa(chainId)},
+	}
+	var resp CollectionResponse
+	err := c.Get(&resp, "nft", query)
+	if err != nil {
+		return nil, err
+	}
+	return resp.Data.Collections, nil
+}
+
+func (c Client) getCollectibles(owner string, collectionID string, chainId int) ([]Collectible, error) {
+	query := url.Values{
+		"user_addr":     {owner},
+		"contract_addr": {collectionID},
+		"chain_id":      {strconv.Itoa(chainId)},
+	}
+
+	var resp CollectibleResponse
+	err := c.Get(&resp, "erc721", query)
+	if err != nil {
+		return nil, err
+	}
+	return resp.Data.Collectibles, err
+}
+
+func fetchTokenURI(uri string) (info CollectionInfo, err error) {
+	url, err := url.Parse(uri)
+	if err != nil {
+		return
+	}
+
+	var c client.Request
+	if strings.HasPrefix(url.Scheme, httpScheme) {
+		c = client.InitClient(uri, middleware.SentryErrorHandler)
+	} else if strings.HasPrefix(url.Scheme, ipfsScheme) {
+		c = client.InitClient(ipfsGatewayUrl(url), middleware.SentryErrorHandler)
+	} else {
+		return info, errors.New("not supported url scheme: " + url.Scheme)
+	}
+
+	err = c.Get(&info, "", nil)
+	return
+}
+
+func normalizeImageUrl(uri string) string {
+	url, err := url.Parse(uri)
+	if err != nil {
+		return uri
+	}
+	if url.Scheme != ipfsScheme {
+		return uri
+	}
+	return ipfsGatewayUrl(url)
+}
+
+func ipfsGatewayUrl(url *url.URL) string {
+	return fmt.Sprintf("https://ipfs.io/ipfs/%s%s", url.Host, url.Path)
+}
diff --git a/platform/ethereum/bounce/collection.go b/platform/ethereum/bounce/collection.go
new file mode 100644
index 0000000..0058cf6
--- /dev/null
+++ b/platform/ethereum/bounce/collection.go
@@ -0,0 +1,103 @@
+package bounce
+
+import (
+	"strconv"
+
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/golibs/types"
+)
+
+var (
+	bscChainId = 56
+	nftVersion = "3.0" // opensea nft_version compatible
+)
+
+func (c *Client) GetCollections(owner string, coinIndex uint) (types.CollectionPage, error) {
+	collections, err := c.getCollections(owner, bscChainId)
+	if err != nil {
+		return nil, err
+	}
+	return c.NormalizeCollections(collections, coinIndex, owner)
+
+}
+
+func (c *Client) GetCollectibles(owner, collectionID string, coinIndex uint) (types.CollectiblePage, error) {
+	collectibles, err := c.getCollectibles(owner, collectionID, bscChainId)
+	if err != nil {
+		return nil, err
+	}
+	return c.NormalizeCollectibles(collectibles, coinIndex)
+}
+
+func (c *Client) NormalizeCollections(collections []Collection, coinIndex uint, owner string) (types.CollectionPage, error) {
+	page := make(types.CollectionPage, 0)
+	category := map[string]bool{}
+	for _, cl := range collections {
+
+		// existed category
+		if _, ok := category[cl.ContractAddr]; ok {
+			continue
+		}
+
+		total, err := strconv.Atoi(cl.Balance)
+		if err != nil {
+			continue
+		}
+		// skip empty info token
+		if len(cl.TokenURI) == 0 {
+			continue
+		}
+
+		info, err := fetchTokenURI(cl.TokenURI)
+		if err != nil {
+			return nil, err
+		}
+
+		// skip empty name/image
+		if len(info.Name) == 0 || len(info.Image) == 0 {
+			continue
+		}
+
+		page = append(page, types.Collection{
+			Id:           cl.ContractAddr,
+			Name:         info.Name,
+			ImageUrl:     normalizeImageUrl(info.Image),
+			Description:  info.Description,
+			ExternalLink: cl.TokenURI,
+			Total:        total,
+			Address:      owner,
+			Coin:         coinIndex,
+			Type:         "ERC" + cl.TokenType,
+		})
+		category[cl.ContractAddr] = true
+	}
+	return page, nil
+}
+
+func (c *Client) NormalizeCollectibles(collectibles []Collectible, coinIndex uint) (types.CollectiblePage, error) {
+	if len(collectibles) == 0 {
+		return types.CollectiblePage{}, nil
+	}
+	page := make(types.CollectiblePage, 0)
+	info, err := fetchTokenURI(collectibles[0].TokenURI)
+	if err != nil {
+		return nil, err
+	}
+	for _, c := range collectibles {
+		tokenId := strconv.Itoa(c.TokenID)
+		page = append(page, types.Collectible{
+			ID:              blockatlas.GenCollectibleId(c.ContractAddr, tokenId),
+			CollectionID:    c.ContractAddr,
+			TokenID:         tokenId,
+			ContractAddress: c.ContractAddr,
+			ImageUrl:        normalizeImageUrl(info.Image),
+			ExternalLink:    c.TokenURI,
+			Type:            string(types.ERC721),
+			Description:     info.Description,
+			Coin:            coinIndex,
+			Name:            info.Name,
+			Version:         nftVersion,
+		})
+	}
+	return page, nil
+}
diff --git a/platform/ethereum/bounce/model.go b/platform/ethereum/bounce/model.go
new file mode 100644
index 0000000..8e1d54c
--- /dev/null
+++ b/platform/ethereum/bounce/model.go
@@ -0,0 +1,48 @@
+package bounce
+
+type Response struct {
+	CodeStatus int    `json:"codeStatus"`
+	Msg        string `json:"msg"`
+}
+
+type Collectible struct {
+	ContractAddr string `json:"contract_addr"`
+	TokenID      int    `json:"token_id"`
+	OwnerAddr    string `json:"owner_addr"`
+	ChainID      int    `json:"chain_id"`
+	TokenURI     string `json:"token_uri"`
+}
+
+type CollectibleList struct {
+	Collectibles []Collectible `json:"tokens"`
+}
+
+type CollectibleResponse struct {
+	Response
+	Data CollectibleList `json:"data"`
+}
+
+type Collection struct {
+	ContractAddr string `json:"contract_addr"`
+	TokenType    string `json:"token_type"`
+	TokenID      int    `json:"token_id"`
+	OwnerAddr    string `json:"owner_addr"`
+	ChainID      int    `json:"chain_id"`
+	Balance      string `json:"balance"`
+	TokenURI     string `json:"token_uri"`
+}
+
+type CollectionList struct {
+	Collections []Collection `json:"nfts"`
+}
+
+type CollectionResponse struct {
+	Response
+	Data CollectionList `json:"data"`
+}
+
+type CollectionInfo struct {
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	Image       string `json:"image"`
+}
diff --git a/platform/ethereum/client.go b/platform/ethereum/client.go
index 219765a..020345c 100644
--- a/platform/ethereum/client.go
+++ b/platform/ethereum/client.go
@@ -10,14 +10,7 @@ type EthereumClient interface {
 	GetBlockByNumber(num int64, coinIndex uint) (*types.Block, error)
 }
 
-func (p *Platform) GetTokenListByAddress(address string) ([]string, error) {
-	return p.client.GetTokenList(address, p.CoinIndex)
-}
-
-func (p *Platform) CurrentBlockNumber() (int64, error) {
-	return p.client.GetCurrentBlockNumber()
-}
-
-func (p *Platform) GetBlockByNumber(num int64) (*types.Block, error) {
-	return p.client.GetBlockByNumber(num, p.CoinIndex)
+type CollectibleClient interface {
+	GetCollections(owner string, coinIndex uint) (types.CollectionPage, error)
+	GetCollectibles(owner, collectionID string, coinIndex uint) (types.CollectiblePage, error)
 }
diff --git a/platform/ethereum/collection.go b/platform/ethereum/collection.go
index 07d33dd..2ef9e76 100644
--- a/platform/ethereum/collection.go
+++ b/platform/ethereum/collection.go
@@ -1,78 +1,13 @@
 package ethereum
 
 import (
-	"strings"
-
-	"github.com/trustwallet/blockatlas/platform/ethereum/collection"
 	"github.com/trustwallet/golibs/types"
 )
 
-var (
-	supportedTypes = map[string]bool{"ERC721": true, "ERC1155": true}
-)
-
 func (p *Platform) GetCollections(owner string) (types.CollectionPage, error) {
-	collections, err := p.collectible.GetCollections(owner)
-	if err != nil {
-		return nil, err
-	}
-	return NormalizeCollections(collections, p.CoinIndex, owner), nil
-}
-
-func (p *Platform) GetCollectibles(owner, collectibleID string) (types.CollectiblePage, error) {
-	items, err := p.collectible.GetCollectibles(owner, collectibleID)
-	if err != nil {
-		return nil, err
-	}
-	return NormalizeCollectiblePage(items, p.CoinIndex), nil
-}
-
-func NormalizeCollections(collections []collection.Collection, coinIndex uint, owner string) (page types.CollectionPage) {
-	for _, collection := range collections {
-		item := NormalizeCollection(collection, coinIndex, owner)
-		page = append(page, item)
-	}
-	return page
-}
-
-func NormalizeCollection(c collection.Collection, coinIndex uint, owner string) types.Collection {
-	return types.Collection{
-		Name:         c.Name,
-		ImageUrl:     c.ImageUrl,
-		Description:  c.Description,
-		ExternalLink: c.ExternalUrl,
-		Total:        int(c.Total.Int64()),
-		Id:           c.Slug,
-		Address:      owner,
-		Coin:         coinIndex,
-	}
-}
-
-func NormalizeCollectiblePage(collectibles []collection.Collectible, coinIndex uint) (page types.CollectiblePage) {
-	for _, collectible := range collectibles {
-		item := NormalizeCollectible(collectible, coinIndex)
-		if _, ok := supportedTypes[item.Type]; ok {
-			page = append(page, item)
-		}
-	}
-	return page
+	return p.collectible.GetCollections(owner, p.CoinIndex)
 }
 
-func NormalizeCollectible(c collection.Collectible, coinIndex uint) types.Collectible {
-	id := strings.Join([]string{c.AssetContract.Address, c.TokenId}, "-")
-	return types.Collectible{
-		ID:              id,
-		CollectionID:    c.Collection.Slug,
-		TokenID:         c.TokenId,
-		ContractAddress: c.AssetContract.Address,
-		Name:            c.Name,
-		Category:        c.Collection.Name,
-		ImageUrl:        c.ImagePreviewUrl,
-		ProviderLink:    c.Permalink,
-		ExternalLink:    c.Collection.ExternalLink,
-		Type:            c.AssetContract.Type,
-		Description:     c.Description,
-		Coin:            coinIndex,
-		Version:         c.AssetContract.Version,
-	}
+func (p *Platform) GetCollectibles(owner, collectionID string) (types.CollectiblePage, error) {
+	return p.collectible.GetCollectibles(owner, collectionID, p.CoinIndex)
 }
diff --git a/platform/ethereum/collection/client.go b/platform/ethereum/collection/client.go
deleted file mode 100644
index c2c646a..0000000
--- a/platform/ethereum/collection/client.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package collection
-
-import (
-	"net/url"
-	"strconv"
-
-	"github.com/trustwallet/golibs/client"
-)
-
-type Client struct {
-	client.Request
-}
-
-func (c Client) GetCollections(owner string) (page []Collection, err error) {
-	query := url.Values{
-		"asset_owner": {owner},
-		"limit":       {"1000"},
-	}
-	err = c.Get(&page, "api/v1/collections", query)
-	return
-}
-
-func (c Client) GetCollectibles(owner string, collectibleID string) ([]Collectible, error) {
-	query := url.Values{
-		"owner":      {owner},
-		"collection": {collectibleID},
-		"limit":      {strconv.Itoa(300)},
-	}
-
-	var page CollectiblePage
-	err := c.Get(&page, "api/v1/assets", query)
-	return page.Collectibles, err
-}
diff --git a/platform/ethereum/opensea/client.go b/platform/ethereum/opensea/client.go
new file mode 100644
index 0000000..8a308de
--- /dev/null
+++ b/platform/ethereum/opensea/client.go
@@ -0,0 +1,40 @@
+package opensea
+
+import (
+	"net/url"
+	"strconv"
+
+	"github.com/trustwallet/golibs/client"
+	"github.com/trustwallet/golibs/network/middleware"
+)
+
+type Client struct {
+	client.Request
+}
+
+func InitClient(api string, apiKey string) *Client {
+	c := Client{client.InitClient(api, middleware.SentryErrorHandler)}
+	c.Headers["X-API-KEY"] = apiKey
+	return &c
+}
+
+func (c Client) GetCollectionsByOwner(owner string) (page []Collection, err error) {
+	query := url.Values{
+		"asset_owner": {owner},
+		"limit":       {"1000"},
+	}
+	err = c.Get(&page, "api/v1/collections", query)
+	return
+}
+
+func (c Client) GetCollectiblesByCollectionId(owner string, collectionId string) ([]Collectible, error) {
+	query := url.Values{
+		"owner":      {owner},
+		"collection": {collectionId},
+		"limit":      {strconv.Itoa(300)},
+	}
+
+	var page CollectiblePage
+	err := c.Get(&page, "api/v1/assets", query)
+	return page.Collectibles, err
+}
diff --git a/platform/ethereum/opensea/collection.go b/platform/ethereum/opensea/collection.go
new file mode 100644
index 0000000..3e9f16b
--- /dev/null
+++ b/platform/ethereum/opensea/collection.go
@@ -0,0 +1,76 @@
+package opensea
+
+import (
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/golibs/types"
+)
+
+var (
+	supportedTypes = map[string]bool{"ERC721": true, "ERC1155": true}
+)
+
+func (c *Client) GetCollections(owner string, coinIndex uint) (types.CollectionPage, error) {
+	collections, err := c.GetCollectionsByOwner(owner)
+	if err != nil {
+		return nil, err
+	}
+	return NormalizeCollections(collections, coinIndex, owner), nil
+
+}
+
+func (c *Client) GetCollectibles(owner, collectionId string, coinIndex uint) (types.CollectiblePage, error) {
+	items, err := c.GetCollectiblesByCollectionId(owner, collectionId)
+	if err != nil {
+		return nil, err
+	}
+	return NormalizeCollectiblePage(items, coinIndex), nil
+}
+
+func NormalizeCollections(collections []Collection, coinIndex uint, owner string) (page types.CollectionPage) {
+	for _, collection := range collections {
+		item := NormalizeCollection(collection, coinIndex, owner)
+		page = append(page, item)
+	}
+	return page
+}
+
+func NormalizeCollection(c Collection, coinIndex uint, owner string) types.Collection {
+	return types.Collection{
+		Name:         c.Name,
+		ImageUrl:     c.ImageUrl,
+		Description:  c.Description,
+		ExternalLink: c.ExternalUrl,
+		Total:        int(c.Total.Int64()),
+		Id:           c.Slug,
+		Address:      owner,
+		Coin:         coinIndex,
+	}
+}
+
+func NormalizeCollectiblePage(collectibles []Collectible, coinIndex uint) (page types.CollectiblePage) {
+	for _, collectible := range collectibles {
+		item := NormalizeCollectible(collectible, coinIndex)
+		if _, ok := supportedTypes[item.Type]; ok {
+			page = append(page, item)
+		}
+	}
+	return page
+}
+
+func NormalizeCollectible(c Collectible, coinIndex uint) types.Collectible {
+	return types.Collectible{
+		ID:              blockatlas.GenCollectibleId(c.AssetContract.Address, c.TokenId),
+		CollectionID:    c.Collection.Slug,
+		TokenID:         c.TokenId,
+		ContractAddress: c.AssetContract.Address,
+		Name:            c.Name,
+		Category:        c.Collection.Name,
+		ImageUrl:        c.ImagePreviewUrl,
+		ProviderLink:    c.Permalink,
+		ExternalLink:    c.Collection.ExternalLink,
+		Type:            c.AssetContract.Type,
+		Description:     c.Description,
+		Coin:            coinIndex,
+		Version:         c.AssetContract.Version,
+	}
+}
diff --git a/platform/ethereum/mocks/opensea_collectible.json b/platform/ethereum/opensea/mocks/opensea_collectible.json
similarity index 100%
rename from platform/ethereum/mocks/opensea_collectible.json
rename to platform/ethereum/opensea/mocks/opensea_collectible.json
diff --git a/platform/ethereum/mocks/opensea_collections.json b/platform/ethereum/opensea/mocks/opensea_collections.json
similarity index 100%
rename from platform/ethereum/mocks/opensea_collections.json
rename to platform/ethereum/opensea/mocks/opensea_collections.json
diff --git a/platform/ethereum/collection/model.go b/platform/ethereum/opensea/model.go
similarity index 99%
rename from platform/ethereum/collection/model.go
rename to platform/ethereum/opensea/model.go
index 7065295..1d63b22 100644
--- a/platform/ethereum/collection/model.go
+++ b/platform/ethereum/opensea/model.go
@@ -1,4 +1,4 @@
-package collection
+package opensea
 
 import (
 	"math/big"
diff --git a/platform/ethereum/transaction.go b/platform/ethereum/transaction.go
index e0568bc..89348a5 100644
--- a/platform/ethereum/transaction.go
+++ b/platform/ethereum/transaction.go
@@ -9,3 +9,7 @@ func (p *Platform) GetTxsByAddress(address string) (types.TxPage, error) {
 func (p *Platform) GetTokenTxsByAddress(address string, token string) (types.TxPage, error) {
 	return p.client.GetTokenTxs(address, token, p.CoinIndex)
 }
+
+func (p *Platform) GetTokenListByAddress(address string) ([]string, error) {
+	return p.client.GetTokenList(address, p.CoinIndex)
+}
diff --git a/platform/platform.go b/platform/platform.go
index 1f59a60..ffc0504 100644
--- a/platform/platform.go
+++ b/platform/platform.go
@@ -92,9 +92,9 @@ func getAllHandlers() blockatlas.Platforms {
 		coin.Callisto().Handle:     ethereum.Init(coin.CLO, config.Default.Callisto.API, config.Default.Callisto.RPC),
 		coin.Wanchain().Handle:     ethereum.Init(coin.WAN, config.Default.Wanchain.API, config.Default.Wanchain.RPC),
 		coin.Tomochain().Handle:    ethereum.Init(coin.TOMO, config.Default.Tomochain.API, config.Default.Tomochain.RPC),
-		coin.Bsc().Handle:          ethereum.InitWithBlockbook(coin.BSCLegacy, config.Default.BSC.API, config.Default.BSC.RPC),
-		coin.Smartchain().Handle:   ethereum.InitWithBlockbook(coin.BSC, config.Default.Smartchain.API, config.Default.Smartchain.RPC),
-		coin.Ethereum().Handle:     ethereum.InitWithCollection(coin.ETH, config.Default.Ethereum.RPC, config.Default.Ethereum.BlockbookAPI, config.Default.Ethereum.CollectionsAPI, config.Default.Ethereum.CollectionsKey),
+		coin.Bsc().Handle:          ethereum.InitWithBounce(coin.BSCLegacy, config.Default.BSC.RPC, config.Default.BSC.API, config.Default.BSC.CollectionsAPI),
+		coin.Smartchain().Handle:   ethereum.InitWithBounce(coin.BSC, config.Default.Smartchain.RPC, config.Default.Smartchain.API, config.Default.Smartchain.CollectionsAPI),
+		coin.Ethereum().Handle:     ethereum.InitWithOpenSea(coin.ETH, config.Default.Ethereum.RPC, config.Default.Ethereum.BlockbookAPI, config.Default.Ethereum.CollectionsAPI, config.Default.Ethereum.CollectionsKey),
 		coin.Near().Handle:         near.Init(config.Default.Near.API),
 		coin.Elrond().Handle:       elrond.Init(coin.EGLD, config.Default.Elrond.API),
 		coin.Filecoin().Handle:     filecoin.Init(config.Default.Filecoin.API, config.Default.Filecoin.Explorer),
@@ -103,6 +103,7 @@ func getAllHandlers() blockatlas.Platforms {
 
 func getCollectionsHandlers() blockatlas.CollectionsAPIs {
 	return blockatlas.CollectionsAPIs{
-		coin.ETH: ethereum.InitWithCollection(coin.ETH, config.Default.Ethereum.RPC, config.Default.Ethereum.BlockbookAPI, config.Default.Ethereum.CollectionsAPI, config.Default.Ethereum.CollectionsKey),
+		coin.ETH: ethereum.InitWithOpenSea(coin.ETH, config.Default.Ethereum.RPC, config.Default.Ethereum.BlockbookAPI, config.Default.Ethereum.CollectionsAPI, config.Default.Ethereum.CollectionsKey),
+		coin.BSC: ethereum.InitWithBounce(coin.BSC, config.Default.Smartchain.RPC, config.Default.Smartchain.API, config.Default.Smartchain.CollectionsAPI),
 	}
 }


Test patch:
diff --git a/platform/ethereum/collection_test.go b/platform/ethereum/opensea/collection_test.go
similarity index 96%
rename from platform/ethereum/collection_test.go
rename to platform/ethereum/opensea/collection_test.go
index cb1c012..5524bbf 100644
--- a/platform/ethereum/collection_test.go
+++ b/platform/ethereum/opensea/collection_test.go
@@ -1,11 +1,10 @@
-package ethereum
+package opensea
 
 import (
 	"encoding/json"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"github.com/trustwallet/blockatlas/platform/ethereum/collection"
 	"github.com/trustwallet/golibs/coin"
 	"github.com/trustwallet/golibs/mock"
 	"github.com/trustwallet/golibs/types"
@@ -66,7 +65,7 @@ var (
 )
 
 func TestNormalizeCollectionV4(t *testing.T) {
-	var collections []collection.Collection
+	var collections []Collection
 	err := json.Unmarshal([]byte(collectionsSrcV4), &collections)
 	assert.Nil(t, err)
 	page := NormalizeCollections(collections, coin.ETH, collectionsOwnerV4)
@@ -76,7 +75,7 @@ func TestNormalizeCollectionV4(t *testing.T) {
 }
 
 func TestNormalizeCollectibleV4(t *testing.T) {
-	var collectibles []collection.Collectible
+	var collectibles []Collectible
 	err := json.Unmarshal([]byte(collectibleSrcV4), &collectibles)
 	assert.Nil(t, err)
 	page := NormalizeCollectiblePage(collectibles, coin.ETH)