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 {
|
if len(openseaKey) == 0 {
|
||||||
openseaKey = OpenseaKeyFromEnv
|
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)
|
services = append(services, walletService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,7 +471,7 @@ func (b *StatusNode) appmetricsService() common.StatusService {
|
||||||
return b.appMetricsSrvc
|
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 {
|
if b.walletSrvc == nil {
|
||||||
var extService *ext.Service
|
var extService *ext.Service
|
||||||
if b.WakuV2ExtService() != nil {
|
if b.WakuV2ExtService() != nil {
|
||||||
|
@ -480,7 +480,7 @@ func (b *StatusNode) walletService(accountsDB *accounts.Database, accountsFeed *
|
||||||
extService = b.WakuExtService().Service
|
extService = b.WakuExtService().Service
|
||||||
}
|
}
|
||||||
b.walletSrvc = wallet.NewService(
|
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.ensService(),
|
||||||
b.stickersService(accountsDB),
|
b.stickersService(accountsDB),
|
||||||
extService,
|
extService,
|
||||||
|
|
|
@ -515,8 +515,11 @@ type Network struct {
|
||||||
|
|
||||||
// WalletConfig extra configuration for wallet.Service.
|
// WalletConfig extra configuration for wallet.Service.
|
||||||
type WalletConfig struct {
|
type WalletConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
OpenseaAPIKey string `json:"OpenseaAPIKey"`
|
OpenseaAPIKey string `json:"OpenseaAPIKey"`
|
||||||
|
AlchemyAPIKeys map[uint64]string `json:"AlchemyAPIKeys"`
|
||||||
|
InfuraAPIKey string `json:"InfuraAPIKey"`
|
||||||
|
InfuraAPIKeySecret string `json:"InfuraAPIKeySecret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalNotificationsConfig extra configuration for localnotifications.Service.
|
// 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)
|
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 {
|
func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error {
|
||||||
log.Debug("call to AddEthereumChain")
|
log.Debug("call to AddEthereumChain")
|
||||||
return api.s.rpcClient.NetworkManager.Upsert(&network)
|
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"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/afex/hystrix-go/hystrix"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
|
@ -17,22 +19,64 @@ import (
|
||||||
|
|
||||||
const requestTimeout = 5 * time.Second
|
const requestTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
const hystrixContractOwnershipClientName = "contractOwnershipClient"
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
rpcClient *rpc.Client
|
rpcClient *rpc.Client
|
||||||
metadataProvider thirdparty.NFTMetadataProvider
|
mainContractOwnershipProvider thirdparty.NFTContractOwnershipProvider
|
||||||
openseaAPIKey string
|
fallbackContractOwnershipProvider thirdparty.NFTContractOwnershipProvider
|
||||||
nftCache map[uint64]map[string]opensea.Asset
|
metadataProvider thirdparty.NFTMetadataProvider
|
||||||
nftCacheLock sync.RWMutex
|
openseaAPIKey string
|
||||||
walletFeed *event.Feed
|
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{
|
return &Manager{
|
||||||
rpcClient: rpcClient,
|
rpcClient: rpcClient,
|
||||||
metadataProvider: metadataProvider,
|
mainContractOwnershipProvider: mainContractOwnershipProvider,
|
||||||
openseaAPIKey: openseaAPIKey,
|
fallbackContractOwnershipProvider: fallbackContractOwnershipProvider,
|
||||||
nftCache: make(map[uint64]map[string]opensea.Asset),
|
metadataProvider: metadataProvider,
|
||||||
walletFeed: walletFeed,
|
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
|
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 {
|
func isMetadataEmpty(asset opensea.Asset) bool {
|
||||||
return asset.Name == "" &&
|
return asset.Name == "" &&
|
||||||
asset.Description == "" &&
|
asset.Description == "" &&
|
||||||
|
|
|
@ -22,8 +22,10 @@ import (
|
||||||
"github.com/status-im/status-go/services/wallet/history"
|
"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/market"
|
||||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
"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/coingecko"
|
||||||
"github.com/status-im/status-go/services/wallet/thirdparty/cryptocompare"
|
"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/token"
|
||||||
"github.com/status-im/status-go/services/wallet/transfer"
|
"github.com/status-im/status-go/services/wallet/transfer"
|
||||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||||
|
@ -41,6 +43,9 @@ func NewService(
|
||||||
rpcClient *rpc.Client,
|
rpcClient *rpc.Client,
|
||||||
accountFeed *event.Feed,
|
accountFeed *event.Feed,
|
||||||
openseaAPIKey string,
|
openseaAPIKey string,
|
||||||
|
alchemyAPIKeys map[uint64]string,
|
||||||
|
infuraAPIKey string,
|
||||||
|
infuraAPIKeySecret string,
|
||||||
gethManager *account.GethManager,
|
gethManager *account.GethManager,
|
||||||
transactor *transactions.Transactor,
|
transactor *transactions.Transactor,
|
||||||
config *params.NodeConfig,
|
config *params.NodeConfig,
|
||||||
|
@ -92,7 +97,10 @@ func NewService(
|
||||||
reader := NewReader(rpcClient, tokenManager, marketManager, accountsDB, walletFeed)
|
reader := NewReader(rpcClient, tokenManager, marketManager, accountsDB, walletFeed)
|
||||||
history := history.NewService(db, walletFeed, rpcClient, tokenManager, marketManager)
|
history := history.NewService(db, walletFeed, rpcClient, tokenManager, marketManager)
|
||||||
currency := currency.NewService(db, walletFeed, 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{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
accountsDB: accountsDB,
|
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)
|
CanProvideNFTMetadata(chainID uint64, id NFTUniqueID, tokenURI string) (bool, error)
|
||||||
FetchNFTMetadata(chainID uint64, id NFTUniqueID, tokenURI string) (*NFTMetadata, 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