feat: fetch NFT metadata from Communities
This commit is contained in:
parent
fae7e8dba5
commit
a1e7eed141
|
@ -473,10 +473,17 @@ func (b *StatusNode) appmetricsService() common.StatusService {
|
|||
|
||||
func (b *StatusNode) walletService(accountsDB *accounts.Database, accountsFeed *event.Feed, openseaAPIKey string) common.StatusService {
|
||||
if b.walletSrvc == nil {
|
||||
var extService *ext.Service
|
||||
if b.WakuV2ExtService() != nil {
|
||||
extService = b.WakuV2ExtService().Service
|
||||
} else if b.WakuExtService() != nil {
|
||||
extService = b.WakuExtService().Service
|
||||
}
|
||||
b.walletSrvc = wallet.NewService(
|
||||
b.appDB, accountsDB, b.rpcClient, accountsFeed, openseaAPIKey, b.gethAccountManager, b.transactor, b.config,
|
||||
b.ensService(),
|
||||
b.stickersService(accountsDB),
|
||||
extService,
|
||||
)
|
||||
}
|
||||
return b.walletSrvc
|
||||
|
|
|
@ -6,9 +6,11 @@ import (
|
|||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
|
@ -24,6 +26,7 @@ import (
|
|||
gethrpc "github.com/ethereum/go-ethereum/rpc"
|
||||
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/api/multiformat"
|
||||
"github.com/status-im/status-go/connection"
|
||||
"github.com/status-im/status-go/db"
|
||||
coretypes "github.com/status-im/status-go/eth-node/core/types"
|
||||
|
@ -34,6 +37,7 @@ import (
|
|||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/protocol"
|
||||
"github.com/status-im/status-go/protocol/anonmetrics"
|
||||
"github.com/status-im/status-go/protocol/common"
|
||||
"github.com/status-im/status-go/protocol/pushnotificationclient"
|
||||
"github.com/status-im/status-go/protocol/pushnotificationserver"
|
||||
"github.com/status-im/status-go/protocol/transport"
|
||||
|
@ -43,6 +47,7 @@ import (
|
|||
"github.com/status-im/status-go/services/ext/mailservers"
|
||||
localnotifications "github.com/status-im/status-go/services/local-notifications"
|
||||
mailserversDB "github.com/status-im/status-go/services/mailservers"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
)
|
||||
|
||||
|
@ -56,6 +61,7 @@ type EnvelopeEventsHandler interface {
|
|||
|
||||
// Service is a service that provides some additional API to whisper-based protocols like Whisper or Waku.
|
||||
type Service struct {
|
||||
thirdparty.NFTMetadataProvider
|
||||
messenger *protocol.Messenger
|
||||
identity *ecdsa.PrivateKey
|
||||
cancelMessenger chan struct{}
|
||||
|
@ -507,3 +513,68 @@ func (s *Service) ConnectionChanged(state connection.State) {
|
|||
func (s *Service) Messenger() *protocol.Messenger {
|
||||
return s.messenger
|
||||
}
|
||||
|
||||
func tokenURIToCommunityID(tokenURI string) string {
|
||||
tmpStr := strings.Split(tokenURI, "/")
|
||||
|
||||
// Community NFTs have a tokenURI of the form "compressedCommunityID/tokenID"
|
||||
if len(tmpStr) != 2 {
|
||||
return ""
|
||||
}
|
||||
compressedCommunityID := tmpStr[0]
|
||||
|
||||
hexCommunityID, err := multiformat.DeserializeCompressedKey(compressedCommunityID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
pubKey, err := common.HexToPubkey(hexCommunityID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
communityID := types.EncodeHex(crypto.CompressPubkey(pubKey))
|
||||
|
||||
return communityID
|
||||
}
|
||||
|
||||
func (s *Service) CanProvideNFTMetadata(chainID uint64, id thirdparty.NFTUniqueID, tokenURI string) (bool, error) {
|
||||
ret := tokenURI != "" && tokenURIToCommunityID(tokenURI) != ""
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *Service) FetchNFTMetadata(chainID uint64, id thirdparty.NFTUniqueID, tokenURI string) (*thirdparty.NFTMetadata, error) {
|
||||
if s.messenger == nil {
|
||||
return nil, fmt.Errorf("messenger not ready")
|
||||
}
|
||||
|
||||
communityID := tokenURIToCommunityID(tokenURI)
|
||||
|
||||
if communityID == "" {
|
||||
return nil, fmt.Errorf("invalid tokenURI")
|
||||
}
|
||||
|
||||
// Try to fetch metadata from Messenger communities
|
||||
community, err := s.messenger.RequestCommunityInfoFromMailserver(communityID, true)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if community != nil {
|
||||
tokensMetadata := community.CommunityTokensMetadata()
|
||||
|
||||
for _, tokenMetadata := range tokensMetadata {
|
||||
contractAddresses := tokenMetadata.GetContractAddresses()
|
||||
if contractAddresses[chainID] == id.ContractAddress.Hex() {
|
||||
return &thirdparty.NFTMetadata{
|
||||
Name: tokenMetadata.GetName(),
|
||||
Description: tokenMetadata.GetDescription(),
|
||||
ImageURL: tokenMetadata.GetImage(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -295,12 +295,7 @@ func (api *API) GetCryptoOnRamps(ctx context.Context) ([]CryptoOnRamp, error) {
|
|||
|
||||
func (api *API) GetOpenseaCollectionsByOwner(ctx context.Context, chainID uint64, owner common.Address) ([]opensea.OwnedCollection, error) {
|
||||
log.Debug("call to get opensea collections")
|
||||
client, err := opensea.NewOpenseaClient(chainID, api.s.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FetchAllCollectionsByOwner(owner)
|
||||
return api.s.collectiblesManager.FetchAllCollectionsByOwner(chainID, owner)
|
||||
}
|
||||
|
||||
// Kept for compatibility with mobile app
|
||||
|
@ -314,43 +309,22 @@ func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, chainI
|
|||
|
||||
func (api *API) GetOpenseaAssetsByOwnerAndCollectionWithCursor(ctx context.Context, chainID uint64, owner common.Address, collectionSlug string, cursor string, limit int) (*opensea.AssetContainer, error) {
|
||||
log.Debug("call to get opensea assets")
|
||||
client, err := opensea.NewOpenseaClient(chainID, api.s.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FetchAllAssetsByOwnerAndCollection(owner, collectionSlug, cursor, limit)
|
||||
return api.s.collectiblesManager.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit)
|
||||
}
|
||||
|
||||
func (api *API) GetOpenseaAssetsByOwnerWithCursor(ctx context.Context, chainID uint64, owner common.Address, cursor string, limit int) (*opensea.AssetContainer, error) {
|
||||
log.Debug("call to FetchAllAssetsByOwner")
|
||||
client, err := opensea.NewOpenseaClient(chainID, api.s.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FetchAllAssetsByOwner(owner, cursor, limit)
|
||||
return api.s.collectiblesManager.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
|
||||
}
|
||||
|
||||
func (api *API) GetOpenseaAssetsByOwnerAndContractAddressWithCursor(ctx context.Context, chainID uint64, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*opensea.AssetContainer, error) {
|
||||
log.Debug("call to GetOpenseaAssetsByOwnerAndContractAddressWithCursor")
|
||||
client, err := opensea.NewOpenseaClient(chainID, api.s.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FetchAllAssetsByOwnerAndContractAddress(owner, contractAddresses, cursor, limit)
|
||||
return api.s.collectiblesManager.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit)
|
||||
}
|
||||
|
||||
func (api *API) GetOpenseaAssetsByNFTUniqueID(ctx context.Context, chainID uint64, uniqueIDs []opensea.NFTUniqueID, limit int) (*opensea.AssetContainer, error) {
|
||||
func (api *API) GetOpenseaAssetsByNFTUniqueID(ctx context.Context, chainID uint64, uniqueIDs []thirdparty.NFTUniqueID, limit int) (*opensea.AssetContainer, error) {
|
||||
log.Debug("call to GetOpenseaAssetsByNFTUniqueID")
|
||||
|
||||
client, err := opensea.NewOpenseaClient(chainID, api.s.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FetchAssetsByNFTUniqueID(uniqueIDs, limit)
|
||||
return api.s.collectiblesManager.FetchAssetsByNFTUniqueID(chainID, uniqueIDs, limit)
|
||||
}
|
||||
|
||||
func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error {
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
package collectibles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/status-im/status-go/contracts/collectibles"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
|
||||
)
|
||||
|
||||
const requestTimeout = 5 * time.Second
|
||||
|
||||
func erc721MetadataInterfaceID() [4]byte {
|
||||
return [...]byte{0x5b, 0x5e, 0x13, 0x9f} // 0x5b5e139f
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
rpcClient *rpc.Client
|
||||
metadataProvider thirdparty.NFTMetadataProvider
|
||||
openseaAPIKey string
|
||||
}
|
||||
|
||||
func NewManager(rpcClient *rpc.Client, metadataProvider thirdparty.NFTMetadataProvider, openseaAPIKey string) *Manager {
|
||||
return &Manager{
|
||||
rpcClient: rpcClient,
|
||||
metadataProvider: metadataProvider,
|
||||
openseaAPIKey: openseaAPIKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAllCollectionsByOwner(chainID uint64, owner common.Address) ([]opensea.OwnedCollection, error) {
|
||||
client, err := opensea.NewOpenseaClient(chainID, o.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.FetchAllCollectionsByOwner(owner)
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner common.Address, collectionSlug string, cursor string, limit int) (*opensea.AssetContainer, error) {
|
||||
client, err := opensea.NewOpenseaClient(chainID, o.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assetContainer, err := client.FetchAllAssetsByOwnerAndCollection(owner, collectionSlug, cursor, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = o.processAssets(chainID, assetContainer.Assets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assetContainer, nil
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*opensea.AssetContainer, error) {
|
||||
client, err := opensea.NewOpenseaClient(chainID, o.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assetContainer, err := client.FetchAllAssetsByOwnerAndContractAddress(owner, contractAddresses, cursor, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = o.processAssets(chainID, assetContainer.Assets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assetContainer, nil
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAllAssetsByOwner(chainID uint64, owner common.Address, cursor string, limit int) (*opensea.AssetContainer, error) {
|
||||
client, err := opensea.NewOpenseaClient(chainID, o.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assetContainer, err := client.FetchAllAssetsByOwner(owner, cursor, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = o.processAssets(chainID, assetContainer.Assets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assetContainer, nil
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAssetsByNFTUniqueID(chainID uint64, uniqueIDs []thirdparty.NFTUniqueID, limit int) (*opensea.AssetContainer, error) {
|
||||
client, err := opensea.NewOpenseaClient(chainID, o.openseaAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assetContainer, err := client.FetchAssetsByNFTUniqueID(uniqueIDs, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = o.processAssets(chainID, assetContainer.Assets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assetContainer, nil
|
||||
}
|
||||
|
||||
func isMetadataEmpty(asset opensea.Asset) bool {
|
||||
return asset.Name == "" &&
|
||||
asset.Description == "" &&
|
||||
asset.ImageURL == "" &&
|
||||
asset.TokenURI == ""
|
||||
}
|
||||
|
||||
func (o *Manager) supportsERC721Metadata(chainID uint64, contractAddress common.Address) (bool, error) {
|
||||
backend, err := o.rpcClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
caller, err := collectibles.NewCollectiblesCaller(contractAddress, backend)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
timeoutContext, timeoutCancel := context.WithTimeout(context.Background(), requestTimeout)
|
||||
defer timeoutCancel()
|
||||
|
||||
supports, err := caller.SupportsInterface(&bind.CallOpts{
|
||||
Context: timeoutContext,
|
||||
}, erc721MetadataInterfaceID())
|
||||
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), vm.ErrExecutionReverted.Error()) {
|
||||
// Contract doesn't support "SupportsInterface"
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return supports, nil
|
||||
}
|
||||
|
||||
func (o *Manager) fetchTokenURI(chainID uint64, id thirdparty.NFTUniqueID) (string, error) {
|
||||
backend, err := o.rpcClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
caller, err := collectibles.NewCollectiblesCaller(id.ContractAddress, backend)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
timeoutContext, timeoutCancel := context.WithTimeout(context.Background(), requestTimeout)
|
||||
defer timeoutCancel()
|
||||
|
||||
return caller.TokenURI(&bind.CallOpts{
|
||||
Context: timeoutContext,
|
||||
}, id.TokenID.Int)
|
||||
}
|
||||
|
||||
func (o *Manager) processAssets(chainID uint64, assets []opensea.Asset) error {
|
||||
for idx, asset := range assets {
|
||||
if isMetadataEmpty(asset) {
|
||||
id := thirdparty.NFTUniqueID{
|
||||
ContractAddress: common.HexToAddress(asset.Contract.Address),
|
||||
TokenID: asset.TokenID,
|
||||
}
|
||||
|
||||
supportsERC721Metadata, err := o.supportsERC721Metadata(chainID, id.ContractAddress)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if supportsERC721Metadata {
|
||||
tokenURI, err := o.fetchTokenURI(chainID, id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assets[idx].TokenURI = tokenURI
|
||||
|
||||
canProvide, err := o.metadataProvider.CanProvideNFTMetadata(chainID, id, tokenURI)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if canProvide {
|
||||
metadata, err := o.metadataProvider.FetchNFTMetadata(chainID, id, tokenURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if metadata != nil {
|
||||
assets[idx].Name = metadata.Name
|
||||
assets[idx].Description = metadata.Description
|
||||
assets[idx].Collection.ImageURL = metadata.ImageURL
|
||||
assets[idx].ImageURL = metadata.ImageURL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -16,9 +16,11 @@ import (
|
|||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/services/ens"
|
||||
"github.com/status-im/status-go/services/stickers"
|
||||
"github.com/status-im/status-go/services/wallet/collectibles"
|
||||
"github.com/status-im/status-go/services/wallet/currency"
|
||||
"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/coingecko"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/cryptocompare"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
|
||||
|
@ -51,6 +53,7 @@ func NewService(
|
|||
config *params.NodeConfig,
|
||||
ens *ens.Service,
|
||||
stickers *stickers.Service,
|
||||
nftMetadataProvider thirdparty.NFTMetadataProvider,
|
||||
) *Service {
|
||||
cryptoOnRampManager := NewCryptoOnRampManager(&CryptoOnRampOptions{
|
||||
dataSourceType: DataSourceStatic,
|
||||
|
@ -69,6 +72,7 @@ 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)
|
||||
return &Service{
|
||||
db: db,
|
||||
accountsDB: accountsDB,
|
||||
|
@ -78,7 +82,7 @@ func NewService(
|
|||
transactionManager: transactionManager,
|
||||
transferController: transferController,
|
||||
cryptoOnRampManager: cryptoOnRampManager,
|
||||
openseaAPIKey: openseaAPIKey,
|
||||
collectiblesManager: collectiblesManager,
|
||||
feesManager: &FeeManager{rpcClient},
|
||||
gethManager: gethManager,
|
||||
marketManager: marketManager,
|
||||
|
@ -106,7 +110,7 @@ type Service struct {
|
|||
feesManager *FeeManager
|
||||
marketManager *market.Manager
|
||||
started bool
|
||||
openseaAPIKey string
|
||||
collectiblesManager *collectibles.Manager
|
||||
gethManager *account.GethManager
|
||||
transactor *transactions.Transactor
|
||||
ens *ens.Service
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/log"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
const AssetLimit = 200
|
||||
|
@ -36,11 +37,6 @@ const ChainIDRequiringAPIKey = 1
|
|||
|
||||
type TraitValue string
|
||||
|
||||
type NFTUniqueID struct {
|
||||
ContractAddress common.Address `json:"contractAddress"`
|
||||
TokenID bigint.BigInt `json:"tokenID"`
|
||||
}
|
||||
|
||||
func (st *TraitValue) UnmarshalJSON(b []byte) error {
|
||||
var item interface{}
|
||||
if err := json.Unmarshal(b, &item); err != nil {
|
||||
|
@ -109,6 +105,7 @@ type Asset struct {
|
|||
LastSale LastSale `json:"last_sale"`
|
||||
SellOrders []SellOrder `json:"sell_orders"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
TokenURI string `json:"token_metadata"`
|
||||
}
|
||||
|
||||
type CollectionTrait struct {
|
||||
|
@ -249,7 +246,7 @@ func (o *Client) FetchAllAssetsByOwner(owner common.Address, cursor string, limi
|
|||
return o.fetchAssets(queryParams, limit)
|
||||
}
|
||||
|
||||
func (o *Client) FetchAssetsByNFTUniqueID(uniqueIDs []NFTUniqueID, limit int) (*AssetContainer, error) {
|
||||
func (o *Client) FetchAssetsByNFTUniqueID(uniqueIDs []thirdparty.NFTUniqueID, limit int) (*AssetContainer, error) {
|
||||
queryParams := url.Values{}
|
||||
|
||||
for _, uniqueID := range uniqueIDs {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package thirdparty
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
)
|
||||
|
||||
type HistoricalPrice struct {
|
||||
Timestamp int64 `json:"time"`
|
||||
Value float64 `json:"close"`
|
||||
|
@ -35,3 +40,19 @@ type MarketDataProvider interface {
|
|||
FetchTokenMarketValues(symbols []string, currency string) (map[string]TokenMarketValues, error)
|
||||
FetchTokenDetails(symbols []string) (map[string]TokenDetails, error)
|
||||
}
|
||||
|
||||
type NFTUniqueID struct {
|
||||
ContractAddress common.Address `json:"contractAddress"`
|
||||
TokenID *bigint.BigInt `json:"tokenID"`
|
||||
}
|
||||
|
||||
type NFTMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ImageURL string `json:"image"`
|
||||
}
|
||||
|
||||
type NFTMetadataProvider interface {
|
||||
CanProvideNFTMetadata(chainID uint64, id NFTUniqueID, tokenURI string) (bool, error)
|
||||
FetchNFTMetadata(chainID uint64, id NFTUniqueID, tokenURI string) (*NFTMetadata, error)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue