chore: rework data structures to accomodate new providers

This commit is contained in:
Dario Gabriel Lipicar 2023-07-31 16:41:14 -03:00 committed by dlipicar
parent d535cd95f8
commit c2ac108556
13 changed files with 377 additions and 200 deletions

View File

@ -550,7 +550,7 @@ func (s *Service) CanProvideCollectibleMetadata(id thirdparty.CollectibleUniqueI
return ret, nil
}
func (s *Service) FetchCollectibleMetadata(id thirdparty.CollectibleUniqueID, tokenURI string) (*thirdparty.CollectibleData, error) {
func (s *Service) FetchCollectibleMetadata(id thirdparty.CollectibleUniqueID, tokenURI string) (*thirdparty.FullCollectibleData, error) {
if s.messenger == nil {
return nil, fmt.Errorf("messenger not ready")
}
@ -573,13 +573,17 @@ func (s *Service) FetchCollectibleMetadata(id thirdparty.CollectibleUniqueID, to
for _, tokenMetadata := range tokensMetadata {
contractAddresses := tokenMetadata.GetContractAddresses()
if contractAddresses[uint64(id.ChainID)] == id.ContractAddress.Hex() {
return &thirdparty.CollectibleData{
ID: id,
Name: tokenMetadata.GetName(),
Description: tokenMetadata.GetDescription(),
ImageURL: tokenMetadata.GetImage(),
CollectionData: thirdparty.CollectionData{
if contractAddresses[uint64(id.ContractID.ChainID)] == id.ContractID.Address.Hex() {
return &thirdparty.FullCollectibleData{
CollectibleData: thirdparty.CollectibleData{
ID: id,
Name: tokenMetadata.GetName(),
Description: tokenMetadata.GetDescription(),
ImageURL: tokenMetadata.GetImage(),
TokenURI: tokenURI,
},
CollectionData: &thirdparty.CollectionData{
ID: id.ContractID,
Name: tokenMetadata.GetName(),
ImageURL: tokenMetadata.GetImage(),
},

View File

@ -316,10 +316,10 @@ func (api *API) FilterOwnedCollectiblesAsync(ctx context.Context, chainIDs []wco
return nil
}
func (api *API) GetCollectiblesDataAsync(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) error {
func (api *API) GetCollectiblesDetailsAsync(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) error {
log.Debug("wallet.api.GetCollectiblesDetailsAsync")
api.s.collectibles.GetCollectiblesDataAsync(ctx, uniqueIDs)
api.s.collectibles.GetCollectiblesDetailsAsync(ctx, uniqueIDs)
return nil
}
@ -342,22 +342,22 @@ func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, chainI
return container.Assets, nil
}
func (api *API) GetCollectiblesByOwnerAndCollectionWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (api *API) GetCollectiblesByOwnerAndCollectionWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
log.Debug("call to GetCollectiblesByOwnerAndCollectionWithCursor")
return api.s.collectiblesManager.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit)
}
func (api *API) GetCollectiblesByOwnerWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (api *API) GetCollectiblesByOwnerWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
log.Debug("call to GetCollectiblesByOwnerWithCursor")
return api.s.collectiblesManager.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
}
func (api *API) GetCollectiblesByOwnerAndContractAddressWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (api *API) GetCollectiblesByOwnerAndContractAddressWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
log.Debug("call to GetCollectiblesByOwnerAndContractAddressWithCursor")
return api.s.collectiblesManager.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit)
}
func (api *API) GetCollectiblesByUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) {
func (api *API) GetCollectiblesByUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
log.Debug("call to GetCollectiblesByUniqueID")
return api.s.collectiblesManager.FetchAssetsByCollectibleUniqueID(uniqueIDs)
}

View File

@ -40,8 +40,10 @@ type Manager struct {
fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider
metadataProvider thirdparty.CollectibleMetadataProvider
opensea *opensea.Client
nftCache map[walletCommon.ChainID]map[string]thirdparty.CollectibleData
nftCacheLock sync.RWMutex
collectiblesDataCache map[string]thirdparty.CollectibleData
collectiblesDataCacheLock sync.RWMutex
collectionsDataCache map[string]thirdparty.CollectionData
collectionsDataCacheLock sync.RWMutex
}
func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, opensea *opensea.Client) *Manager {
@ -57,7 +59,8 @@ func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty.
mainContractOwnershipProvider: mainContractOwnershipProvider,
fallbackContractOwnershipProvider: fallbackContractOwnershipProvider,
opensea: opensea,
nftCache: make(map[walletCommon.ChainID]map[string]thirdparty.CollectibleData),
collectiblesDataCache: make(map[string]thirdparty.CollectibleData),
collectionsDataCache: make(map[string]thirdparty.CollectionData),
}
}
@ -103,13 +106,13 @@ func (o *Manager) FetchAllOpenseaAssetsByOwnerAndCollection(chainID walletCommon
return o.opensea.FetchAllOpenseaAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit)
}
func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit)
if err != nil {
return nil, err
}
err = o.processAssets(assetContainer.Collectibles)
err = o.processFullCollectibleData(assetContainer.Items)
if err != nil {
return nil, err
}
@ -127,7 +130,7 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID walletCommon.Ch
// Try with more direct endpoint first (OpenSea)
assetsContainer, err := o.FetchAllAssetsByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses, FetchFromStartCursor, FetchNoLimit)
if err == opensea.ErrChainIDNotSupported {
if err == thirdparty.ErrChainIDNotSupported {
// Use contract ownership providers
for _, contractAddress := range contractAddresses {
ownership, err := o.FetchCollectibleOwnersByContractAddress(chainID, contractAddress)
@ -143,10 +146,10 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID walletCommon.Ch
}
} else if err == nil {
// OpenSea could provide
for _, collectible := range assetsContainer.Collectibles {
contractAddress := collectible.ID.ContractAddress
for _, fullData := range assetsContainer.Items {
contractAddress := fullData.CollectibleData.ID.ContractID.Address
balance := thirdparty.TokenBalance{
TokenID: collectible.ID.TokenID,
TokenID: fullData.CollectibleData.ID.TokenID,
Balance: &bigint.BigInt{Int: big.NewInt(1)},
}
ret[contractAddress] = append(ret[contractAddress], balance)
@ -159,13 +162,13 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID walletCommon.Ch
return ret, nil
}
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit)
if err != nil {
return nil, err
}
err = o.processAssets(assetContainer.Collectibles)
err = o.processFullCollectibleData(assetContainer.Items)
if err != nil {
return nil, err
}
@ -173,13 +176,13 @@ func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.C
return assetContainer, nil
}
func (o *Manager) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (o *Manager) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
assetContainer, err := o.opensea.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
if err != nil {
return nil, err
}
err = o.processAssets(assetContainer.Collectibles)
err = o.processFullCollectibleData(assetContainer.Items)
if err != nil {
return nil, err
}
@ -188,17 +191,18 @@ func (o *Manager) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner comm
}
func (o *Manager) FetchCollectibleOwnershipByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleOwnershipContainer, error) {
// We don't yet have an API that will return only Ownership data
// Use the full Ownership + Metadata endpoint and use the data we need
assetContainer, err := o.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
if err != nil {
return nil, err
}
ret := assetContainer.ToOwnershipContainer()
return &ret, nil
}
func (o *Manager) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) {
func (o *Manager) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
idsToFetch := o.getIDsNotInCollectiblesDataCache(uniqueIDs)
if len(idsToFetch) > 0 {
fetchedAssets, err := o.opensea.FetchAssetsByCollectibleUniqueID(idsToFetch)
@ -206,14 +210,13 @@ func (o *Manager) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.Collec
return nil, err
}
err = o.processAssets(fetchedAssets)
err = o.processFullCollectibleData(fetchedAssets)
if err != nil {
return nil, err
}
}
return o.getCacheCollectiblesData(uniqueIDs), nil
return o.getCacheFullCollectibleData(uniqueIDs), nil
}
func (o *Manager) FetchCollectibleOwnersByContractAddress(chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
@ -242,12 +245,12 @@ func isMetadataEmpty(asset thirdparty.CollectibleData) bool {
}
func (o *Manager) fetchTokenURI(id thirdparty.CollectibleUniqueID) (string, error) {
backend, err := o.rpcClient.EthClient(uint64(id.ChainID))
backend, err := o.rpcClient.EthClient(uint64(id.ContractID.ChainID))
if err != nil {
return "", err
}
caller, err := collectibles.NewCollectiblesCaller(id.ContractAddress, backend)
caller, err := collectibles.NewCollectiblesCaller(id.ContractID.Address, backend)
if err != nil {
return "", err
}
@ -272,11 +275,11 @@ func (o *Manager) fetchTokenURI(id thirdparty.CollectibleUniqueID) (string, erro
return tokenURI, err
}
func (o *Manager) processAssets(assets []thirdparty.CollectibleData) error {
func (o *Manager) processFullCollectibleData(assets []thirdparty.FullCollectibleData) error {
for idx, asset := range assets {
id := asset.ID
id := asset.CollectibleData.ID
if isMetadataEmpty(asset) {
if isMetadataEmpty(asset.CollectibleData) {
if o.metadataProvider == nil {
return fmt.Errorf("CollectibleMetadataProvider not available")
}
@ -286,7 +289,7 @@ func (o *Manager) processAssets(assets []thirdparty.CollectibleData) error {
return err
}
assets[idx].TokenURI = tokenURI
assets[idx].CollectibleData.TokenURI = tokenURI
canProvide, err := o.metadataProvider.CanProvideCollectibleMetadata(id, tokenURI)
@ -306,21 +309,21 @@ func (o *Manager) processAssets(assets []thirdparty.CollectibleData) error {
}
}
o.setCacheCollectibleData(assets[idx])
o.setCacheCollectibleData(assets[idx].CollectibleData)
if assets[idx].CollectionData != nil {
o.setCacheCollectionData(*assets[idx].CollectionData)
}
}
return nil
}
func (o *Manager) isIDInCollectiblesDataCache(id thirdparty.CollectibleUniqueID) bool {
o.nftCacheLock.RLock()
defer o.nftCacheLock.RUnlock()
if _, ok := o.nftCache[id.ChainID]; ok {
if _, ok := o.nftCache[id.ChainID][id.HashKey()]; ok {
return true
}
o.collectiblesDataCacheLock.RLock()
defer o.collectiblesDataCacheLock.RUnlock()
if _, ok := o.collectiblesDataCache[id.HashKey()]; ok {
return true
}
return false
}
@ -335,35 +338,94 @@ func (o *Manager) getIDsNotInCollectiblesDataCache(uniqueIDs []thirdparty.Collec
return idsToFetch
}
func (o *Manager) getCacheCollectiblesData(uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.CollectibleData {
o.nftCacheLock.RLock()
defer o.nftCacheLock.RUnlock()
func (o *Manager) getCacheCollectiblesData(uniqueIDs []thirdparty.CollectibleUniqueID) map[string]*thirdparty.CollectibleData {
o.collectiblesDataCacheLock.RLock()
defer o.collectiblesDataCacheLock.RUnlock()
assets := make([]thirdparty.CollectibleData, 0, len(uniqueIDs))
collectibles := make(map[string]*thirdparty.CollectibleData)
for _, id := range uniqueIDs {
if _, ok := o.nftCache[id.ChainID]; ok {
if asset, ok := o.nftCache[id.ChainID][id.HashKey()]; ok {
assets = append(assets, asset)
continue
}
if collectible, ok := o.collectiblesDataCache[id.HashKey()]; ok {
collectibles[id.HashKey()] = &collectible
continue
}
emptyAsset := thirdparty.CollectibleData{
ID: id,
}
assets = append(assets, emptyAsset)
}
return assets
return collectibles
}
func (o *Manager) setCacheCollectibleData(data thirdparty.CollectibleData) {
o.nftCacheLock.Lock()
defer o.nftCacheLock.Unlock()
o.collectiblesDataCacheLock.Lock()
defer o.collectiblesDataCacheLock.Unlock()
id := data.ID
o.collectiblesDataCache[data.ID.HashKey()] = data
}
if _, ok := o.nftCache[id.ChainID]; !ok {
o.nftCache[id.ChainID] = make(map[string]thirdparty.CollectibleData)
func (o *Manager) isIDInContractDataCache(id thirdparty.ContractID) bool {
o.collectionsDataCacheLock.RLock()
defer o.collectionsDataCacheLock.RUnlock()
if _, ok := o.collectionsDataCache[id.HashKey()]; ok {
return true
}
return false
}
func (o *Manager) getIDsNotInContractDataCache(ids []thirdparty.ContractID) []thirdparty.ContractID {
idsToFetch := make([]thirdparty.ContractID, 0, len(ids))
for _, id := range ids {
if o.isIDInContractDataCache(id) {
continue
}
idsToFetch = append(idsToFetch, id)
}
return idsToFetch
}
func (o *Manager) getCacheCollectionData(ids []thirdparty.ContractID) map[string]*thirdparty.CollectionData {
o.collectionsDataCacheLock.RLock()
defer o.collectionsDataCacheLock.RUnlock()
collections := make(map[string]*thirdparty.CollectionData)
for _, id := range ids {
if collection, ok := o.collectionsDataCache[id.HashKey()]; ok {
collections[id.HashKey()] = &collection
continue
}
}
return collections
}
func (o *Manager) setCacheCollectionData(data thirdparty.CollectionData) {
o.collectionsDataCacheLock.Lock()
defer o.collectionsDataCacheLock.Unlock()
o.collectionsDataCache[data.ID.HashKey()] = data
}
func (o *Manager) getCacheFullCollectibleData(uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.FullCollectibleData {
ret := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
collectiblesData := o.getCacheCollectiblesData(uniqueIDs)
contractIDs := make([]thirdparty.ContractID, 0, len(uniqueIDs))
for _, id := range uniqueIDs {
contractIDs = append(contractIDs, id.ContractID)
}
o.nftCache[id.ChainID][id.HashKey()] = data
collectionsData := o.getCacheCollectionData(contractIDs)
for _, id := range uniqueIDs {
collectibleData := collectiblesData[id.HashKey()]
if collectibleData == nil {
// Use empty data, set only ID
collectibleData = &thirdparty.CollectibleData{
ID: id,
}
}
fullData := thirdparty.FullCollectibleData{
CollectibleData: *collectibleData,
CollectionData: collectionsData[id.ContractID.HashKey()],
}
ret = append(ret, fullData)
}
return ret
}

View File

@ -154,9 +154,9 @@ func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
break
}
log.Debug("partial loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "found", len(partialOwnership.Collectibles), "collectibles")
log.Debug("partial loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "found", len(partialOwnership.Items), "collectibles")
c.partialOwnership = append(c.partialOwnership, partialOwnership.Collectibles...)
c.partialOwnership = append(c.partialOwnership, partialOwnership.Items...)
pageNr++
cursor = partialOwnership.NextCursor

View File

@ -54,7 +54,7 @@ func insertAddressOwnership(creator statementCreator, ownerAddress common.Addres
}
for _, c := range collectibles {
_, err = insertOwnership.Exec(c.ChainID, c.ContractAddress, (*bigint.SQLBigIntBytes)(c.TokenID.Int), ownerAddress)
_, err = insertOwnership.Exec(c.ContractID.ChainID, c.ContractID.Address, (*bigint.SQLBigIntBytes)(c.TokenID.Int), ownerAddress)
if err != nil {
return err
}
@ -101,8 +101,8 @@ func rowsToCollectibles(rows *sql.Rows) ([]thirdparty.CollectibleUniqueID, error
TokenID: &bigint.BigInt{Int: big.NewInt(0)},
}
err := rows.Scan(
&id.ChainID,
&id.ContractAddress,
&id.ContractID.ChainID,
&id.ContractID.Address,
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
)
if err != nil {

View File

@ -27,9 +27,11 @@ func generateTestCollectibles(chainID w_common.ChainID, count int) (result []thi
for i := 0; i < count; i++ {
bigI := big.NewInt(int64(i))
newCollectible := thirdparty.CollectibleUniqueID{
ChainID: chainID,
ContractAddress: common.BigToAddress(bigI),
TokenID: &bigint.BigInt{Int: bigI},
ContractID: thirdparty.ContractID{
ChainID: chainID,
Address: common.BigToAddress(bigI),
},
TokenID: &bigint.BigInt{Int: bigI},
}
result = append(result, newCollectible)
}
@ -98,9 +100,11 @@ func TestLargeTokenID(t *testing.T) {
ownedListChain := []thirdparty.CollectibleUniqueID{
{
ChainID: chainID,
ContractAddress: common.HexToAddress("0x1234"),
TokenID: &bigint.BigInt{Int: big.NewInt(0).SetBytes([]byte("0x1234567890123456789012345678901234567890"))},
ContractID: thirdparty.ContractID{
ChainID: chainID,
Address: common.HexToAddress("0x1234"),
},
TokenID: &bigint.BigInt{Int: big.NewInt(0).SetBytes([]byte("0x1234567890123456789012345678901234567890"))},
},
}

View File

@ -28,7 +28,7 @@ const (
EventCollectiblesOwnershipUpdateFinishedWithError walletevent.EventType = "wallet-collectibles-ownership-update-finished-with-error"
EventOwnedCollectiblesFilteringDone walletevent.EventType = "wallet-owned-collectibles-filtering-done"
EventGetCollectiblesDataDone walletevent.EventType = "wallet-get-collectibles-data-done"
EventGetCollectiblesDetailsDone walletevent.EventType = "wallet-get-collectibles-details-done"
)
var (
@ -78,21 +78,21 @@ const (
)
type FilterOwnedCollectiblesResponse struct {
Collectibles []thirdparty.CollectibleHeader `json:"collectibles"`
Offset int `json:"offset"`
Collectibles []CollectibleHeader `json:"collectibles"`
Offset int `json:"offset"`
// Used to indicate that there might be more collectibles that were not returned
// based on a simple heuristic
HasMore bool `json:"hasMore"`
ErrorCode ErrorCode `json:"errorCode"`
}
type GetCollectiblesDataResponse struct {
Collectibles []thirdparty.CollectibleData `json:"collectibles"`
ErrorCode ErrorCode `json:"errorCode"`
type GetCollectiblesDetailsResponse struct {
Collectibles []CollectibleDetails `json:"collectibles"`
ErrorCode ErrorCode `json:"errorCode"`
}
type filterOwnedCollectiblesTaskReturnType struct {
collectibles []thirdparty.CollectibleHeader
collectibles []CollectibleHeader
hasMore bool
}
@ -111,7 +111,7 @@ func (s *Service) FilterOwnedCollectiblesAsync(ctx context.Context, chainIDs []w
}
return filterOwnedCollectiblesTaskReturnType{
collectibles: thirdparty.CollectiblesToHeaders(data),
collectibles: fullCollectiblesDataToHeaders(data),
hasMore: hasMore,
}, err
}, func(result interface{}, taskType async.TaskType, err error) {
@ -133,24 +133,24 @@ func (s *Service) FilterOwnedCollectiblesAsync(ctx context.Context, chainIDs []w
})
}
func (s *Service) GetCollectiblesDataAsync(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) {
func (s *Service) GetCollectiblesDetailsAsync(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) {
s.scheduler.Enqueue(getCollectiblesDataTask, func(ctx context.Context) (interface{}, error) {
collectibles, err := s.manager.FetchAssetsByCollectibleUniqueID(uniqueIDs)
return collectibles, err
}, func(result interface{}, taskType async.TaskType, err error) {
res := GetCollectiblesDataResponse{
res := GetCollectiblesDetailsResponse{
ErrorCode: ErrorCodeFailed,
}
if errors.Is(err, context.Canceled) || errors.Is(err, async.ErrTaskOverwritten) {
res.ErrorCode = ErrorCodeTaskCanceled
} else if err == nil {
collectibles := result.([]thirdparty.CollectibleData)
res.Collectibles = collectibles
collectibles := result.([]thirdparty.FullCollectibleData)
res.Collectibles = fullCollectiblesDataToDetails(collectibles)
res.ErrorCode = ErrorCodeSuccess
}
s.sendResponseEvent(EventGetCollectiblesDataDone, res, err)
s.sendResponseEvent(EventGetCollectiblesDetailsDone, res, err)
})
}

View File

@ -0,0 +1,83 @@
package collectibles
import "github.com/status-im/status-go/services/wallet/thirdparty"
// Combined Collection+Collectible info, used to display a detailed view of a collectible
type CollectibleDetails struct {
ID thirdparty.CollectibleUniqueID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ImageURL string `json:"image_url"`
AnimationURL string `json:"animation_url"`
AnimationMediaType string `json:"animation_media_type"`
Traits []thirdparty.CollectibleTrait `json:"traits"`
BackgroundColor string `json:"background_color"`
CollectionName string `json:"collection_name"`
CollectionSlug string `json:"collection_slug"`
CollectionImageURL string `json:"collection_image_url"`
}
// Combined Collection+Collectible info, used to display a basic view of a collectible in a list
type CollectibleHeader struct {
ID thirdparty.CollectibleUniqueID `json:"id"`
Name string `json:"name"`
ImageURL string `json:"image_url"`
AnimationURL string `json:"animation_url"`
AnimationMediaType string `json:"animation_media_type"`
BackgroundColor string `json:"background_color"`
CollectionName string `json:"collection_name"`
}
func fullCollectibleDataToHeader(c thirdparty.FullCollectibleData) CollectibleHeader {
ret := CollectibleHeader{
ID: c.CollectibleData.ID,
Name: c.CollectibleData.Name,
ImageURL: c.CollectibleData.ImageURL,
AnimationURL: c.CollectibleData.AnimationURL,
AnimationMediaType: c.CollectibleData.AnimationMediaType,
BackgroundColor: c.CollectibleData.BackgroundColor,
}
if c.CollectionData != nil {
ret.CollectionName = c.CollectionData.Name
}
return ret
}
func fullCollectiblesDataToHeaders(data []thirdparty.FullCollectibleData) []CollectibleHeader {
res := make([]CollectibleHeader, 0, len(data))
for _, c := range data {
res = append(res, fullCollectibleDataToHeader(c))
}
return res
}
func fullCollectibleDataToDetails(c thirdparty.FullCollectibleData) CollectibleDetails {
ret := CollectibleDetails{
ID: c.CollectibleData.ID,
Name: c.CollectibleData.Name,
Description: c.CollectibleData.Description,
ImageURL: c.CollectibleData.ImageURL,
AnimationURL: c.CollectibleData.AnimationURL,
AnimationMediaType: c.CollectibleData.AnimationMediaType,
BackgroundColor: c.CollectibleData.BackgroundColor,
Traits: c.CollectibleData.Traits,
}
if c.CollectionData != nil {
ret.CollectionName = c.CollectionData.Name
ret.CollectionSlug = c.CollectionData.Slug
ret.CollectionImageURL = c.CollectionData.ImageURL
}
return ret
}
func fullCollectiblesDataToDetails(data []thirdparty.FullCollectibleData) []CollectibleDetails {
res := make([]CollectibleDetails, 0, len(data))
for _, c := range data {
res = append(res, fullCollectibleDataToDetails(c))
}
return res
}

View File

@ -33,7 +33,16 @@ func getBaseURL(chainID walletCommon.ChainID) (string, error) {
return "https://arb-goerli.g.alchemy.com", nil
}
return "", fmt.Errorf("chainID not supported: %d", chainID)
return "", thirdparty.ErrChainIDNotSupported
}
func (o *Client) ID() string {
return "alchemy"
}
func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
_, err := getBaseURL(chainID)
return err == nil
}
func getAPIKeySubpath(apiKey string) string {
@ -93,11 +102,6 @@ func (o *Client) doQuery(url string) (*http.Response, error) {
return resp, nil
}
func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
_, err := getBaseURL(chainID)
return err == nil
}
func alchemyOwnershipToCommon(contractAddress common.Address, alchemyOwnership CollectibleContractOwnership) (*thirdparty.CollectibleContractOwnership, error) {
owners := make([]thirdparty.CollectibleOwner, 0, len(alchemyOwnership.Owners))
for _, alchemyOwner := range alchemyOwnership.Owners {

View File

@ -1,6 +1,7 @@
package thirdparty
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
@ -8,24 +9,41 @@ import (
w_common "github.com/status-im/status-go/services/wallet/common"
)
var (
ErrChainIDNotSupported = errors.New("chainID not supported")
)
type CollectibleProvider interface {
ID() string
IsChainSupported(chainID w_common.ChainID) bool
}
type ContractID struct {
ChainID w_common.ChainID `json:"chainID"`
Address common.Address `json:"address"`
}
func (k *ContractID) HashKey() string {
return fmt.Sprintf("%d+%s", k.ChainID, k.Address.String())
}
type CollectibleUniqueID struct {
ChainID w_common.ChainID `json:"chainID"`
ContractAddress common.Address `json:"contractAddress"`
TokenID *bigint.BigInt `json:"tokenID"`
ContractID ContractID `json:"contractID"`
TokenID *bigint.BigInt `json:"tokenID"`
}
func (k *CollectibleUniqueID) HashKey() string {
return fmt.Sprintf("%d+%s+%s", k.ChainID, k.ContractAddress.String(), k.TokenID.String())
return fmt.Sprintf("%s+%s", k.ContractID.HashKey(), k.TokenID.String())
}
func GroupCollectibleUIDsByChainID(uids []CollectibleUniqueID) map[w_common.ChainID][]CollectibleUniqueID {
ret := make(map[w_common.ChainID][]CollectibleUniqueID)
for _, uid := range uids {
if _, ok := ret[uid.ChainID]; !ok {
ret[uid.ChainID] = make([]CollectibleUniqueID, 0, len(uids))
if _, ok := ret[uid.ContractID.ChainID]; !ok {
ret[uid.ContractID.ChainID] = make([]CollectibleUniqueID, 0, len(uids))
}
ret[uid.ChainID] = append(ret[uid.ChainID], uid)
ret[uid.ContractID.ChainID] = append(ret[uid.ContractID.ChainID], uid)
}
return ret
@ -36,7 +54,9 @@ type CollectionTrait struct {
Max float64 `json:"max"`
}
// Collection info
type CollectionData struct {
ID ContractID `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
ImageURL string `json:"image_url"`
@ -50,6 +70,7 @@ type CollectibleTrait struct {
MaxValue string `json:"max_value"`
}
// Collectible info
type CollectibleData struct {
ID CollectibleUniqueID `json:"id"`
Name string `json:"name"`
@ -61,75 +82,46 @@ type CollectibleData struct {
Traits []CollectibleTrait `json:"traits"`
BackgroundColor string `json:"background_color"`
TokenURI string `json:"token_uri"`
CollectionData CollectionData `json:"collection_data"`
}
type CollectibleHeader struct {
ID CollectibleUniqueID `json:"id"`
Name string `json:"name"`
ImageURL string `json:"image_url"`
AnimationURL string `json:"animation_url"`
AnimationMediaType string `json:"animation_media_type"`
BackgroundColor string `json:"background_color"`
CollectionName string `json:"collection_name"`
// Combined Collection+Collectible info returned by the CollectibleProvider
// Some providers may not return the CollectionData in the same API call, so it's optional
type FullCollectibleData struct {
CollectibleData CollectibleData
CollectionData *CollectionData
}
type CollectibleOwnershipContainer struct {
Collectibles []CollectibleUniqueID
type CollectiblesContainer[T any] struct {
Items []T
NextCursor string
PreviousCursor string
}
type CollectibleDataContainer struct {
Collectibles []CollectibleData
NextCursor string
PreviousCursor string
}
type CollectibleOwnershipContainer CollectiblesContainer[CollectibleUniqueID]
type CollectionDataContainer CollectiblesContainer[CollectionData]
type CollectibleDataContainer CollectiblesContainer[CollectibleData]
type FullCollectibleDataContainer CollectiblesContainer[FullCollectibleData]
func (c *CollectibleDataContainer) ToOwnershipContainer() CollectibleOwnershipContainer {
ret := CollectibleOwnershipContainer{
Collectibles: make([]CollectibleUniqueID, 0, len(c.Collectibles)),
NextCursor: c.NextCursor,
PreviousCursor: c.PreviousCursor,
// Tried to find a way to make this generic, but couldn't, so the code below is duplicated somewhere else
func collectibleItemsToIDs(items []FullCollectibleData) []CollectibleUniqueID {
ret := make([]CollectibleUniqueID, 0, len(items))
for _, item := range items {
ret = append(ret, item.CollectibleData.ID)
}
for _, collectible := range c.Collectibles {
ret.Collectibles = append(ret.Collectibles, collectible.ID)
}
return ret
}
func (c *CollectibleData) toHeader() CollectibleHeader {
return CollectibleHeader{
ID: c.ID,
Name: c.Name,
ImageURL: c.ImageURL,
AnimationURL: c.AnimationURL,
AnimationMediaType: c.AnimationMediaType,
BackgroundColor: c.BackgroundColor,
CollectionName: c.CollectionData.Name,
func (c *FullCollectibleDataContainer) ToOwnershipContainer() CollectibleOwnershipContainer {
return CollectibleOwnershipContainer{
Items: collectibleItemsToIDs(c.Items),
NextCursor: c.NextCursor,
PreviousCursor: c.PreviousCursor,
}
}
func CollectiblesToHeaders(collectibles []CollectibleData) []CollectibleHeader {
res := make([]CollectibleHeader, 0, len(collectibles))
for _, c := range collectibles {
res = append(res, c.toHeader())
}
return res
}
type CollectibleOwnershipProvider interface {
CanProvideAccountOwnership(chainID uint64) (bool, error)
FetchAccountOwnership(chainID uint64, address common.Address) (*CollectibleData, error)
}
type CollectibleMetadataProvider interface {
CanProvideCollectibleMetadata(id CollectibleUniqueID, tokenURI string) (bool, error)
FetchCollectibleMetadata(id CollectibleUniqueID, tokenURI string) (*CollectibleData, error)
FetchCollectibleMetadata(id CollectibleUniqueID, tokenURI string) (*FullCollectibleData, error)
}
type TokenBalance struct {

View File

@ -63,6 +63,10 @@ func (o *Client) doQuery(url string) (*http.Response, error) {
return resp, nil
}
func (o *Client) ID() string {
return "infura"
}
func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
switch uint64(chainID) {
case walletCommon.EthereumMainnet, walletCommon.EthereumGoerli, walletCommon.EthereumSepolia, walletCommon.ArbitrumMainnet:

View File

@ -2,7 +2,6 @@ package opensea
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@ -41,13 +40,9 @@ const ChainIDRequiringAPIKey = walletCommon.EthereumMainnet
const FetchNoLimit = 0
var (
ErrChainIDNotSupported = errors.New("chainID not supported by opensea API")
)
type urlGetter func(walletCommon.ChainID, string) (string, error)
func getbaseURL(chainID walletCommon.ChainID) (string, error) {
func getBaseURL(chainID walletCommon.ChainID) (string, error) {
// v1 Endpoints only support L1 chain
switch uint64(chainID) {
case walletCommon.EthereumMainnet:
@ -56,11 +51,20 @@ func getbaseURL(chainID walletCommon.ChainID) (string, error) {
return "https://testnets-api.opensea.io/api/v1", nil
}
return "", ErrChainIDNotSupported
return "", thirdparty.ErrChainIDNotSupported
}
func (o *Client) ID() string {
return "opensea"
}
func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
_, err := getBaseURL(chainID)
return err == nil
}
func getURL(chainID walletCommon.ChainID, path string) (string, error) {
baseURL, err := getbaseURL(chainID)
baseURL, err := getBaseURL(chainID)
if err != nil {
return "", err
}
@ -182,9 +186,11 @@ type OwnedCollection struct {
func (c *Asset) id() thirdparty.CollectibleUniqueID {
return thirdparty.CollectibleUniqueID{
ChainID: chainStringToChainID(c.Contract.ChainIdentifier),
ContractAddress: common.HexToAddress(c.Contract.Address),
TokenID: c.TokenID,
ContractID: thirdparty.ContractID{
ChainID: chainStringToChainID(c.Contract.ChainIdentifier),
Address: common.HexToAddress(c.Contract.Address),
},
TokenID: c.TokenID,
}
}
@ -204,14 +210,15 @@ func openseaToCollectibleTraits(traits []Trait) []thirdparty.CollectibleTrait {
return ret
}
func (c *Collection) toCommon() thirdparty.CollectionData {
func (c *Asset) toCollectionData() thirdparty.CollectionData {
ret := thirdparty.CollectionData{
Name: c.Name,
Slug: c.Slug,
ImageURL: c.ImageURL,
ID: c.id().ContractID,
Name: c.Collection.Name,
Slug: c.Collection.Slug,
ImageURL: c.Collection.ImageURL,
Traits: make(map[string]thirdparty.CollectionTrait),
}
for traitType, trait := range c.Traits {
for traitType, trait := range c.Collection.Traits {
ret.Traits[traitType] = thirdparty.CollectionTrait{
Min: trait.Min,
Max: trait.Max,
@ -220,7 +227,7 @@ func (c *Collection) toCommon() thirdparty.CollectionData {
return ret
}
func (c *Asset) toCommon() thirdparty.CollectibleData {
func (c *Asset) toCollectiblesData() thirdparty.CollectibleData {
return thirdparty.CollectibleData{
ID: c.id(),
Name: c.Name,
@ -232,7 +239,14 @@ func (c *Asset) toCommon() thirdparty.CollectibleData {
Traits: openseaToCollectibleTraits(c.Traits),
BackgroundColor: c.BackgroundColor,
TokenURI: c.TokenURI,
CollectionData: c.Collection.toCommon(),
}
}
func (c *Asset) toCommon() thirdparty.FullCollectibleData {
collection := c.toCollectionData()
return thirdparty.FullCollectibleData{
CollectibleData: c.toCollectiblesData(),
CollectionData: &collection,
}
}
@ -386,7 +400,7 @@ func (o *Client) FetchAllCollectionsByOwner(chainID walletCommon.ChainID, owner
return collections, nil
}
func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
queryParams := url.Values{
"owner": {owner.String()},
"collection": {collectionSlug},
@ -399,7 +413,7 @@ func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID
return o.fetchAssets(chainID, queryParams, limit)
}
func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
queryParams := url.Values{
"owner": {owner.String()},
}
@ -415,7 +429,7 @@ func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.Ch
return o.fetchAssets(chainID, queryParams, limit)
}
func (o *Client) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
func (o *Client) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
queryParams := url.Values{
"owner": {owner.String()},
}
@ -427,16 +441,16 @@ func (o *Client) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner commo
return o.fetchAssets(chainID, queryParams, limit)
}
func (o *Client) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) {
func (o *Client) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
queryParams := url.Values{}
ret := make([]thirdparty.CollectibleData, 0, len(uniqueIDs))
ret := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
idsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(uniqueIDs)
for chainID, ids := range idsPerChainID {
for _, id := range ids {
queryParams.Add("token_ids", id.TokenID.String())
queryParams.Add("asset_contract_addresses", id.ContractAddress.String())
queryParams.Add("asset_contract_addresses", id.ContractID.Address.String())
}
data, err := o.fetchAssets(chainID, queryParams, FetchNoLimit)
@ -444,14 +458,14 @@ func (o *Client) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.Collect
return nil, err
}
ret = append(ret, data.Collectibles...)
ret = append(ret, data.Items...)
}
return ret, nil
}
func (o *Client) fetchAssets(chainID walletCommon.ChainID, queryParams url.Values, limit int) (*thirdparty.CollectibleDataContainer, error) {
assets := new(thirdparty.CollectibleDataContainer)
func (o *Client) fetchAssets(chainID walletCommon.ChainID, queryParams url.Values, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
assets := new(thirdparty.FullCollectibleDataContainer)
if len(queryParams["cursor"]) > 0 {
assets.PreviousCursor = queryParams["cursor"][0]
@ -495,7 +509,7 @@ func (o *Client) fetchAssets(chainID walletCommon.ChainID, queryParams url.Value
asset.AnimationURL = ""
}
}
assets.Collectibles = append(assets.Collectibles, asset.toCommon())
assets.Items = append(assets.Items, asset.toCommon())
}
assets.NextCursor = container.NextCursor
@ -505,7 +519,7 @@ func (o *Client) fetchAssets(chainID walletCommon.ChainID, queryParams url.Value
queryParams["cursor"] = []string{assets.NextCursor}
if limit > FetchNoLimit && len(assets.Collectibles) >= limit {
if limit > FetchNoLimit && len(assets.Items) >= limit {
break
}
}
@ -539,7 +553,7 @@ func (o *Client) fetchOpenseaAssets(chainID walletCommon.ChainID, queryParams ur
tmpLimit = limit
}
baseURL, err := getbaseURL(chainID)
baseURL, err := getBaseURL(chainID)
if err != nil {
return nil, err

View File

@ -106,23 +106,33 @@ func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) {
NextCursor: "",
PreviousCursor: "",
}
expectedCommon := thirdparty.CollectibleDataContainer{
Collectibles: []thirdparty.CollectibleData{{
ID: thirdparty.CollectibleUniqueID{
ChainID: 1,
ContractAddress: common.HexToAddress("0x1"),
TokenID: &bigint.BigInt{Int: big.NewInt(1)},
expectedCommon := thirdparty.FullCollectibleDataContainer{
Items: []thirdparty.FullCollectibleData{
thirdparty.FullCollectibleData{
CollectibleData: thirdparty.CollectibleData{
ID: thirdparty.CollectibleUniqueID{
ContractID: thirdparty.ContractID{
ChainID: 1,
Address: common.HexToAddress("0x1"),
},
TokenID: &bigint.BigInt{Int: big.NewInt(1)},
},
Name: "Rocky",
Description: "Rocky Balboa",
Permalink: "permalink",
ImageURL: "ImageUrl",
Traits: []thirdparty.CollectibleTrait{},
},
CollectionData: &thirdparty.CollectionData{
ID: thirdparty.ContractID{
ChainID: 1,
Address: common.HexToAddress("0x1"),
},
Name: "Rocky",
Traits: map[string]thirdparty.CollectionTrait{},
},
},
Name: "Rocky",
Description: "Rocky Balboa",
Permalink: "permalink",
ImageURL: "ImageUrl",
Traits: []thirdparty.CollectibleTrait{},
CollectionData: thirdparty.CollectionData{
Name: "Rocky",
Traits: map[string]thirdparty.CollectionTrait{},
},
}},
},
NextCursor: "",
PreviousCursor: "",
}