feat: add api to get list of owners from a given nft contract
Fixes #10290
This commit is contained in:
parent
2ebe5b97e7
commit
c8f0ceccc8
|
@ -136,7 +136,7 @@ func (b *StatusNode) initServices(config *params.NodeConfig, mediaServer *server
|
|||
if len(openseaKey) == 0 {
|
||||
openseaKey = OpenseaKeyFromEnv
|
||||
}
|
||||
walletService := b.walletService(accDB, accountsFeed, openseaKey)
|
||||
walletService := b.walletService(accDB, accountsFeed, openseaKey, config.WalletConfig.AlchemyAPIKeys, config.WalletConfig.InfuraAPIKey, config.WalletConfig.InfuraAPIKeySecret)
|
||||
services = append(services, walletService)
|
||||
}
|
||||
|
||||
|
@ -471,7 +471,7 @@ func (b *StatusNode) appmetricsService() common.StatusService {
|
|||
return b.appMetricsSrvc
|
||||
}
|
||||
|
||||
func (b *StatusNode) walletService(accountsDB *accounts.Database, accountsFeed *event.Feed, openseaAPIKey string) common.StatusService {
|
||||
func (b *StatusNode) walletService(accountsDB *accounts.Database, accountsFeed *event.Feed, openseaAPIKey string, alchemyAPIKeys map[uint64]string, infuraAPIKey string, infuraAPIKeySecret string) common.StatusService {
|
||||
if b.walletSrvc == nil {
|
||||
var extService *ext.Service
|
||||
if b.WakuV2ExtService() != nil {
|
||||
|
@ -480,7 +480,7 @@ func (b *StatusNode) walletService(accountsDB *accounts.Database, accountsFeed *
|
|||
extService = b.WakuExtService().Service
|
||||
}
|
||||
b.walletSrvc = wallet.NewService(
|
||||
b.appDB, accountsDB, b.rpcClient, accountsFeed, openseaAPIKey, b.gethAccountManager, b.transactor, b.config,
|
||||
b.appDB, accountsDB, b.rpcClient, accountsFeed, openseaAPIKey, alchemyAPIKeys, infuraAPIKey, infuraAPIKeySecret, b.gethAccountManager, b.transactor, b.config,
|
||||
b.ensService(),
|
||||
b.stickersService(accountsDB),
|
||||
extService,
|
||||
|
|
|
@ -515,8 +515,11 @@ type Network struct {
|
|||
|
||||
// WalletConfig extra configuration for wallet.Service.
|
||||
type WalletConfig struct {
|
||||
Enabled bool
|
||||
OpenseaAPIKey string `json:"OpenseaAPIKey"`
|
||||
Enabled bool
|
||||
OpenseaAPIKey string `json:"OpenseaAPIKey"`
|
||||
AlchemyAPIKeys map[uint64]string `json:"AlchemyAPIKeys"`
|
||||
InfuraAPIKey string `json:"InfuraAPIKey"`
|
||||
InfuraAPIKeySecret string `json:"InfuraAPIKeySecret"`
|
||||
}
|
||||
|
||||
// LocalNotificationsConfig extra configuration for localnotifications.Service.
|
||||
|
|
|
@ -320,6 +320,11 @@ func (api *API) GetOpenseaAssetsByNFTUniqueID(ctx context.Context, chainID uint6
|
|||
return api.s.collectiblesManager.FetchAssetsByNFTUniqueID(chainID, uniqueIDs, limit)
|
||||
}
|
||||
|
||||
func (api *API) GetCollectibleOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.NFTContractOwnership, error) {
|
||||
log.Debug("call to GetCollectibleOwnersByContractAddress")
|
||||
return api.s.collectiblesManager.FetchNFTOwnersByContractAddress(chainID, contractAddress)
|
||||
}
|
||||
|
||||
func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error {
|
||||
log.Debug("call to AddEthereumChain")
|
||||
return api.s.rpcClient.NetworkManager.Upsert(&network)
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package bigint
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"reflect"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
)
|
||||
|
||||
// Unmarshals a u256 as a fixed-length hex string with 0x prefix and leading zeros
|
||||
type HexBigInt struct {
|
||||
*big.Int
|
||||
}
|
||||
|
||||
const FixedLength = 32 // u256 -> 32 bytes
|
||||
|
||||
var (
|
||||
hexBigIntT = reflect.TypeOf(HexBigInt{})
|
||||
)
|
||||
|
||||
func (b *HexBigInt) UnmarshalJSON(input []byte) error {
|
||||
var buf [FixedLength]byte
|
||||
err := hexutil.UnmarshalFixedJSON(hexBigIntT, input, buf[:])
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
z := new(big.Int)
|
||||
z.SetBytes(buf[:])
|
||||
b.Int = z
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package bigint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMarshalUnmarshal(t *testing.T) {
|
||||
inputString := "0x09abc5177d51c36ef4c6a36197d023b60d8fec0100000000000001000000000a"
|
||||
inputInt := new(big.Int)
|
||||
inputInt.SetString(inputString[2:], 16)
|
||||
|
||||
inputBytes, err := json.Marshal(inputString)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
u := new(HexBigInt)
|
||||
err = u.UnmarshalJSON(inputBytes)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, inputInt, u.Int)
|
||||
}
|
|
@ -6,6 +6,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/afex/hystrix-go/hystrix"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
|
@ -17,22 +19,64 @@ import (
|
|||
|
||||
const requestTimeout = 5 * time.Second
|
||||
|
||||
const hystrixContractOwnershipClientName = "contractOwnershipClient"
|
||||
|
||||
type Manager struct {
|
||||
rpcClient *rpc.Client
|
||||
metadataProvider thirdparty.NFTMetadataProvider
|
||||
openseaAPIKey string
|
||||
nftCache map[uint64]map[string]opensea.Asset
|
||||
nftCacheLock sync.RWMutex
|
||||
walletFeed *event.Feed
|
||||
rpcClient *rpc.Client
|
||||
mainContractOwnershipProvider thirdparty.NFTContractOwnershipProvider
|
||||
fallbackContractOwnershipProvider thirdparty.NFTContractOwnershipProvider
|
||||
metadataProvider thirdparty.NFTMetadataProvider
|
||||
openseaAPIKey string
|
||||
nftCache map[uint64]map[string]opensea.Asset
|
||||
nftCacheLock sync.RWMutex
|
||||
walletFeed *event.Feed
|
||||
}
|
||||
|
||||
func NewManager(rpcClient *rpc.Client, metadataProvider thirdparty.NFTMetadataProvider, openseaAPIKey string, walletFeed *event.Feed) *Manager {
|
||||
func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty.NFTContractOwnershipProvider, fallbackContractOwnershipProvider thirdparty.NFTContractOwnershipProvider, metadataProvider thirdparty.NFTMetadataProvider, openseaAPIKey string, walletFeed *event.Feed) *Manager {
|
||||
hystrix.ConfigureCommand(hystrixContractOwnershipClientName, hystrix.CommandConfig{
|
||||
Timeout: 10000,
|
||||
MaxConcurrentRequests: 100,
|
||||
SleepWindow: 300000,
|
||||
ErrorPercentThreshold: 25,
|
||||
})
|
||||
|
||||
return &Manager{
|
||||
rpcClient: rpcClient,
|
||||
metadataProvider: metadataProvider,
|
||||
openseaAPIKey: openseaAPIKey,
|
||||
nftCache: make(map[uint64]map[string]opensea.Asset),
|
||||
walletFeed: walletFeed,
|
||||
rpcClient: rpcClient,
|
||||
mainContractOwnershipProvider: mainContractOwnershipProvider,
|
||||
fallbackContractOwnershipProvider: fallbackContractOwnershipProvider,
|
||||
metadataProvider: metadataProvider,
|
||||
openseaAPIKey: openseaAPIKey,
|
||||
nftCache: make(map[uint64]map[string]opensea.Asset),
|
||||
walletFeed: walletFeed,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,6 +174,24 @@ func (o *Manager) FetchAssetsByNFTUniqueID(chainID uint64, uniqueIDs []thirdpart
|
|||
return assetContainer, nil
|
||||
}
|
||||
|
||||
func (o *Manager) FetchNFTOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.NFTContractOwnership, error) {
|
||||
mainFunc := func() (any, error) {
|
||||
return o.mainContractOwnershipProvider.FetchNFTOwnersByContractAddress(chainID, contractAddress)
|
||||
}
|
||||
var fallbackFunc func() (any, error) = nil
|
||||
if o.fallbackContractOwnershipProvider != nil && o.fallbackContractOwnershipProvider.IsChainSupported(chainID) {
|
||||
fallbackFunc = func() (any, error) {
|
||||
return o.fallbackContractOwnershipProvider.FetchNFTOwnersByContractAddress(chainID, contractAddress)
|
||||
}
|
||||
}
|
||||
owners, err := makeContractOwnershipCall(mainFunc, fallbackFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return owners.(*thirdparty.NFTContractOwnership), nil
|
||||
}
|
||||
|
||||
func isMetadataEmpty(asset opensea.Asset) bool {
|
||||
return asset.Name == "" &&
|
||||
asset.Description == "" &&
|
||||
|
|
|
@ -22,8 +22,10 @@ import (
|
|||
"github.com/status-im/status-go/services/wallet/history"
|
||||
"github.com/status-im/status-go/services/wallet/market"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/alchemy"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/coingecko"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/cryptocompare"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/infura"
|
||||
"github.com/status-im/status-go/services/wallet/token"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||
|
@ -41,6 +43,9 @@ func NewService(
|
|||
rpcClient *rpc.Client,
|
||||
accountFeed *event.Feed,
|
||||
openseaAPIKey string,
|
||||
alchemyAPIKeys map[uint64]string,
|
||||
infuraAPIKey string,
|
||||
infuraAPIKeySecret string,
|
||||
gethManager *account.GethManager,
|
||||
transactor *transactions.Transactor,
|
||||
config *params.NodeConfig,
|
||||
|
@ -92,7 +97,10 @@ func NewService(
|
|||
reader := NewReader(rpcClient, tokenManager, marketManager, accountsDB, walletFeed)
|
||||
history := history.NewService(db, walletFeed, rpcClient, tokenManager, marketManager)
|
||||
currency := currency.NewService(db, walletFeed, tokenManager, marketManager)
|
||||
collectiblesManager := collectibles.NewManager(rpcClient, nftMetadataProvider, openseaAPIKey, walletFeed)
|
||||
|
||||
alchemyClient := alchemy.NewClient(alchemyAPIKeys)
|
||||
infuraClient := infura.NewClient(infuraAPIKey, infuraAPIKeySecret)
|
||||
collectiblesManager := collectibles.NewManager(rpcClient, alchemyClient, infuraClient, nftMetadataProvider, openseaAPIKey, walletFeed)
|
||||
return &Service{
|
||||
db: db,
|
||||
accountsDB: accountsDB,
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
package alchemy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
func getBaseURL(chainID uint64) (string, error) {
|
||||
switch chainID {
|
||||
case 1:
|
||||
return "https://eth-mainnet.g.alchemy.com", nil
|
||||
case 5:
|
||||
return "https://eth-goerli.g.alchemy.com", nil
|
||||
case 11155111:
|
||||
return "https://eth-sepolia.g.alchemy.com", nil
|
||||
case 10:
|
||||
return "https://opt-mainnet.g.alchemy.com", nil
|
||||
case 420:
|
||||
return "https://opt-goerli.g.alchemy.com", nil
|
||||
case 42161:
|
||||
return "https://arb-mainnet.g.alchemy.com", nil
|
||||
case 421613:
|
||||
return "https://arb-goerli.g.alchemy.com", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("chainID not supported: %d", chainID)
|
||||
}
|
||||
|
||||
func getAPIKeySubpath(apiKey string) string {
|
||||
if apiKey == "" {
|
||||
return "demo"
|
||||
}
|
||||
return apiKey
|
||||
}
|
||||
|
||||
func getNFTBaseURL(chainID uint64, apiKey string) (string, error) {
|
||||
baseURL, err := getBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/nft/v2/%s", baseURL, getAPIKeySubpath(apiKey)), nil
|
||||
}
|
||||
|
||||
type TokenBalance struct {
|
||||
TokenID *bigint.HexBigInt `json:"tokenId"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
}
|
||||
|
||||
type NFTOwner struct {
|
||||
OwnerAddress common.Address `json:"ownerAddress"`
|
||||
TokenBalances []TokenBalance `json:"tokenBalances"`
|
||||
}
|
||||
|
||||
type NFTContractOwnership struct {
|
||||
Owners []NFTOwner `json:"ownerAddresses"`
|
||||
PageKey string `json:"pageKey"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
thirdparty.NFTContractOwnershipProvider
|
||||
client *http.Client
|
||||
apiKeys map[uint64]string
|
||||
IsConnected bool
|
||||
IsConnectedLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewClient(apiKeys map[uint64]string) *Client {
|
||||
return &Client{
|
||||
client: &http.Client{Timeout: time.Minute},
|
||||
apiKeys: apiKeys,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Client) doQuery(url string) (*http.Response, error) {
|
||||
resp, err := o.client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (o *Client) IsChainSupported(chainID uint64) bool {
|
||||
_, err := getBaseURL(chainID)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func alchemyOwnershipToCommon(contractAddress common.Address, alchemyOwnership NFTContractOwnership) (*thirdparty.NFTContractOwnership, error) {
|
||||
owners := make([]thirdparty.NFTOwner, 0, len(alchemyOwnership.Owners))
|
||||
for _, alchemyOwner := range alchemyOwnership.Owners {
|
||||
balances := make([]thirdparty.TokenBalance, 0, len(alchemyOwner.TokenBalances))
|
||||
|
||||
for _, alchemyBalance := range alchemyOwner.TokenBalances {
|
||||
balances = append(balances, thirdparty.TokenBalance{
|
||||
TokenID: &bigint.BigInt{Int: alchemyBalance.TokenID.Int},
|
||||
Balance: alchemyBalance.Balance,
|
||||
})
|
||||
}
|
||||
owner := thirdparty.NFTOwner{
|
||||
OwnerAddress: alchemyOwner.OwnerAddress,
|
||||
TokenBalances: balances,
|
||||
}
|
||||
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
|
||||
ownership := thirdparty.NFTContractOwnership{
|
||||
ContractAddress: contractAddress,
|
||||
Owners: owners,
|
||||
}
|
||||
|
||||
return &ownership, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchNFTOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.NFTContractOwnership, error) {
|
||||
queryParams := url.Values{
|
||||
"contractAddress": {contractAddress.String()},
|
||||
"withTokenBalances": {"true"},
|
||||
}
|
||||
|
||||
url, err := getNFTBaseURL(chainID, o.apiKeys[chainID])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url = url + "/getOwnersForCollection?" + queryParams.Encode()
|
||||
|
||||
resp, err := o.doQuery(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var alchemyOwnership NFTContractOwnership
|
||||
err = json.Unmarshal(body, &alchemyOwnership)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return alchemyOwnershipToCommon(contractAddress, alchemyOwnership)
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package infura
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
const baseURL = "https://nft.api.infura.io"
|
||||
|
||||
type NFTOwner struct {
|
||||
ContractAddress common.Address `json:"tokenAddress"`
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
Amount *bigint.BigInt `json:"amount"`
|
||||
OwnerAddress common.Address `json:"ownerOf"`
|
||||
}
|
||||
|
||||
type NFTContractOwnership struct {
|
||||
Owners []NFTOwner `json:"owners"`
|
||||
Network string `json:"network"`
|
||||
Cursor string `json:"cursor"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
thirdparty.NFTContractOwnershipProvider
|
||||
client *http.Client
|
||||
apiKey string
|
||||
apiKeySecret string
|
||||
IsConnected bool
|
||||
IsConnectedLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewClient(apiKey string, apiKeySecret string) *Client {
|
||||
return &Client{
|
||||
client: &http.Client{Timeout: time.Minute},
|
||||
apiKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Client) doQuery(url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth(o.apiKey, o.apiKeySecret)
|
||||
|
||||
resp, err := o.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (o *Client) IsChainSupported(chainID uint64) bool {
|
||||
switch chainID {
|
||||
case 1, 5, 42161, 11155111:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func infuraOwnershipToCommon(contractAddress common.Address, ownersMap map[common.Address][]NFTOwner) (*thirdparty.NFTContractOwnership, error) {
|
||||
owners := make([]thirdparty.NFTOwner, 0, len(ownersMap))
|
||||
|
||||
for ownerAddress, ownerTokens := range ownersMap {
|
||||
tokenBalances := make([]thirdparty.TokenBalance, 0, len(ownerTokens))
|
||||
|
||||
for _, token := range ownerTokens {
|
||||
tokenBalances = append(tokenBalances, thirdparty.TokenBalance{
|
||||
TokenID: token.TokenID,
|
||||
Balance: token.Amount,
|
||||
})
|
||||
}
|
||||
|
||||
owners = append(owners, thirdparty.NFTOwner{
|
||||
OwnerAddress: ownerAddress,
|
||||
TokenBalances: tokenBalances,
|
||||
})
|
||||
}
|
||||
|
||||
ownership := thirdparty.NFTContractOwnership{
|
||||
ContractAddress: contractAddress,
|
||||
Owners: owners,
|
||||
}
|
||||
|
||||
return &ownership, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchNFTOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.NFTContractOwnership, error) {
|
||||
cursor := ""
|
||||
ownersMap := make(map[common.Address][]NFTOwner)
|
||||
|
||||
for {
|
||||
url := fmt.Sprintf("%s/networks/%d/nfts/%s/owners", baseURL, chainID, contractAddress.String())
|
||||
|
||||
if cursor != "" {
|
||||
url = url + "?cursor=" + cursor
|
||||
}
|
||||
|
||||
resp, err := o.doQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var infuraOwnership NFTContractOwnership
|
||||
err = json.Unmarshal(body, &infuraOwnership)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, infuraOwner := range infuraOwnership.Owners {
|
||||
ownersMap[infuraOwner.OwnerAddress] = append(ownersMap[infuraOwner.OwnerAddress], infuraOwner)
|
||||
}
|
||||
|
||||
cursor = infuraOwnership.Cursor
|
||||
|
||||
if cursor == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return infuraOwnershipToCommon(contractAddress, ownersMap)
|
||||
}
|
|
@ -61,3 +61,23 @@ type NFTMetadataProvider interface {
|
|||
CanProvideNFTMetadata(chainID uint64, id NFTUniqueID, tokenURI string) (bool, error)
|
||||
FetchNFTMetadata(chainID uint64, id NFTUniqueID, tokenURI string) (*NFTMetadata, error)
|
||||
}
|
||||
|
||||
type TokenBalance struct {
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
}
|
||||
|
||||
type NFTOwner struct {
|
||||
OwnerAddress common.Address `json:"ownerAddress"`
|
||||
TokenBalances []TokenBalance `json:"tokenBalances"`
|
||||
}
|
||||
|
||||
type NFTContractOwnership struct {
|
||||
ContractAddress common.Address `json:"contractAddress"`
|
||||
Owners []NFTOwner `json:"owners"`
|
||||
}
|
||||
|
||||
type NFTContractOwnershipProvider interface {
|
||||
FetchNFTOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*NFTContractOwnership, error)
|
||||
IsChainSupported(chainID uint64) bool
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue