2023-03-21 13:52:14 +00:00
package collectibles
import (
"context"
2023-08-07 22:30:18 +00:00
"database/sql"
2023-12-13 12:19:25 +00:00
"encoding/json"
2023-08-01 23:17:59 +00:00
"errors"
2023-07-13 17:26:17 +00:00
"math/big"
2023-07-31 23:34:53 +00:00
"net/http"
2023-03-21 13:52:14 +00:00
"strings"
"time"
2023-04-17 11:42:01 +00:00
"github.com/afex/hystrix-go/hystrix"
2023-03-21 13:52:14 +00:00
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
2023-09-22 13:18:42 +00:00
"github.com/ethereum/go-ethereum/event"
2023-07-31 23:34:53 +00:00
"github.com/ethereum/go-ethereum/log"
2023-07-18 08:33:45 +00:00
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
2023-03-21 13:52:14 +00:00
"github.com/status-im/status-go/rpc"
2023-12-15 20:29:39 +00:00
"github.com/status-im/status-go/server"
2023-12-13 12:19:25 +00:00
"github.com/status-im/status-go/services/wallet/async"
2023-07-13 17:26:17 +00:00
"github.com/status-im/status-go/services/wallet/bigint"
2023-07-18 15:01:53 +00:00
walletCommon "github.com/status-im/status-go/services/wallet/common"
2023-10-26 06:30:18 +00:00
"github.com/status-im/status-go/services/wallet/community"
2023-09-22 13:18:42 +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-09-22 13:18:42 +00:00
"github.com/status-im/status-go/services/wallet/walletevent"
2023-03-21 13:52:14 +00:00
)
const requestTimeout = 5 * time . Second
2023-12-13 12:19:25 +00:00
const signalUpdatedCollectiblesDataPageSize = 10
2023-03-21 13:52:14 +00:00
2023-04-17 11:42:01 +00:00
const hystrixContractOwnershipClientName = "contractOwnershipClient"
2023-09-22 13:18:42 +00:00
const EventCollectiblesConnectionStatusChanged walletevent . EventType = "wallet-collectible-status-changed"
2023-06-06 17:49:36 +00:00
// ERC721 does not support function "TokenURI" if call
// returns error starting with one of these strings
var noTokenURIErrorPrefixes = [ ] string {
"execution reverted" ,
"abi: attempting to unmarshall" ,
}
2023-08-01 23:17:59 +00:00
var (
2023-09-22 13:18:42 +00:00
ErrAllProvidersFailedForChainID = errors . New ( "all providers failed for chainID" )
2023-08-01 23:17:59 +00:00
ErrNoProvidersAvailableForChainID = errors . New ( "no providers available for chainID" )
)
2023-08-11 17:28:46 +00:00
type ManagerInterface interface {
2023-12-13 12:19:25 +00:00
FetchAssetsByCollectibleUniqueID ( ctx context . Context , uniqueIDs [ ] thirdparty . CollectibleUniqueID , asyncFetch bool ) ( [ ] thirdparty . FullCollectibleData , error )
2023-08-11 17:28:46 +00:00
}
2023-03-21 13:52:14 +00:00
type Manager struct {
2023-08-01 23:17:59 +00:00
rpcClient * rpc . Client
contractOwnershipProviders [ ] thirdparty . CollectibleContractOwnershipProvider
accountOwnershipProviders [ ] thirdparty . CollectibleAccountOwnershipProvider
2023-08-03 12:24:23 +00:00
collectibleDataProviders [ ] thirdparty . CollectibleDataProvider
2023-08-16 13:01:57 +00:00
collectionDataProviders [ ] thirdparty . CollectionDataProvider
2023-09-22 13:18:42 +00:00
collectibleProviders [ ] thirdparty . CollectibleProvider
httpClient * http . Client
collectiblesDataDB * CollectibleDataDB
collectionsDataDB * CollectionDataDB
2023-12-14 16:50:46 +00:00
communityManager * community . Manager
2023-12-13 12:19:25 +00:00
ownershipDB * OwnershipDB
2023-09-22 13:18:42 +00:00
2023-12-15 20:29:39 +00:00
mediaServer * server . MediaServer
2023-09-22 13:18:42 +00:00
statuses map [ string ] * connection . Status
statusNotifier * connection . StatusNotifier
2023-12-13 12:19:25 +00:00
feed * event . Feed
2023-03-21 13:52:14 +00:00
}
2023-09-21 12:40:58 +00:00
func NewManager (
db * sql . DB ,
rpcClient * rpc . Client ,
2023-12-14 16:50:46 +00:00
communityManager * community . Manager ,
2023-09-21 12:40:58 +00:00
contractOwnershipProviders [ ] thirdparty . CollectibleContractOwnershipProvider ,
accountOwnershipProviders [ ] thirdparty . CollectibleAccountOwnershipProvider ,
collectibleDataProviders [ ] thirdparty . CollectibleDataProvider ,
collectionDataProviders [ ] thirdparty . CollectionDataProvider ,
2023-12-15 20:29:39 +00:00
mediaServer * server . MediaServer ,
2023-09-22 13:18:42 +00:00
feed * event . Feed ) * Manager {
2023-04-17 11:42:01 +00:00
hystrix . ConfigureCommand ( hystrixContractOwnershipClientName , hystrix . CommandConfig {
Timeout : 10000 ,
MaxConcurrentRequests : 100 ,
SleepWindow : 300000 ,
ErrorPercentThreshold : 25 ,
} )
2023-09-22 13:18:42 +00:00
ownershipDB := NewOwnershipDB ( db )
statuses := make ( map [ string ] * connection . Status )
allChainIDs := walletCommon . AllChainIDs ( )
for _ , chainID := range allChainIDs {
status := connection . NewStatus ( )
state := status . GetState ( )
latestUpdateTimestamp , err := ownershipDB . GetLatestOwnershipUpdateTimestamp ( chainID )
if err == nil {
state . LastSuccessAt = latestUpdateTimestamp
status . SetState ( state )
}
statuses [ chainID . String ( ) ] = status
}
statusNotifier := connection . NewStatusNotifier (
statuses ,
EventCollectiblesConnectionStatusChanged ,
feed ,
)
// Get list of all providers
collectibleProvidersMap := make ( map [ string ] thirdparty . CollectibleProvider )
collectibleProviders := make ( [ ] thirdparty . CollectibleProvider , 0 )
for _ , provider := range contractOwnershipProviders {
collectibleProvidersMap [ provider . ID ( ) ] = provider
}
for _ , provider := range accountOwnershipProviders {
collectibleProvidersMap [ provider . ID ( ) ] = provider
}
for _ , provider := range collectibleDataProviders {
collectibleProvidersMap [ provider . ID ( ) ] = provider
}
for _ , provider := range collectionDataProviders {
collectibleProvidersMap [ provider . ID ( ) ] = provider
}
for _ , provider := range collectibleProvidersMap {
collectibleProviders = append ( collectibleProviders , provider )
}
2023-03-21 13:52:14 +00:00
return & Manager {
2023-08-01 23:17:59 +00:00
rpcClient : rpcClient ,
contractOwnershipProviders : contractOwnershipProviders ,
accountOwnershipProviders : accountOwnershipProviders ,
2023-08-03 12:24:23 +00:00
collectibleDataProviders : collectibleDataProviders ,
2023-08-16 13:01:57 +00:00
collectionDataProviders : collectionDataProviders ,
2023-09-22 13:18:42 +00:00
collectibleProviders : collectibleProviders ,
2023-07-31 23:34:53 +00:00
httpClient : & http . Client {
Timeout : requestTimeout ,
} ,
2023-08-07 22:30:18 +00:00
collectiblesDataDB : NewCollectibleDataDB ( db ) ,
collectionsDataDB : NewCollectionDataDB ( db ) ,
2023-12-14 16:50:46 +00:00
communityManager : communityManager ,
2023-12-13 12:19:25 +00:00
ownershipDB : ownershipDB ,
2023-12-15 20:29:39 +00:00
mediaServer : mediaServer ,
2023-09-22 13:18:42 +00:00
statuses : statuses ,
statusNotifier : statusNotifier ,
2023-12-13 12:19:25 +00:00
feed : feed ,
2023-04-17 11:42:01 +00:00
}
}
2023-08-07 22:30:18 +00:00
func mapToList [ K comparable , T any ] ( m map [ K ] T ) [ ] T {
2023-08-03 12:24:23 +00:00
list := make ( [ ] T , 0 , len ( m ) )
for _ , v := range m {
2023-08-07 22:30:18 +00:00
list = append ( list , v )
2023-08-03 12:24:23 +00:00
}
return list
}
2023-04-17 11:42:01 +00:00
func makeContractOwnershipCall ( main func ( ) ( any , error ) , fallback func ( ) ( any , error ) ) ( any , error ) {
resultChan := make ( chan any , 1 )
errChan := hystrix . Go ( hystrixContractOwnershipClientName , func ( ) error {
res , err := main ( )
if err != nil {
return err
}
resultChan <- res
return nil
} , func ( err error ) error {
if fallback == nil {
return err
}
res , err := fallback ( )
if err != nil {
return err
}
resultChan <- res
return nil
} )
select {
case result := <- resultChan :
return result , nil
case err := <- errChan :
return nil , err
2023-03-21 13:52:14 +00:00
}
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) doContentTypeRequest ( ctx context . Context , url string ) ( string , error ) {
req , err := http . NewRequestWithContext ( ctx , http . MethodHead , url , nil )
2023-07-31 23:34:53 +00:00
if err != nil {
return "" , err
}
resp , err := o . httpClient . Do ( req )
if err != nil {
return "" , err
}
defer func ( ) {
if err := resp . Body . Close ( ) ; err != nil {
log . Error ( "failed to close head request body" , "err" , err )
}
} ( )
return resp . Header . Get ( "Content-Type" ) , nil
}
2023-07-13 17:26:17 +00:00
// Need to combine different providers to support all needed ChainIDs
2023-11-14 17:16:39 +00:00
func ( o * Manager ) FetchBalancesByOwnerAndContractAddress ( ctx context . Context , chainID walletCommon . ChainID , ownerAddress common . Address , contractAddresses [ ] common . Address ) ( thirdparty . TokenBalancesPerContractAddress , error ) {
2023-07-13 17:26:17 +00:00
ret := make ( thirdparty . TokenBalancesPerContractAddress )
for _ , contractAddress := range contractAddresses {
ret [ contractAddress ] = make ( [ ] thirdparty . TokenBalance , 0 )
}
2023-08-01 23:17:59 +00:00
// Try with account ownership providers first
2023-11-14 17:16:39 +00:00
assetsContainer , err := o . FetchAllAssetsByOwnerAndContractAddress ( ctx , chainID , ownerAddress , contractAddresses , thirdparty . FetchFromStartCursor , thirdparty . FetchNoLimit , thirdparty . FetchFromAnyProvider )
2023-08-01 23:17:59 +00:00
if err == ErrNoProvidersAvailableForChainID {
2023-07-13 17:26:17 +00:00
// Use contract ownership providers
for _ , contractAddress := range contractAddresses {
2023-11-14 17:16:39 +00:00
ownership , err := o . FetchCollectibleOwnersByContractAddress ( ctx , chainID , contractAddress )
2023-07-13 17:26:17 +00:00
if err != nil {
return nil , err
}
for _ , nftOwner := range ownership . Owners {
if nftOwner . OwnerAddress == ownerAddress {
ret [ contractAddress ] = nftOwner . TokenBalances
break
}
}
}
} else if err == nil {
2023-08-01 23:17:59 +00:00
// Account ownership providers succeeded
2023-07-31 19:41:14 +00:00
for _ , fullData := range assetsContainer . Items {
contractAddress := fullData . CollectibleData . ID . ContractID . Address
2023-07-13 17:26:17 +00:00
balance := thirdparty . TokenBalance {
2023-07-31 19:41:14 +00:00
TokenID : fullData . CollectibleData . ID . TokenID ,
2023-07-13 17:26:17 +00:00
Balance : & bigint . BigInt { Int : big . NewInt ( 1 ) } ,
}
ret [ contractAddress ] = append ( ret [ contractAddress ] , balance )
}
} else {
// OpenSea could have provided, but returned error
return nil , err
}
return ret , nil
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) FetchAllAssetsByOwnerAndContractAddress ( ctx context . Context , chainID walletCommon . ChainID , owner common . Address , contractAddresses [ ] common . Address , cursor string , limit int , providerID string ) ( * thirdparty . FullCollectibleDataContainer , error ) {
2023-09-22 13:18:42 +00:00
defer o . checkConnectionStatus ( chainID )
anyProviderAvailable := false
2023-08-01 23:17:59 +00:00
for _ , provider := range o . accountOwnershipProviders {
if ! provider . IsChainSupported ( chainID ) {
continue
}
2023-09-22 13:18:42 +00:00
anyProviderAvailable = true
2023-10-04 16:21:45 +00:00
if providerID != thirdparty . FetchFromAnyProvider && providerID != provider . ID ( ) {
continue
}
2023-03-21 13:52:14 +00:00
2023-11-14 17:16:39 +00:00
assetContainer , err := provider . FetchAllAssetsByOwnerAndContractAddress ( ctx , chainID , owner , contractAddresses , cursor , limit )
2023-08-01 23:17:59 +00:00
if err != nil {
2023-09-22 13:18:42 +00:00
log . Error ( "FetchAllAssetsByOwnerAndContractAddress failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
continue
2023-08-01 23:17:59 +00:00
}
2023-12-13 12:19:25 +00:00
_ , err = o . processFullCollectibleData ( ctx , assetContainer . Items , true )
2023-08-01 23:17:59 +00:00
if err != nil {
return nil , err
}
return assetContainer , nil
2023-03-21 13:52:14 +00:00
}
2023-09-22 13:18:42 +00:00
if anyProviderAvailable {
return nil , ErrAllProvidersFailedForChainID
}
2023-08-01 23:17:59 +00:00
return nil , ErrNoProvidersAvailableForChainID
2023-03-21 13:52:14 +00:00
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) FetchAllAssetsByOwner ( ctx context . Context , chainID walletCommon . ChainID , owner common . Address , cursor string , limit int , providerID string ) ( * thirdparty . FullCollectibleDataContainer , error ) {
2023-09-22 13:18:42 +00:00
defer o . checkConnectionStatus ( chainID )
anyProviderAvailable := false
2023-08-01 23:17:59 +00:00
for _ , provider := range o . accountOwnershipProviders {
if ! provider . IsChainSupported ( chainID ) {
continue
}
2023-10-04 16:21:45 +00:00
anyProviderAvailable = true
if providerID != thirdparty . FetchFromAnyProvider && providerID != provider . ID ( ) {
continue
}
2023-03-21 13:52:14 +00:00
2023-11-14 17:16:39 +00:00
assetContainer , err := provider . FetchAllAssetsByOwner ( ctx , chainID , owner , cursor , limit )
2023-08-01 23:17:59 +00:00
if err != nil {
2023-09-22 13:18:42 +00:00
log . Error ( "FetchAllAssetsByOwner failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
continue
2023-08-01 23:17:59 +00:00
}
2023-12-13 12:19:25 +00:00
_ , err = o . processFullCollectibleData ( ctx , assetContainer . Items , true )
2023-08-01 23:17:59 +00:00
if err != nil {
return nil , err
}
return assetContainer , nil
2023-03-21 13:52:14 +00:00
}
2023-09-22 13:18:42 +00:00
if anyProviderAvailable {
return nil , ErrAllProvidersFailedForChainID
}
2023-08-01 23:17:59 +00:00
return nil , ErrNoProvidersAvailableForChainID
2023-03-21 13:52:14 +00:00
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) FetchCollectibleOwnershipByOwner ( ctx context . Context , chainID walletCommon . ChainID , owner common . Address , cursor string , limit int , providerID string ) ( * thirdparty . CollectibleOwnershipContainer , error ) {
2023-07-31 19:41:14 +00:00
// 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
2023-11-14 17:16:39 +00:00
assetContainer , err := o . FetchAllAssetsByOwner ( ctx , chainID , owner , cursor , limit , providerID )
2023-07-18 15:02:56 +00:00
if err != nil {
2023-07-26 17:48:14 +00:00
return nil , err
2023-07-18 15:02:56 +00:00
}
2023-07-26 17:48:14 +00:00
ret := assetContainer . ToOwnershipContainer ( )
return & ret , nil
2023-07-18 15:02:56 +00:00
}
2023-12-13 12:19:25 +00:00
// Returns collectible metadata for the given unique IDs.
// If asyncFetch is true, empty metadata will be returned for any missing collectibles and an EventCollectiblesDataUpdated will be sent when the data is ready.
// If asyncFetch is false, it will wait for all collectibles' metadata to be retrieved before returning.
func ( o * Manager ) FetchAssetsByCollectibleUniqueID ( ctx context . Context , uniqueIDs [ ] thirdparty . CollectibleUniqueID , asyncFetch bool ) ( [ ] thirdparty . FullCollectibleData , error ) {
2023-08-07 22:30:18 +00:00
missingIDs , err := o . collectiblesDataDB . GetIDsNotInDB ( uniqueIDs )
if err != nil {
return nil , err
}
2023-03-30 21:01:28 +00:00
2023-08-07 22:30:18 +00:00
missingIDsPerChainID := thirdparty . GroupCollectibleUIDsByChainID ( missingIDs )
2023-12-13 12:19:25 +00:00
group := async . NewGroup ( ctx )
group . Add ( func ( ctx context . Context ) error {
for chainID , idsToFetch := range missingIDsPerChainID {
defer o . checkConnectionStatus ( chainID )
2023-09-22 13:18:42 +00:00
2023-12-13 12:19:25 +00:00
for _ , provider := range o . collectibleDataProviders {
if ! provider . IsChainSupported ( chainID ) {
continue
}
2023-08-03 12:24:23 +00:00
2023-12-13 12:19:25 +00:00
fetchedAssets , err := provider . FetchAssetsByCollectibleUniqueID ( ctx , idsToFetch )
if err != nil {
log . Error ( "FetchAssetsByCollectibleUniqueID failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
continue
}
2023-08-03 12:24:23 +00:00
2023-12-13 12:19:25 +00:00
updatedCollectibles , err := o . processFullCollectibleData ( ctx , fetchedAssets , asyncFetch )
if err != nil {
log . Error ( "processFullCollectibleData failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "len(fetchedAssets)" , len ( fetchedAssets ) , "err" , err )
return err
}
2023-08-03 12:24:23 +00:00
2023-12-13 12:19:25 +00:00
if asyncFetch {
o . signalUpdatedCollectiblesData ( updatedCollectibles )
}
break
}
2023-03-30 21:01:28 +00:00
}
2023-12-13 12:19:25 +00:00
return nil
} )
if ! asyncFetch {
group . Wait ( )
2023-03-21 13:52:14 +00:00
}
2023-08-07 22:30:18 +00:00
return o . getCacheFullCollectibleData ( uniqueIDs )
2023-03-21 13:52:14 +00:00
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) FetchCollectionsDataByContractID ( ctx context . Context , ids [ ] thirdparty . ContractID ) ( [ ] thirdparty . CollectionData , error ) {
2023-08-07 22:30:18 +00:00
missingIDs , err := o . collectionsDataDB . GetIDsNotInDB ( ids )
if err != nil {
return nil , err
}
2023-08-03 12:24:23 +00:00
2023-08-07 22:30:18 +00:00
missingIDsPerChainID := thirdparty . GroupContractIDsByChainID ( missingIDs )
for chainID , idsToFetch := range missingIDsPerChainID {
2023-09-22 13:18:42 +00:00
defer o . checkConnectionStatus ( chainID )
2023-08-16 13:01:57 +00:00
for _ , provider := range o . collectionDataProviders {
2023-08-03 12:24:23 +00:00
if ! provider . IsChainSupported ( chainID ) {
continue
}
2023-11-14 17:16:39 +00:00
fetchedCollections , err := provider . FetchCollectionsDataByContractID ( ctx , idsToFetch )
2023-08-03 12:24:23 +00:00
if err != nil {
2023-09-22 13:18:42 +00:00
log . Error ( "FetchCollectionsDataByContractID failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
continue
2023-08-03 12:24:23 +00:00
}
2023-11-14 17:16:39 +00:00
err = o . processCollectionData ( ctx , fetchedCollections )
2023-08-03 12:24:23 +00:00
if err != nil {
return nil , err
}
break
}
}
2023-08-07 22:30:18 +00:00
data , err := o . collectionsDataDB . GetData ( ids )
if err != nil {
return nil , err
}
return mapToList ( data ) , nil
2023-08-03 12:24:23 +00:00
}
2023-08-01 23:17:59 +00:00
func ( o * Manager ) getContractOwnershipProviders ( chainID walletCommon . ChainID ) ( mainProvider thirdparty . CollectibleContractOwnershipProvider , fallbackProvider thirdparty . CollectibleContractOwnershipProvider ) {
mainProvider = nil
fallbackProvider = nil
for _ , provider := range o . contractOwnershipProviders {
if provider . IsChainSupported ( chainID ) {
if mainProvider == nil {
// First provider found
mainProvider = provider
continue
}
// Second provider found
fallbackProvider = provider
break
2023-04-17 11:42:01 +00:00
}
}
2023-08-01 23:17:59 +00:00
return
}
2023-11-14 17:16:39 +00:00
func getCollectibleOwnersByContractAddressFunc ( ctx context . Context , chainID walletCommon . ChainID , contractAddress common . Address , provider thirdparty . CollectibleContractOwnershipProvider ) func ( ) ( any , error ) {
2023-08-01 23:17:59 +00:00
if provider == nil {
return nil
}
return func ( ) ( any , error ) {
2023-11-14 17:16:39 +00:00
res , err := provider . FetchCollectibleOwnersByContractAddress ( ctx , chainID , contractAddress )
2023-09-22 13:18:42 +00:00
if err != nil {
log . Error ( "FetchCollectibleOwnersByContractAddress failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
}
return res , err
2023-08-01 23:17:59 +00:00
}
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) FetchCollectibleOwnersByContractAddress ( ctx context . Context , chainID walletCommon . ChainID , contractAddress common . Address ) ( * thirdparty . CollectibleContractOwnership , error ) {
2023-09-22 13:18:42 +00:00
defer o . checkConnectionStatus ( chainID )
2023-08-01 23:17:59 +00:00
mainProvider , fallbackProvider := o . getContractOwnershipProviders ( chainID )
if mainProvider == nil {
return nil , ErrNoProvidersAvailableForChainID
}
2023-11-14 17:16:39 +00:00
mainFn := getCollectibleOwnersByContractAddressFunc ( ctx , chainID , contractAddress , mainProvider )
fallbackFn := getCollectibleOwnersByContractAddressFunc ( ctx , chainID , contractAddress , fallbackProvider )
2023-08-01 23:17:59 +00:00
owners , err := makeContractOwnershipCall ( mainFn , fallbackFn )
2023-04-17 11:42:01 +00:00
if err != nil {
return nil , err
}
2023-07-05 09:33:48 +00:00
return owners . ( * thirdparty . CollectibleContractOwnership ) , nil
2023-04-17 11:42:01 +00:00
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) fetchTokenURI ( ctx context . Context , id thirdparty . CollectibleUniqueID ) ( string , error ) {
2023-09-11 09:54:37 +00:00
if id . TokenID == nil {
return "" , errors . New ( "empty token ID" )
}
2023-07-31 19:41:14 +00:00
backend , err := o . rpcClient . EthClient ( uint64 ( id . ContractID . ChainID ) )
2023-03-21 13:52:14 +00:00
if err != nil {
2023-03-29 16:36:23 +00:00
return "" , err
2023-03-21 13:52:14 +00:00
}
2023-07-31 19:41:14 +00:00
caller , err := collectibles . NewCollectiblesCaller ( id . ContractID . Address , backend )
2023-03-21 13:52:14 +00:00
if err != nil {
2023-03-29 16:36:23 +00:00
return "" , err
2023-03-21 13:52:14 +00:00
}
2023-03-29 16:36:23 +00:00
tokenURI , err := caller . TokenURI ( & bind . CallOpts {
2023-11-14 17:16:39 +00:00
Context : ctx ,
2023-03-29 16:36:23 +00:00
} , id . TokenID . Int )
2023-03-21 13:52:14 +00:00
if err != nil {
2023-06-06 17:49:36 +00:00
for _ , errorPrefix := range noTokenURIErrorPrefixes {
if strings . HasPrefix ( err . Error ( ) , errorPrefix ) {
// Contract doesn't support "TokenURI" method
return "" , nil
}
2023-03-21 13:52:14 +00:00
}
return "" , err
}
2023-03-29 16:36:23 +00:00
return tokenURI , err
2023-03-21 13:52:14 +00:00
}
2023-12-13 12:19:25 +00:00
func isMetadataEmpty ( asset thirdparty . CollectibleData ) bool {
return asset . Description == "" &&
asset . ImageURL == ""
}
// Processes collectible metadata obtained from a provider and ensures any missing data is fetched.
// If asyncFetch is true, community collectibles metadata will be fetched async and an EventCollectiblesDataUpdated will be sent when the data is ready.
// If asyncFetch is false, it will wait for all community collectibles' metadata to be retrieved before returning.
func ( o * Manager ) processFullCollectibleData ( ctx context . Context , assets [ ] thirdparty . FullCollectibleData , asyncFetch bool ) ( [ ] thirdparty . CollectibleUniqueID , error ) {
2023-10-26 06:30:18 +00:00
fullyFetchedAssets := make ( map [ string ] * thirdparty . FullCollectibleData )
communityCollectibles := make ( map [ string ] [ ] * thirdparty . FullCollectibleData )
2023-12-13 12:19:25 +00:00
processedIDs := make ( [ ] thirdparty . CollectibleUniqueID , 0 , len ( assets ) )
2023-08-03 12:24:23 +00:00
2023-10-26 06:30:18 +00:00
// Start with all assets, remove if any of the fetch steps fail
for idx := range assets {
asset := & assets [ idx ]
2023-07-31 19:41:14 +00:00
id := asset . CollectibleData . ID
2023-10-26 06:30:18 +00:00
fullyFetchedAssets [ id . HashKey ( ) ] = asset
}
2023-03-21 13:52:14 +00:00
2023-12-13 12:19:25 +00:00
// Detect community collectibles
2023-10-26 06:30:18 +00:00
for _ , asset := range fullyFetchedAssets {
// Only check community ownership if metadata is empty
2023-07-31 19:41:14 +00:00
if isMetadataEmpty ( asset . CollectibleData ) {
2023-12-13 12:19:25 +00:00
// Get TokenURI if not given by provider
2023-11-14 17:16:39 +00:00
err := o . fillTokenURI ( ctx , asset )
2023-10-26 06:30:18 +00:00
if err != nil {
log . Error ( "fillTokenURI failed" , "err" , err )
delete ( fullyFetchedAssets , asset . CollectibleData . ID . HashKey ( ) )
continue
2023-09-21 12:40:58 +00:00
}
2023-12-13 12:19:25 +00:00
// Get CommunityID if obtainable from TokenURI
2023-10-26 06:30:18 +00:00
err = o . fillCommunityID ( asset )
2023-03-29 16:36:23 +00:00
if err != nil {
2023-10-26 06:30:18 +00:00
log . Error ( "fillCommunityID failed" , "err" , err )
delete ( fullyFetchedAssets , asset . CollectibleData . ID . HashKey ( ) )
continue
2023-03-29 16:36:23 +00:00
}
2023-03-21 13:52:14 +00:00
2023-12-13 12:19:25 +00:00
// Get metadata from community if community collectible
2023-10-26 06:30:18 +00:00
communityID := asset . CollectibleData . CommunityID
if communityID != "" {
if _ , ok := communityCollectibles [ communityID ] ; ! ok {
communityCollectibles [ communityID ] = make ( [ ] * thirdparty . FullCollectibleData , 0 )
2023-03-21 13:52:14 +00:00
}
2023-10-26 06:30:18 +00:00
communityCollectibles [ communityID ] = append ( communityCollectibles [ communityID ] , asset )
2023-12-13 12:19:25 +00:00
// Community collectibles are handled separately, remove from list
delete ( fullyFetchedAssets , asset . CollectibleData . ID . HashKey ( ) )
2023-03-21 13:52:14 +00:00
}
}
2023-10-26 06:30:18 +00:00
}
2023-03-30 21:01:28 +00:00
2023-10-26 06:30:18 +00:00
// Community collectibles are grouped by community ID
for communityID , communityAssets := range communityCollectibles {
2023-12-13 12:19:25 +00:00
if asyncFetch {
o . fetchCommunityAssetsAsync ( ctx , communityID , communityAssets )
} else {
err := o . fetchCommunityAssets ( communityID , communityAssets )
if err != nil {
log . Error ( "fetchCommunityAssets failed" , "communityID" , communityID , "err" , err )
continue
}
for _ , asset := range communityAssets {
processedIDs = append ( processedIDs , asset . CollectibleData . ID )
2023-07-31 23:34:53 +00:00
}
}
2023-10-26 06:30:18 +00:00
}
for _ , asset := range fullyFetchedAssets {
2023-11-14 17:16:39 +00:00
err := o . fillAnimationMediatype ( ctx , asset )
2023-10-26 06:30:18 +00:00
if err != nil {
log . Error ( "fillAnimationMediatype failed" , "err" , err )
delete ( fullyFetchedAssets , asset . CollectibleData . ID . HashKey ( ) )
continue
}
}
// Save successfully fetched data to DB
collectiblesData := make ( [ ] thirdparty . CollectibleData , 0 , len ( assets ) )
collectionsData := make ( [ ] thirdparty . CollectionData , 0 , len ( assets ) )
missingCollectionIDs := make ( [ ] thirdparty . ContractID , 0 )
for _ , asset := range fullyFetchedAssets {
id := asset . CollectibleData . ID
2023-12-13 12:19:25 +00:00
processedIDs = append ( processedIDs , id )
2023-07-31 23:34:53 +00:00
2023-08-07 22:30:18 +00:00
collectiblesData = append ( collectiblesData , asset . CollectibleData )
if asset . CollectionData != nil {
collectionsData = append ( collectionsData , * asset . CollectionData )
2023-08-03 12:24:23 +00:00
} else {
missingCollectionIDs = append ( missingCollectionIDs , id . ContractID )
}
}
2023-08-07 22:30:18 +00:00
err := o . collectiblesDataDB . SetData ( collectiblesData )
if err != nil {
2023-12-13 12:19:25 +00:00
return nil , err
2023-10-26 06:30:18 +00:00
}
2023-08-07 22:30:18 +00:00
err = o . collectionsDataDB . SetData ( collectionsData )
if err != nil {
2023-12-13 12:19:25 +00:00
return nil , err
2023-08-07 22:30:18 +00:00
}
2023-08-03 12:24:23 +00:00
if len ( missingCollectionIDs ) > 0 {
// Calling this ensures collection data is fetched and cached (if not already available)
2023-11-14 17:16:39 +00:00
_ , err := o . FetchCollectionsDataByContractID ( ctx , missingCollectionIDs )
2023-08-03 12:24:23 +00:00
if err != nil {
2023-12-13 12:19:25 +00:00
return nil , err
2023-07-31 19:41:14 +00:00
}
2023-03-21 13:52:14 +00:00
}
2023-12-13 12:19:25 +00:00
return processedIDs , nil
2023-03-21 13:52:14 +00:00
}
2023-03-30 21:01:28 +00:00
2023-11-14 17:16:39 +00:00
func ( o * Manager ) fillTokenURI ( ctx context . Context , asset * thirdparty . FullCollectibleData ) error {
2023-10-26 06:30:18 +00:00
id := asset . CollectibleData . ID
tokenURI := asset . CollectibleData . TokenURI
// Only need to fetch it from contract if it was empty
if tokenURI == "" {
2023-11-14 17:16:39 +00:00
tokenURI , err := o . fetchTokenURI ( ctx , id )
2023-10-26 06:30:18 +00:00
if err != nil {
return err
}
asset . CollectibleData . TokenURI = tokenURI
}
return nil
}
func ( o * Manager ) fillCommunityID ( asset * thirdparty . FullCollectibleData ) error {
tokenURI := asset . CollectibleData . TokenURI
communityID := ""
if tokenURI != "" {
2023-12-14 16:50:46 +00:00
communityID = o . communityManager . GetCommunityID ( tokenURI )
2023-10-26 06:30:18 +00:00
}
asset . CollectibleData . CommunityID = communityID
return nil
}
2023-12-13 12:19:25 +00:00
func ( o * Manager ) fetchCommunityAssets ( communityID string , communityAssets [ ] * thirdparty . FullCollectibleData ) error {
2023-12-14 16:50:46 +00:00
communityInfo , err := o . communityManager . FetchCommunityInfo ( communityID )
2023-10-26 06:30:18 +00:00
if err != nil {
2023-12-13 12:19:25 +00:00
log . Error ( "fetchCommunityInfo failed" , "communityID" , communityID , "err" , err )
2023-10-26 06:30:18 +00:00
return err
}
if communityInfo != nil {
for _ , communityAsset := range communityAssets {
2023-12-14 16:50:46 +00:00
err := o . communityManager . FillCollectibleMetadata ( communityAsset )
2023-10-26 06:30:18 +00:00
if err != nil {
2023-12-13 12:19:25 +00:00
log . Error ( "FillCollectibleMetadata failed" , "communityID" , communityID , "err" , err )
return err
}
}
} else {
log . Warn ( "fetchCommunityAssets community not found" , "communityID" , communityID )
}
collectiblesData := make ( [ ] thirdparty . CollectibleData , 0 , len ( communityAssets ) )
collectionsData := make ( [ ] thirdparty . CollectionData , 0 , len ( communityAssets ) )
for _ , asset := range communityAssets {
collectiblesData = append ( collectiblesData , asset . CollectibleData )
if asset . CollectionData != nil {
collectionsData = append ( collectionsData , * asset . CollectionData )
}
}
err = o . collectiblesDataDB . SetData ( collectiblesData )
if err != nil {
log . Error ( "collectiblesDataDB SetData failed" , "communityID" , communityID , "err" , err )
return err
}
err = o . collectionsDataDB . SetData ( collectionsData )
if err != nil {
log . Error ( "collectionsDataDB SetData failed" , "communityID" , communityID , "err" , err )
return err
}
for _ , asset := range communityAssets {
if asset . CollectibleCommunityInfo != nil {
err = o . collectiblesDataDB . SetCommunityInfo ( asset . CollectibleData . ID , * asset . CollectibleCommunityInfo )
if err != nil {
log . Error ( "collectiblesDataDB SetCommunityInfo failed" , "communityID" , communityID , "err" , err )
2023-10-26 06:30:18 +00:00
return err
}
}
}
return nil
}
2023-12-13 12:19:25 +00:00
func ( o * Manager ) fetchCommunityAssetsAsync ( ctx context . Context , communityID string , communityAssets [ ] * thirdparty . FullCollectibleData ) {
if len ( communityAssets ) == 0 {
return
}
go func ( ) {
err := o . fetchCommunityAssets ( communityID , communityAssets )
if err != nil {
log . Error ( "fetchCommunityAssets failed" , "communityID" , communityID , "err" , err )
return
}
// Metadata is up to date in db at this point, fetch and send Event.
ids := make ( [ ] thirdparty . CollectibleUniqueID , 0 , len ( communityAssets ) )
for _ , asset := range communityAssets {
ids = append ( ids , asset . CollectibleData . ID )
}
o . signalUpdatedCollectiblesData ( ids )
} ( )
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) fillAnimationMediatype ( ctx context . Context , asset * thirdparty . FullCollectibleData ) error {
2023-10-26 06:30:18 +00:00
if len ( asset . CollectibleData . AnimationURL ) > 0 {
2023-11-14 17:16:39 +00:00
contentType , err := o . doContentTypeRequest ( ctx , asset . CollectibleData . AnimationURL )
2023-10-26 06:30:18 +00:00
if err != nil {
asset . CollectibleData . AnimationURL = ""
}
asset . CollectibleData . AnimationMediaType = contentType
}
return nil
}
2023-11-14 17:16:39 +00:00
func ( o * Manager ) processCollectionData ( ctx context . Context , collections [ ] thirdparty . CollectionData ) error {
2023-08-07 22:30:18 +00:00
return o . collectionsDataDB . SetData ( collections )
2023-07-31 19:41:14 +00:00
}
2023-08-07 22:30:18 +00:00
func ( o * Manager ) getCacheFullCollectibleData ( uniqueIDs [ ] thirdparty . CollectibleUniqueID ) ( [ ] thirdparty . FullCollectibleData , error ) {
2023-07-31 19:41:14 +00:00
ret := make ( [ ] thirdparty . FullCollectibleData , 0 , len ( uniqueIDs ) )
2023-08-07 22:30:18 +00:00
collectiblesData , err := o . collectiblesDataDB . GetData ( uniqueIDs )
if err != nil {
return nil , err
}
2023-07-31 19:41:14 +00:00
contractIDs := make ( [ ] thirdparty . ContractID , 0 , len ( uniqueIDs ) )
for _ , id := range uniqueIDs {
contractIDs = append ( contractIDs , id . ContractID )
}
2023-08-07 22:30:18 +00:00
collectionsData , err := o . collectionsDataDB . GetData ( contractIDs )
if err != nil {
return nil , err
}
2023-07-31 19:41:14 +00:00
for _ , id := range uniqueIDs {
2023-08-07 22:30:18 +00:00
collectibleData , ok := collectiblesData [ id . HashKey ( ) ]
if ! ok {
2023-07-31 19:41:14 +00:00
// Use empty data, set only ID
2023-08-07 22:30:18 +00:00
collectibleData = thirdparty . CollectibleData {
2023-07-31 19:41:14 +00:00
ID : id ,
}
}
2023-12-15 20:29:39 +00:00
if o . mediaServer != nil && len ( collectibleData . ImagePayload ) > 0 {
collectibleData . ImageURL = o . mediaServer . MakeWalletCollectibleImagesURL ( collectibleData . ID )
}
2023-07-31 19:41:14 +00:00
2023-08-07 22:30:18 +00:00
collectionData , ok := collectionsData [ id . ContractID . HashKey ( ) ]
if ! ok {
// Use empty data, set only ID
collectionData = thirdparty . CollectionData {
ID : id . ContractID ,
}
}
2023-12-15 20:29:39 +00:00
if o . mediaServer != nil && len ( collectionData . ImagePayload ) > 0 {
collectionData . ImageURL = o . mediaServer . MakeWalletCollectionImagesURL ( collectionData . ID )
}
2023-08-07 22:30:18 +00:00
2023-12-13 12:19:25 +00:00
communityInfo , _ , err := o . communityManager . GetCommunityInfo ( collectibleData . CommunityID )
if err != nil {
return nil , err
}
collectibleCommunityInfo , err := o . collectiblesDataDB . GetCommunityInfo ( id )
if err != nil {
return nil , err
}
ownership , err := o . ownershipDB . GetOwnership ( id )
2023-11-01 18:44:11 +00:00
if err != nil {
return nil , err
}
2023-07-31 19:41:14 +00:00
fullData := thirdparty . FullCollectibleData {
2023-12-13 12:19:25 +00:00
CollectibleData : collectibleData ,
CollectionData : & collectionData ,
CommunityInfo : communityInfo ,
CollectibleCommunityInfo : collectibleCommunityInfo ,
Ownership : ownership ,
2023-07-31 19:41:14 +00:00
}
ret = append ( ret , fullData )
}
2023-08-07 22:30:18 +00:00
return ret , nil
2023-07-18 15:01:53 +00:00
}
2023-09-21 12:40:58 +00:00
2023-09-22 13:18:42 +00:00
// Reset connection status to trigger notifications
// on the next status update
func ( o * Manager ) ResetConnectionStatus ( ) {
for _ , status := range o . statuses {
status . ResetStateValue ( )
}
}
func ( o * Manager ) checkConnectionStatus ( chainID walletCommon . ChainID ) {
for _ , provider := range o . collectibleProviders {
if provider . IsChainSupported ( chainID ) && provider . IsConnected ( ) {
o . statuses [ chainID . String ( ) ] . SetIsConnected ( true )
return
}
}
o . statuses [ chainID . String ( ) ] . SetIsConnected ( false )
}
2023-12-13 12:19:25 +00:00
func ( o * Manager ) signalUpdatedCollectiblesData ( ids [ ] thirdparty . CollectibleUniqueID ) {
// We limit how much collectibles data we send in each event to avoid problems on the client side
for startIdx := 0 ; startIdx < len ( ids ) ; startIdx += signalUpdatedCollectiblesDataPageSize {
endIdx := startIdx + signalUpdatedCollectiblesDataPageSize
if endIdx > len ( ids ) {
endIdx = len ( ids )
}
pageIDs := ids [ startIdx : endIdx ]
collectibles , err := o . getCacheFullCollectibleData ( pageIDs )
if err != nil {
log . Error ( "Error getting FullCollectibleData from cache: %v" , err )
return
}
// Send update event with most complete data type available
details := fullCollectiblesDataToDetails ( collectibles )
payload , err := json . Marshal ( details )
if err != nil {
log . Error ( "Error marshaling response: %v" , err )
return
}
event := walletevent . Event {
Type : EventCollectiblesDataUpdated ,
Message : string ( payload ) ,
}
o . feed . Send ( event )
}
}