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"
2024-03-11 11:09:50 +00:00
"sync"
2023-03-21 13:52:14 +00:00
"time"
"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"
2024-03-11 11:09:50 +00:00
"github.com/status-im/status-go/circuitbreaker"
2023-07-18 08:33:45 +00:00
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
2024-03-05 18:57:02 +00:00
"github.com/status-im/status-go/contracts/ierc1155"
2023-03-21 13:52:14 +00:00
"github.com/status-im/status-go/rpc"
2024-07-29 17:07:43 +00:00
"github.com/status-im/status-go/rpc/chain"
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-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 )
2024-05-14 06:58:08 +00:00
FetchCollectionSocialsAsync ( contractID thirdparty . ContractID ) error
2023-08-11 17:28:46 +00:00
}
2023-03-21 13:52:14 +00:00
type Manager struct {
2024-07-29 17:07:43 +00:00
rpcClient rpc . ClientInterface
2024-03-13 14:59:18 +00:00
providers thirdparty . CollectibleProviders
2023-09-22 13:18:42 +00:00
httpClient * http . Client
2024-07-29 17:07:43 +00:00
collectiblesDataDB CollectibleDataStorage
collectionsDataDB CollectionDataStorage
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
2024-07-02 17:58:55 +00:00
statuses * sync . Map
statusNotifier * connection . StatusNotifier
feed * event . Feed
circuitBreaker * circuitbreaker . CircuitBreaker
2023-03-21 13:52:14 +00:00
}
2023-09-21 12:40:58 +00:00
func NewManager (
db * sql . DB ,
2024-07-29 17:07:43 +00:00
rpcClient rpc . ClientInterface ,
2023-12-14 16:50:46 +00:00
communityManager * community . Manager ,
2024-03-13 14:59:18 +00:00
providers thirdparty . CollectibleProviders ,
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
2024-07-29 17:07:43 +00:00
var ownershipDB * OwnershipDB
var statuses * sync . Map
var statusNotifier * connection . StatusNotifier
if db != nil {
ownershipDB = NewOwnershipDB ( db )
statuses = initStatuses ( ownershipDB )
statusNotifier = createStatusNotifier ( statuses , feed )
}
2023-09-22 13:18:42 +00:00
2024-07-02 17:58:55 +00:00
cb := circuitbreaker . NewCircuitBreaker ( circuitbreaker . Config {
Timeout : 10000 ,
MaxConcurrentRequests : 100 ,
RequestVolumeThreshold : 25 ,
SleepWindow : 300000 ,
ErrorPercentThreshold : 25 ,
} )
2023-03-21 13:52:14 +00:00
return & Manager {
2024-03-13 14:59:18 +00:00
rpcClient : rpcClient ,
providers : providers ,
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 ,
2024-07-29 17:07:43 +00:00
statusNotifier : statusNotifier ,
2023-12-13 12:19:25 +00:00
feed : feed ,
2024-07-02 17:58:55 +00:00
circuitBreaker : cb ,
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-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
}
2024-06-11 21:00:04 +00:00
func ( o * Manager ) getTokenBalancesByOwnerAddress ( collectibles * thirdparty . CollectibleContractOwnership , ownerAddress common . Address ) map [ common . Address ] [ ] thirdparty . TokenBalance {
ret := make ( map [ common . Address ] [ ] thirdparty . TokenBalance )
for _ , nftOwner := range collectibles . Owners {
if nftOwner . OwnerAddress == ownerAddress {
ret [ collectibles . ContractAddress ] = nftOwner . TokenBalances
break
}
}
return ret
}
func ( o * Manager ) FetchCachedBalancesByOwnerAndContractAddress ( ctx context . Context , chainID walletCommon . ChainID , ownerAddress common . Address , contractAddresses [ ] common . Address ) ( thirdparty . TokenBalancesPerContractAddress , error ) {
ret := make ( map [ common . Address ] [ ] thirdparty . TokenBalance )
for _ , contractAddress := range contractAddresses {
ret [ contractAddress ] = make ( [ ] thirdparty . TokenBalance , 0 )
}
for _ , contractAddress := range contractAddresses {
ownership , err := o . ownershipDB . FetchCachedCollectibleOwnersByContractAddress ( chainID , contractAddress )
if err != nil {
return nil , err
}
t := o . getTokenBalancesByOwnerAddress ( ownership , ownerAddress )
for address , tokenBalances := range t {
ret [ address ] = append ( ret [ address ] , tokenBalances ... )
}
}
return ret , 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
}
2024-06-11 21:00:04 +00:00
ret = o . getTokenBalancesByOwnerAddress ( ownership , ownerAddress )
2023-07-13 17:26:17 +00:00
}
} 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 )
2024-07-02 17:58:55 +00:00
cmd := circuitbreaker . NewCommand ( ctx , nil )
2024-03-13 14:59:18 +00:00
for _ , provider := range o . providers . AccountOwnershipProviders {
2023-08-01 23:17:59 +00:00
if ! provider . IsChainSupported ( chainID ) {
continue
}
2023-10-04 16:21:45 +00:00
if providerID != thirdparty . FetchFromAnyProvider && providerID != provider . ID ( ) {
continue
}
2023-03-21 13:52:14 +00:00
2024-03-11 11:09:50 +00:00
provider := provider
f := circuitbreaker . NewFunctor (
func ( ) ( [ ] interface { } , error ) {
assetContainer , err := provider . FetchAllAssetsByOwnerAndContractAddress ( ctx , chainID , owner , contractAddresses , cursor , limit )
if err != nil {
log . Error ( "FetchAllAssetsByOwnerAndContractAddress failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
}
return [ ] interface { } { assetContainer } , err
2024-07-02 17:58:55 +00:00
} , getCircuitName ( provider , chainID ) ,
2024-03-11 11:09:50 +00:00
)
cmd . Add ( f )
}
2023-08-01 23:17:59 +00:00
2024-03-11 11:09:50 +00:00
if cmd . IsEmpty ( ) {
return nil , ErrNoProvidersAvailableForChainID
}
2023-08-01 23:17:59 +00:00
2024-07-02 17:58:55 +00:00
cmdRes := o . circuitBreaker . Execute ( cmd )
2024-03-11 11:09:50 +00:00
if cmdRes . Error ( ) != nil {
log . Error ( "FetchAllAssetsByOwnerAndContractAddress failed for" , "chainID" , chainID , "err" , cmdRes . Error ( ) )
return nil , cmdRes . Error ( )
2023-03-21 13:52:14 +00:00
}
2024-03-11 11:09:50 +00:00
assetContainer := cmdRes . Result ( ) [ 0 ] . ( * thirdparty . FullCollectibleDataContainer )
_ , err := o . processFullCollectibleData ( ctx , assetContainer . Items , true )
if err != nil {
return nil , err
2023-09-22 13:18:42 +00:00
}
2024-03-11 11:09:50 +00:00
return assetContainer , nil
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 )
2024-07-02 17:58:55 +00:00
cmd := circuitbreaker . NewCommand ( ctx , nil )
2024-03-13 14:59:18 +00:00
for _ , provider := range o . providers . AccountOwnershipProviders {
2023-08-01 23:17:59 +00:00
if ! provider . IsChainSupported ( chainID ) {
continue
}
2023-10-04 16:21:45 +00:00
if providerID != thirdparty . FetchFromAnyProvider && providerID != provider . ID ( ) {
continue
}
2023-03-21 13:52:14 +00:00
2024-03-11 11:09:50 +00:00
provider := provider
f := circuitbreaker . NewFunctor (
func ( ) ( [ ] interface { } , error ) {
assetContainer , err := provider . FetchAllAssetsByOwner ( ctx , chainID , owner , cursor , limit )
if err != nil {
log . Error ( "FetchAllAssetsByOwner failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
}
return [ ] interface { } { assetContainer } , err
2024-07-02 17:58:55 +00:00
} , getCircuitName ( provider , chainID ) ,
2024-03-11 11:09:50 +00:00
)
cmd . Add ( f )
}
2023-08-01 23:17:59 +00:00
2024-03-11 11:09:50 +00:00
if cmd . IsEmpty ( ) {
return nil , ErrNoProvidersAvailableForChainID
}
2023-08-01 23:17:59 +00:00
2024-07-02 17:58:55 +00:00
cmdRes := o . circuitBreaker . Execute ( cmd )
2024-03-11 11:09:50 +00:00
if cmdRes . Error ( ) != nil {
log . Error ( "FetchAllAssetsByOwner failed for" , "chainID" , chainID , "err" , cmdRes . Error ( ) )
return nil , cmdRes . Error ( )
2023-03-21 13:52:14 +00:00
}
2024-03-11 11:09:50 +00:00
assetContainer := cmdRes . Result ( ) [ 0 ] . ( * thirdparty . FullCollectibleDataContainer )
_ , err := o . processFullCollectibleData ( ctx , assetContainer . Items , true )
if err != nil {
return nil , err
2023-09-22 13:18:42 +00:00
}
2024-03-11 11:09:50 +00:00
return assetContainer , nil
2023-03-21 13:52:14 +00:00
}
2024-03-11 11:09:50 +00:00
2024-03-05 18:57:02 +00:00
func ( o * Manager ) FetchERC1155Balances ( ctx context . Context , owner common . Address , chainID walletCommon . ChainID , contractAddress common . Address , tokenIDs [ ] * bigint . BigInt ) ( [ ] * bigint . BigInt , error ) {
if len ( tokenIDs ) == 0 {
return nil , nil
}
backend , err := o . rpcClient . EthClient ( uint64 ( chainID ) )
if err != nil {
return nil , err
}
caller , err := ierc1155 . NewIerc1155Caller ( contractAddress , backend )
if err != nil {
return nil , err
}
owners := make ( [ ] common . Address , len ( tokenIDs ) )
ids := make ( [ ] * big . Int , len ( tokenIDs ) )
for i , tokenID := range tokenIDs {
owners [ i ] = owner
ids [ i ] = tokenID . Int
}
balances , err := caller . BalanceOfBatch ( & bind . CallOpts {
Context : ctx ,
} , owners , ids )
if err != nil {
return nil , err
}
bigIntBalances := make ( [ ] * bigint . BigInt , len ( balances ) )
for i , balance := range balances {
bigIntBalances [ i ] = & bigint . BigInt { Int : balance }
}
return bigIntBalances , err
}
func ( o * Manager ) fillMissingBalances ( ctx context . Context , owner common . Address , collectibles [ ] * thirdparty . FullCollectibleData ) {
collectiblesByChainIDAndContractAddress := thirdparty . GroupCollectiblesByChainIDAndContractAddress ( collectibles )
for chainID , collectiblesByContract := range collectiblesByChainIDAndContractAddress {
for contractAddress , contractCollectibles := range collectiblesByContract {
collectiblesToFetchPerTokenID := make ( map [ string ] * thirdparty . FullCollectibleData )
for _ , collectible := range contractCollectibles {
if collectible . AccountBalance == nil {
switch getContractType ( * collectible ) {
case walletCommon . ContractTypeERC1155 :
collectiblesToFetchPerTokenID [ collectible . CollectibleData . ID . TokenID . String ( ) ] = collectible
default :
// Any other type of collectible is non-fungible, balance is 1
collectible . AccountBalance = & bigint . BigInt { Int : big . NewInt ( 1 ) }
}
}
}
if len ( collectiblesToFetchPerTokenID ) == 0 {
continue
}
tokenIDs := make ( [ ] * bigint . BigInt , 0 , len ( collectiblesToFetchPerTokenID ) )
for _ , c := range collectiblesToFetchPerTokenID {
tokenIDs = append ( tokenIDs , c . CollectibleData . ID . TokenID )
}
balances , err := o . FetchERC1155Balances ( ctx , owner , chainID , contractAddress , tokenIDs )
if err != nil {
log . Error ( "FetchERC1155Balances failed" , "chainID" , chainID , "contractAddress" , contractAddress , "err" , err )
continue
}
for i := range balances {
collectible := collectiblesToFetchPerTokenID [ tokenIDs [ i ] . String ( ) ]
collectible . AccountBalance = balances [ i ]
}
}
}
}
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
}
2024-03-05 18:57:02 +00:00
// Some providers do not give us the balances for ERC1155 tokens, so we need to fetch them separately.
collectibles := make ( [ ] * thirdparty . FullCollectibleData , 0 , len ( assetContainer . Items ) )
for i := range assetContainer . Items {
collectibles = append ( collectibles , & assetContainer . Items [ i ] )
}
o . fillMissingBalances ( ctx , owner , collectibles )
2023-07-26 17:48:14 +00:00
ret := assetContainer . ToOwnershipContainer ( )
2024-03-05 18:57:02 +00:00
2023-07-26 17:48:14 +00:00
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 ) {
2024-03-11 11:09:50 +00:00
err := o . FetchMissingAssetsByCollectibleUniqueID ( ctx , uniqueIDs , asyncFetch )
2023-08-07 22:30:18 +00:00
if err != nil {
return nil , err
}
2023-03-30 21:01:28 +00:00
2024-03-11 11:09:50 +00:00
return o . getCacheFullCollectibleData ( uniqueIDs )
}
func ( o * Manager ) FetchMissingAssetsByCollectibleUniqueID ( ctx context . Context , uniqueIDs [ ] thirdparty . CollectibleUniqueID , asyncFetch bool ) error {
missingIDs , err := o . collectiblesDataDB . GetIDsNotInDB ( uniqueIDs )
if err != nil {
return err
}
2023-08-07 22:30:18 +00:00
missingIDsPerChainID := thirdparty . GroupCollectibleUIDsByChainID ( missingIDs )
2024-03-11 11:09:50 +00:00
// Atomic group stores the error from the first failed command and stops other commands on error
group := async . NewAtomicGroup ( ctx )
for chainID , idsToFetch := range missingIDsPerChainID {
group . Add ( func ( ctx context . Context ) error {
2023-12-13 12:19:25 +00:00
defer o . checkConnectionStatus ( chainID )
2023-09-22 13:18:42 +00:00
2024-03-11 11:09:50 +00:00
fetchedAssets , err := o . fetchMissingAssetsForChainByCollectibleUniqueID ( ctx , chainID , idsToFetch )
if err != nil {
log . Error ( "FetchMissingAssetsByCollectibleUniqueID failed for" , "chainID" , chainID , "ids" , idsToFetch , "err" , err )
return err
}
2023-08-03 12:24:23 +00:00
2024-03-11 11:09:50 +00:00
updatedCollectibles , err := o . processFullCollectibleData ( ctx , fetchedAssets , asyncFetch )
if err != nil {
log . Error ( "processFullCollectibleData failed for" , "chainID" , chainID , "len(fetchedAssets)" , len ( fetchedAssets ) , "err" , err )
return err
}
2023-08-03 12:24:23 +00:00
2024-03-11 11:09:50 +00:00
o . signalUpdatedCollectiblesData ( updatedCollectibles )
return nil
} )
}
2023-08-03 12:24:23 +00:00
2024-03-11 11:09:50 +00:00
if asyncFetch {
group . Wait ( )
return group . Error ( )
}
return nil
}
func ( o * Manager ) fetchMissingAssetsForChainByCollectibleUniqueID ( ctx context . Context , chainID walletCommon . ChainID , idsToFetch [ ] thirdparty . CollectibleUniqueID ) ( [ ] thirdparty . FullCollectibleData , error ) {
2024-07-02 17:58:55 +00:00
cmd := circuitbreaker . NewCommand ( ctx , nil )
2024-03-13 14:59:18 +00:00
for _ , provider := range o . providers . CollectibleDataProviders {
2024-03-11 11:09:50 +00:00
if ! provider . IsChainSupported ( chainID ) {
continue
2023-03-30 21:01:28 +00:00
}
2023-12-13 12:19:25 +00:00
2024-03-11 11:09:50 +00:00
provider := provider
cmd . Add ( circuitbreaker . NewFunctor ( func ( ) ( [ ] any , error ) {
fetchedAssets , err := provider . FetchAssetsByCollectibleUniqueID ( ctx , idsToFetch )
if err != nil {
log . Error ( "fetchMissingAssetsForChainByCollectibleUniqueID failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
}
return [ ] any { fetchedAssets } , err
2024-07-02 17:58:55 +00:00
} , getCircuitName ( provider , chainID ) ) )
2024-03-11 11:09:50 +00:00
}
2023-12-13 12:19:25 +00:00
2024-03-11 11:09:50 +00:00
if cmd . IsEmpty ( ) {
return nil , ErrNoProvidersAvailableForChainID // lets not stop the group if no providers are available for the chain
2023-03-21 13:52:14 +00:00
}
2024-07-02 17:58:55 +00:00
cmdRes := o . circuitBreaker . Execute ( cmd )
2024-03-11 11:09:50 +00:00
if cmdRes . Error ( ) != nil {
log . Error ( "fetchMissingAssetsForChainByCollectibleUniqueID failed for" , "chainID" , chainID , "err" , cmdRes . Error ( ) )
return nil , cmdRes . Error ( )
}
return cmdRes . Result ( ) [ 0 ] . ( [ ] thirdparty . FullCollectibleData ) , cmdRes . Error ( )
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 )
2024-03-11 11:09:50 +00:00
// Atomic group stores the error from the first failed command and stops other commands on error
group := async . NewAtomicGroup ( ctx )
2023-08-07 22:30:18 +00:00
for chainID , idsToFetch := range missingIDsPerChainID {
2024-03-11 11:09:50 +00:00
group . Add ( func ( ctx context . Context ) error {
defer o . checkConnectionStatus ( chainID )
2023-09-22 13:18:42 +00:00
2024-07-02 17:58:55 +00:00
cmd := circuitbreaker . NewCommand ( ctx , nil )
2024-03-13 14:59:18 +00:00
for _ , provider := range o . providers . CollectionDataProviders {
2024-03-11 11:09:50 +00:00
if ! provider . IsChainSupported ( chainID ) {
continue
}
provider := provider
cmd . Add ( circuitbreaker . NewFunctor ( func ( ) ( [ ] any , error ) {
fetchedCollections , err := provider . FetchCollectionsDataByContractID ( ctx , idsToFetch )
return [ ] any { fetchedCollections } , err
2024-07-02 17:58:55 +00:00
} , getCircuitName ( provider , chainID ) ) )
2023-08-03 12:24:23 +00:00
}
2024-03-11 11:09:50 +00:00
if cmd . IsEmpty ( ) {
return nil
}
2024-07-02 17:58:55 +00:00
cmdRes := o . circuitBreaker . Execute ( cmd )
2024-03-11 11:09:50 +00:00
if cmdRes . Error ( ) != nil {
log . Error ( "FetchCollectionsDataByContractID failed for" , "chainID" , chainID , "err" , cmdRes . Error ( ) )
return cmdRes . Error ( )
2023-08-03 12:24:23 +00:00
}
2024-03-11 11:09:50 +00:00
fetchedCollections := cmdRes . Result ( ) [ 0 ] . ( [ ] thirdparty . CollectionData )
2023-11-14 17:16:39 +00:00
err = o . processCollectionData ( ctx , fetchedCollections )
2023-08-03 12:24:23 +00:00
if err != nil {
2024-03-11 11:09:50 +00:00
return err
2023-08-03 12:24:23 +00:00
}
2024-03-11 11:09:50 +00:00
return err
} )
}
group . Wait ( )
if group . Error ( ) != nil {
return nil , group . Error ( )
2023-08-03 12:24:23 +00:00
}
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
}
2024-02-22 08:08:58 +00:00
func ( o * Manager ) GetCollectibleOwnership ( id thirdparty . CollectibleUniqueID ) ( [ ] thirdparty . AccountBalance , error ) {
return o . ownershipDB . GetOwnership ( id )
}
2024-03-11 11:09:50 +00:00
func ( o * Manager ) FetchCollectibleOwnersByContractAddress ( ctx context . Context , chainID walletCommon . ChainID , contractAddress common . Address ) ( * thirdparty . CollectibleContractOwnership , error ) {
defer o . checkConnectionStatus ( chainID )
2023-08-01 23:17:59 +00:00
2024-07-02 17:58:55 +00:00
cmd := circuitbreaker . NewCommand ( ctx , nil )
2024-03-13 14:59:18 +00:00
for _ , provider := range o . providers . ContractOwnershipProviders {
2024-03-11 11:09:50 +00:00
if ! provider . IsChainSupported ( chainID ) {
continue
2023-04-17 11:42:01 +00:00
}
2023-08-01 23:17:59 +00:00
2024-03-11 11:09:50 +00:00
provider := provider
cmd . Add ( circuitbreaker . NewFunctor ( func ( ) ( [ ] any , error ) {
res , err := provider . FetchCollectibleOwnersByContractAddress ( ctx , chainID , contractAddress )
if err != nil {
log . Error ( "FetchCollectibleOwnersByContractAddress failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
}
return [ ] any { res } , err
2024-07-02 17:58:55 +00:00
} , getCircuitName ( provider , chainID ) ) )
2023-08-01 23:17:59 +00:00
}
2023-09-22 13:18:42 +00:00
2024-03-11 11:09:50 +00:00
if cmd . IsEmpty ( ) {
2023-08-01 23:17:59 +00:00
return nil , ErrNoProvidersAvailableForChainID
}
2024-07-02 17:58:55 +00:00
cmdRes := o . circuitBreaker . Execute ( cmd )
2024-03-11 11:09:50 +00:00
if cmdRes . Error ( ) != nil {
log . Error ( "FetchCollectibleOwnersByContractAddress failed for" , "chainID" , chainID , "err" , cmdRes . Error ( ) )
return nil , cmdRes . Error ( )
2023-04-17 11:42:01 +00:00
}
2024-03-11 11:09:50 +00:00
return cmdRes . Result ( ) [ 0 ] . ( * thirdparty . CollectibleContractOwnership ) , cmdRes . Error ( )
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" )
}
2024-07-29 17:07:43 +00:00
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
}
2024-07-29 17:07:43 +00:00
backend = getClientWithNoCircuitTripping ( backend )
2023-07-31 19:41:14 +00:00
caller , err := collectibles . NewCollectiblesCaller ( id . ContractID . Address , backend )
2024-07-29 17:07:43 +00:00
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 {
2024-07-04 15:01:42 +00:00
if strings . Contains ( err . Error ( ) , errorPrefix ) {
2023-06-06 17:49:36 +00:00
// 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 )
}
}
2024-01-11 15:05:40 +00:00
err := o . collectiblesDataDB . SetData ( collectiblesData , true )
2023-08-07 22:30:18 +00:00
if err != nil {
2023-12-13 12:19:25 +00:00
return nil , err
2023-10-26 06:30:18 +00:00
}
2024-01-11 15:05:40 +00:00
err = o . collectionsDataDB . SetData ( collectionsData , true )
2023-08-07 22:30:18 +00:00
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 {
2024-04-10 09:21:41 +00:00
communityFound , err := o . communityManager . FillCollectiblesMetadata ( communityID , communityAssets )
2023-10-26 06:30:18 +00:00
if err != nil {
2024-04-10 09:21:41 +00:00
log . Error ( "FillCollectiblesMetadata failed" , "communityID" , communityID , "err" , err )
} else if ! communityFound {
2024-01-11 15:05:40 +00:00
log . Warn ( "fetchCommunityAssets community not found" , "communityID" , communityID )
2023-12-13 12:19:25 +00:00
}
2024-04-10 09:21:41 +00:00
// If the community is found, we update the DB.
// If the community is not found, we only insert new entries to the DB (don't replace what is already there).
allowUpdate := communityFound
2023-12-13 12:19:25 +00:00
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 )
}
}
2024-01-11 15:05:40 +00:00
err = o . collectiblesDataDB . SetData ( collectiblesData , allowUpdate )
2023-12-13 12:19:25 +00:00
if err != nil {
log . Error ( "collectiblesDataDB SetData failed" , "communityID" , communityID , "err" , err )
return err
}
2024-01-11 15:05:40 +00:00
err = o . collectionsDataDB . SetData ( collectionsData , allowUpdate )
2023-12-13 12:19:25 +00:00
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
}
2024-06-05 16:32:09 +00:00
func ( o * Manager ) fetchCommunityAssetsAsync ( _ context . Context , communityID string , communityAssets [ ] * thirdparty . FullCollectibleData ) {
2023-12-13 12:19:25 +00:00
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
}
2024-06-05 16:32:09 +00:00
func ( o * Manager ) processCollectionData ( _ context . Context , collections [ ] thirdparty . CollectionData ) error {
2024-01-11 15:05:40 +00:00
return o . collectionsDataDB . SetData ( collections , true )
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
2024-02-15 00:42:27 +00:00
func ( o * Manager ) SetCollectibleTransferID ( ownerAddress common . Address , id thirdparty . CollectibleUniqueID , transferID common . Hash , notify bool ) error {
changed , err := o . ownershipDB . SetTransferID ( ownerAddress , id , transferID )
if err != nil {
return err
}
if changed && notify {
o . signalUpdatedCollectiblesData ( [ ] thirdparty . CollectibleUniqueID { id } )
}
return nil
}
2023-09-22 13:18:42 +00:00
// Reset connection status to trigger notifications
// on the next status update
func ( o * Manager ) ResetConnectionStatus ( ) {
2024-06-05 16:32:09 +00:00
o . statuses . Range ( func ( key , value interface { } ) bool {
value . ( * connection . Status ) . ResetStateValue ( )
return true
} )
2023-09-22 13:18:42 +00:00
}
func ( o * Manager ) checkConnectionStatus ( chainID walletCommon . ChainID ) {
2024-03-13 14:59:18 +00:00
for _ , provider := range o . providers . GetProviderList ( ) {
2023-09-22 13:18:42 +00:00
if provider . IsChainSupported ( chainID ) && provider . IsConnected ( ) {
2024-06-05 16:32:09 +00:00
if status , ok := o . statuses . Load ( chainID . String ( ) ) ; ok {
status . ( * connection . Status ) . SetIsConnected ( true )
}
2023-09-22 13:18:42 +00:00
return
}
}
2024-06-05 16:32:09 +00:00
// If no chain in statuses, add it
statusVal , ok := o . statuses . Load ( chainID . String ( ) )
if ! ok {
2024-06-18 16:09:25 +00:00
status := connection . NewStatus ( )
2024-06-05 16:32:09 +00:00
status . SetIsConnected ( false )
o . statuses . Store ( chainID . String ( ) , status )
o . updateStatusNotifier ( )
} else {
2024-06-18 16:09:25 +00:00
statusVal . ( * connection . Status ) . SetIsConnected ( false )
2024-06-05 16:32:09 +00:00
}
2023-09-22 13:18:42 +00:00
}
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 )
}
}
2024-03-11 11:09:50 +00:00
2024-03-13 14:59:18 +00:00
func ( o * Manager ) SearchCollectibles ( ctx context . Context , chainID walletCommon . ChainID , text string , cursor string , limit int , providerID string ) ( * thirdparty . FullCollectibleDataContainer , error ) {
defer o . checkConnectionStatus ( chainID )
anyProviderAvailable := false
for _ , provider := range o . providers . SearchProviders {
if ! provider . IsChainSupported ( chainID ) {
continue
}
anyProviderAvailable = true
if providerID != thirdparty . FetchFromAnyProvider && providerID != provider . ID ( ) {
continue
}
// TODO (#13951): Be smarter about how we handle the user-entered string
collections := [ ] common . Address { }
container , err := provider . SearchCollectibles ( ctx , chainID , collections , text , cursor , limit )
if err != nil {
log . Error ( "FetchAllAssetsByOwner failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
continue
}
_ , err = o . processFullCollectibleData ( ctx , container . Items , true )
if err != nil {
return nil , err
}
return container , nil
}
if anyProviderAvailable {
return nil , ErrAllProvidersFailedForChainID
}
return nil , ErrNoProvidersAvailableForChainID
}
func ( o * Manager ) SearchCollections ( ctx context . Context , chainID walletCommon . ChainID , query string , cursor string , limit int , providerID string ) ( * thirdparty . CollectionDataContainer , error ) {
defer o . checkConnectionStatus ( chainID )
anyProviderAvailable := false
for _ , provider := range o . providers . SearchProviders {
if ! provider . IsChainSupported ( chainID ) {
continue
}
anyProviderAvailable = true
if providerID != thirdparty . FetchFromAnyProvider && providerID != provider . ID ( ) {
continue
}
// TODO (#13951): Be smarter about how we handle the user-entered string
container , err := provider . SearchCollections ( ctx , chainID , query , cursor , limit )
if err != nil {
log . Error ( "FetchAllAssetsByOwner failed for" , "provider" , provider . ID ( ) , "chainID" , chainID , "err" , err )
continue
}
err = o . processCollectionData ( ctx , container . Items )
if err != nil {
return nil , err
}
return container , nil
}
if anyProviderAvailable {
return nil , ErrAllProvidersFailedForChainID
}
return nil , ErrNoProvidersAvailableForChainID
}
2024-05-14 06:58:08 +00:00
func ( o * Manager ) FetchCollectionSocialsAsync ( contractID thirdparty . ContractID ) error {
go func ( ) {
defer o . checkConnectionStatus ( contractID . ChainID )
socials , err := o . getOrFetchSocialsForCollection ( context . Background ( ) , contractID )
if err != nil || socials == nil {
log . Debug ( "FetchCollectionSocialsAsync failed for" , "chainID" , contractID . ChainID , "address" , contractID . Address , "err" , err )
return
}
socialsMessage := CollectionSocialsMessage {
ID : contractID ,
Socials : socials ,
}
payload , err := json . Marshal ( socialsMessage )
if err != nil {
log . Error ( "Error marshaling response: %v" , err )
return
}
event := walletevent . Event {
Type : EventGetCollectionSocialsDone ,
Message : string ( payload ) ,
}
o . feed . Send ( event )
} ( )
return nil
}
2024-06-05 16:32:09 +00:00
func ( o * Manager ) getOrFetchSocialsForCollection ( _ context . Context , contractID thirdparty . ContractID ) ( * thirdparty . CollectionSocials , error ) {
2024-05-14 06:58:08 +00:00
socials , err := o . collectionsDataDB . GetSocialsForID ( contractID )
if err != nil {
log . Debug ( "getOrFetchSocialsForCollection failed for" , "chainID" , contractID . ChainID , "address" , contractID . Address , "err" , err )
return nil , err
}
if socials == nil {
return o . fetchSocialsForCollection ( context . Background ( ) , contractID )
}
return socials , nil
}
func ( o * Manager ) fetchSocialsForCollection ( ctx context . Context , contractID thirdparty . ContractID ) ( * thirdparty . CollectionSocials , error ) {
2024-07-02 17:58:55 +00:00
cmd := circuitbreaker . NewCommand ( ctx , nil )
2024-05-14 06:58:08 +00:00
for _ , provider := range o . providers . CollectibleDataProviders {
if ! provider . IsChainSupported ( contractID . ChainID ) {
continue
}
provider := provider
cmd . Add ( circuitbreaker . NewFunctor ( func ( ) ( [ ] interface { } , error ) {
socials , err := provider . FetchCollectionSocials ( ctx , contractID )
if err != nil {
log . Error ( "FetchCollectionSocials failed for" , "provider" , provider . ID ( ) , "chainID" , contractID . ChainID , "err" , err )
}
return [ ] interface { } { socials } , err
2024-07-02 17:58:55 +00:00
} , getCircuitName ( provider , contractID . ChainID ) ) )
2024-05-14 06:58:08 +00:00
}
if cmd . IsEmpty ( ) {
return nil , ErrNoProvidersAvailableForChainID // lets not stop the group if no providers are available for the chain
}
2024-07-02 17:58:55 +00:00
cmdRes := o . circuitBreaker . Execute ( cmd )
2024-05-14 06:58:08 +00:00
if cmdRes . Error ( ) != nil {
log . Error ( "fetchSocialsForCollection failed for" , "chainID" , contractID . ChainID , "err" , cmdRes . Error ( ) )
return nil , cmdRes . Error ( )
}
socials := cmdRes . Result ( ) [ 0 ] . ( * thirdparty . CollectionSocials )
err := o . collectionsDataDB . SetCollectionSocialsData ( contractID , socials )
if err != nil {
log . Error ( "Error saving socials to DB: %v" , err )
return nil , err
}
return socials , cmdRes . Error ( )
}
2024-06-05 16:32:09 +00:00
func ( o * Manager ) updateStatusNotifier ( ) {
o . statusNotifier = createStatusNotifier ( o . statuses , o . feed )
}
func initStatuses ( ownershipDB * OwnershipDB ) * sync . Map {
statuses := & sync . Map { }
for _ , chainID := range walletCommon . AllChainIDs ( ) {
status := connection . NewStatus ( )
state := status . GetState ( )
latestUpdateTimestamp , err := ownershipDB . GetLatestOwnershipUpdateTimestamp ( chainID )
if err == nil {
state . LastSuccessAt = latestUpdateTimestamp
status . SetState ( state )
}
statuses . Store ( chainID . String ( ) , status )
}
return statuses
}
func createStatusNotifier ( statuses * sync . Map , feed * event . Feed ) * connection . StatusNotifier {
return connection . NewStatusNotifier (
statuses ,
EventCollectiblesConnectionStatusChanged ,
feed ,
)
}
2024-07-02 17:58:55 +00:00
// Different providers have API keys per chain or per testnet/mainnet.
// Proper implementation should respect that. For now, the safest solution is to use the provider ID and chain ID as the key.
func getCircuitName ( provider thirdparty . CollectibleProvider , chainID walletCommon . ChainID ) string {
return provider . ID ( ) + chainID . String ( )
}
2024-07-29 17:07:43 +00:00
func getCircuitNameForTokenURI ( mainCircuitName string ) string {
return mainCircuitName + "_tokenURI"
}
// As we don't use hystrix internal way of switching to another circuit, just its metrics,
// we still can switch to another provider without tripping the circuit.
func getClientWithNoCircuitTripping ( backend chain . ClientInterface ) chain . ClientInterface {
copyable := backend . ( chain . Copyable )
if copyable != nil {
backendCopy := copyable . Copy ( ) . ( chain . ClientInterface )
hm := backendCopy . ( chain . HealthMonitor )
if hm != nil {
cb := circuitbreaker . NewCircuitBreaker ( circuitbreaker . Config {
Timeout : 20000 ,
MaxConcurrentRequests : 100 ,
SleepWindow : 300000 ,
ErrorPercentThreshold : 101 , // Always healthy
} )
cb . SetOverrideCircuitNameHandler ( func ( circuitName string ) string {
return getCircuitNameForTokenURI ( circuitName )
} )
hm . SetCircuitBreaker ( cb )
backend = backendCopy
}
}
return backend
}