UNPKG

@cross-nft-marketplace/auction-house-nft-hooks

Version:

Generic react hooks for fetching cross-nft-marketplace auctions, nfts, and data on arbitary 721s. Powers nft-components.

446 lines (445 loc) 21.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MediaFetchAgent = void 0; const tslib_1 = require("tslib"); const DataLoader_1 = require("./DataLoader"); const graphql_request_1 = require("graphql-request"); const address_1 = require("@ethersproject/address"); const RequestError_1 = require("./RequestError"); const urls_1 = require("../constants/urls"); const EnsReverseFetcher_1 = require("./EnsReverseFetcher"); const networks_1 = require("../constants/networks"); const zora_graph_1 = require("../graph-queries/zora-graph"); const uniswap_1 = require("../graph-queries/uniswap"); const timeouts_1 = require("../constants/timeouts"); const TransformFetchResults_1 = require("./TransformFetchResults"); const FetchWithTimeout_1 = require("./FetchWithTimeout"); const OpenseaUtils_1 = require("./OpenseaUtils"); const ErrorUtils_1 = require("./ErrorUtils"); const UriUtils_1 = require("./UriUtils"); const zora_indexer_1 = require("../graph-queries/zora-indexer"); const ethcall_1 = require("ethcall"); const etherProviders = tslib_1.__importStar(require("@ethersproject/providers")); const apiKeys_1 = require("../constants/apiKeys"); const erc721abi_1 = require("../constants/erc721abi"); const addresses_1 = require("../constants/addresses"); const zdk_1 = require("@zoralabs/zdk"); const atob_1 = require("../utils/atob"); /** * Internal agent for NFT Hooks to fetch NFT information. * Can be used directly for interaction with non-react web frameworks or server frameworks. * Uses a cached promise-based API. * Fetches from IPFS providers and thegraph. */ class MediaFetchAgent { constructor(props_ = {}) { this.timeouts = timeouts_1.DEFAULT_NETWORK_TIMEOUTS_MS; this.networkId = props_.network || '1'; this.infuraApiKey = props_.infuraApiKey || apiKeys_1.DEFAULT_INFURA_API_KEY; const DEFAULT_BATCH_TIMEOUT_MS = 15; const batchScheduleFn = (callback) => setTimeout(callback, props_.batchScheduleTimeoutMs || DEFAULT_BATCH_TIMEOUT_MS); this.loaders = { currencyLoader: new DataLoader_1.DataLoader((keys) => this.fetchCurrenciesGraph(keys), { cache: false, maxBatchSize: 30, batchScheduleFn: batchScheduleFn }), zoraNFTIndexerLoader: new DataLoader_1.DataLoader((keys) => this.fetchZoraNFTIndexerNFTs(keys), { cache: false, maxBatchSize: 500, batchScheduleFn: batchScheduleFn }), zoraNFTIndexerV2Loader: new DataLoader_1.DataLoader((keys) => this.fetchZoraNFTIndexerV2NFTs(keys), { cache: false, maxBatchSize: 50, batchScheduleFn: batchScheduleFn }), genericNFTLoader: new DataLoader_1.DataLoader((keys) => this.fetchGenericNFT(keys), { cache: false, maxBatchSize: 30, batchScheduleFn: batchScheduleFn }), blockchainNFTLoader: new DataLoader_1.DataLoader((keys) => this.fetchBlockchainGenericNFT(keys), { cache: false, maxBatchSize: 100, batchScheduleFn: batchScheduleFn }), ensLoader: new DataLoader_1.DataLoader((keys) => this.loadEnsBatch(keys), { maxBatchSize: 100, batchScheduleFn: batchScheduleFn }), auctionInfoLoader: new DataLoader_1.DataLoader((keys) => this.fetchAuctionNFTInfo(keys), { cache: false, maxBatchSize: 300, batchScheduleFn: batchScheduleFn }) }; this.zdk = new zdk_1.ZDK({ endpoint: urls_1.ZORA_V2_INDEXER, apiKey: props_.zdkApiKey, networks: [ { chain: this.networkId == networks_1.Networks.MAINNET ? zdk_1.ZDKChain.Mainnet : zdk_1.ZDKChain.Rinkeby, network: zdk_1.ZDKNetwork.Ethereum, } ] }); } /** * Clear all cached responses from metadata, currency, and NFT chain information loaders */ clearCache() { Object.values(this.loaders).forEach((loader) => loader.clearAll()); } /** * Gets information of currencies and trading prices from uniswap * @param currencies list of currency contract ids on ethereum * @returns Promise<CurrencyLookupType> */ async loadCurrencies(currencies) { const results = await this.loaders.currencyLoader.loadMany(currencies); return results.reduce((last, result) => { if (!(result instanceof Error)) { last[result.token.id] = result; } return last; }, {}); } /** * Fetch NFT content or retun URI if content shouild not be fetched * @param url NFT Content URL * @param contentType string mime type to fetch * @returns Promise<MediaContentType> Media content information or URL */ async fetchContent(url, contentType) { if (contentType.startsWith('text/')) { try { const response = await new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.IPFS).fetch(UriUtils_1.convertURIToHTTPS(url)); return { text: await response.text(), type: 'text', mimeType: contentType, }; } catch (e) { throw new RequestError_1.RequestError('Issue fetching IPFS data', e); } } return { uri: url, type: 'uri', mimeType: contentType }; } /** * Fetch Content MIME type from content URI * * @param url IPFS Content URI * @returns mime type as a string * @throws RequestError */ async fetchContentMimeType(url) { const response = await new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.IPFS).fetch(UriUtils_1.convertURIToHTTPS(url), { method: 'HEAD', }); const header = response.headers.get('content-type'); if (!header) { throw new RequestError_1.RequestError('No content type returned for URI'); } return header; } async loadEnsBatch(addresses) { const addressToNames = await EnsReverseFetcher_1.reverseResolveEnsAddresses(addresses, this.networkId, this.timeouts.Rpc); return addresses.map((address) => addressToNames[address] || Error('Not found')); } // Alpha: uses zora indexer // format CONTRACT_ID-TOKEN_ID async fetchZoraNFTIndexerNFTs(keys) { const fetchWithTimeout = new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.ZoraIndexer); const client = new graphql_request_1.GraphQLClient(urls_1.ZORA_INDEXER_URL_BY_NETWORK[this.networkId], { fetch: fetchWithTimeout.fetch, }); const response = await client.request(zora_indexer_1.BY_IDS, { ids: keys, }); return keys.map((key) => response.Token.find((token) => token.id === key) || new ErrorUtils_1.NotFoundError('Did not find token')); } async fetchZoraNFTIndexerV2NFTs(keys) { let tokenInput = keys .map((address) => address.split('-')) .map(([contractAddress, tokenId]) => { return { address: contractAddress, tokenId: tokenId }; }); let res = await this.zdk.tokens({ where: { tokens: tokenInput }, includeSalesHistory: false, includeFullDetails: true }); let tokens = keys.map((key) => { let [contractAddress, tokenId] = key.split('-'); return res.tokens.nodes.find((token) => { var _a; return ((_a = token.token.tokenContract) === null || _a === void 0 ? void 0 : _a.collectionAddress.toLowerCase()) === contractAddress.toLowerCase() && token.token.tokenId == tokenId; }) || new ErrorUtils_1.NotFoundError('Did not find token'); }); return tokens; } async loadZoraNFTIndexerNFTUntransformed(contractAddress, tokenId) { return this.loaders.zoraNFTIndexerLoader.load(`${address_1.getAddress(contractAddress)}-${tokenId}`); } async loadZoraNFTIndexerV2NFTUntransformed(contractAddress, tokenId) { return this.loaders.zoraNFTIndexerV2Loader.load(`${address_1.getAddress(contractAddress)}-${tokenId}`); } async loadNFTData(contractAddress, tokenId, auctionData, currencyData) { const contractAndToken = `${contractAddress.toLowerCase()}:${tokenId}`; const nftInfo = await this.loaders.genericNFTLoader.load(contractAndToken); if (!auctionData) { try { auctionData = await this.loadAuctionInfo(contractAddress, tokenId); } catch (err) { if (!(err instanceof ErrorUtils_1.NotFoundError)) { // Log any not-found error console.error(err); } } } if (!nftInfo) { throw new RequestError_1.RequestError('Cannot fetch NFT information'); } return OpenseaUtils_1.transformOpenseaResponse(nftInfo, auctionData, currencyData); } async loadNFTDataUntransformed(contractAddress, tokenId) { const contractAndToken = `${contractAddress.toLowerCase()}:${tokenId}`; const nftInfo = await this.loaders.genericNFTLoader.load(contractAndToken); if (!nftInfo) { throw new RequestError_1.RequestError('Cannot fetch NFT information'); } return nftInfo; } async loadBlockchainNFTDataUntransformed(contractAddress, tokenId) { const contractAndToken = `${contractAddress.toLowerCase()}:${tokenId}`; const nftInfo = await this.loaders.blockchainNFTLoader.load(contractAndToken); if (!nftInfo) { throw new RequestError_1.RequestError('Cannot fetch NFT information'); } return nftInfo; } async loadAuctionInfo(tokenContract, tokenId) { return await this.loaders.auctionInfoLoader.load([tokenContract.toLowerCase(), tokenId].join('-')); } // use dash between lowercase contract id and token id async loadAuctionInfos(tokenContractAndIds) { return await this.loaders.auctionInfoLoader.loadMany(tokenContractAndIds); } async loadEnsName(address) { return this.loaders.ensLoader.load(address.toLowerCase()); } /** * Fetch function to retrieve Graph data for matching curated auctions * This function is not cached * * @function fetchReserveAuctions * @private * @param curatorIds list of Zora NFT IDs to fetch from the graph datastore * @returns mapped transformed list of curated auction results */ async fetchReserveAuctions(curatorIds, isApproved = null, first = 1000, skip = 0) { const fetchWithTimeout = new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.Graph); const client = new graphql_request_1.GraphQLClient(urls_1.THEGRAPH_API_URL_BY_NETWORK[this.networkId], { fetch: fetchWithTimeout.fetch, }); let query = zora_graph_1.GET_ALL_AUCTIONS; if (curatorIds.length) { query = zora_graph_1.GET_AUCTION_BY_CURATOR; } const response = (await client.request(query, { curators: curatorIds.length ? curatorIds.map(curator => curator.toLowerCase()) : undefined, first: first, skip: skip, approved: isApproved === null ? [true, false] : [isApproved], })); return response.reserveAuctions; } /** * Fetch function to retrieve Graph data for matching curated auctions * This function is not cached * * @function fetchReserveAuctionsByOwner * @private * @param curatorIds list of Zora NFT IDs to fetch from the graph datastore * @returns mapped transformed list of curated auction results */ async fetchReserveAuctionsByOwner(owner, curatorIds, isApproved = null, first = 1000, skip = 0) { const fetchWithTimeout = new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.Graph); const client = new graphql_request_1.GraphQLClient(urls_1.THEGRAPH_API_URL_BY_NETWORK[this.networkId], { fetch: fetchWithTimeout.fetch, }); let query = zora_graph_1.GET_AUCTION_BY_TOKEN_OWNER; const response = (await client.request(query, { curators: curatorIds.length ? curatorIds.map(curator => curator.toLowerCase()) : undefined, owner: owner.toLowerCase(), first: first, skip: skip, approved: isApproved === null ? [true, false] : [isApproved], })); return response.reserveAuctions; } async fetchAuctionNFTInfo(tokenAndAddresses) { const fetchWithTimeout = new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.Graph); const client = new graphql_request_1.GraphQLClient(urls_1.THEGRAPH_API_URL_BY_NETWORK[this.networkId], { fetch: fetchWithTimeout.fetch, }); const response = (await client.request(zora_graph_1.GET_AUCTION_BY_MEDIA, { tokens: tokenAndAddresses.map((tokenAndAddress) => tokenAndAddress.toLowerCase()), })); if (!response.reserveAuctions) { throw new RequestError_1.RequestError('Missing auction in reponse'); } return tokenAndAddresses.map((tokenAndAddress) => response.reserveAuctions.find((auction) => auction.token === tokenAndAddress) || new ErrorUtils_1.NotFoundError('Missing Auction')); } /** * Fetches generic NFT information * * @param nftAddresses list of addresses in a 0xcontractid:tokenid format * @returns */ async fetchGenericNFT(nftAddresses) { const fetchWithTimeout = new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.OpenSea); const apiBase = urls_1.OPENSEA_API_URL_BY_NETWORK[this.networkId]; const urlParams = []; nftAddresses .map((address) => address.split(':')) .forEach(([address, tokenId]) => { urlParams.push(`token_ids=${tokenId}&asset_contract_addresses=${address}`); }); const response = await fetchWithTimeout.fetch(`${apiBase}assets?${urlParams.join('&')}&order_direction=desc&limit=50`); const responseJson = await response.json(); return nftAddresses.map((nftAddress) => OpenseaUtils_1.transformGenericNFTForKey(responseJson.assets, nftAddress)); } async fetchBlockchainGenericNFT(nftAddresses) { //todo support client provider //todo support fallbackProvider const ethcallProvider = new ethcall_1.Provider(); const provider = new etherProviders.InfuraProvider(Number(this.networkId), this.infuraApiKey); await ethcallProvider.init(provider); let calls = []; for (let addressWithTokenId of nftAddresses) { let [address, tokenId] = addressWithTokenId.split(':'); let erc721Contract = new ethcall_1.Contract(address, erc721abi_1.ERC721_ABI); if (address.toLowerCase() == addresses_1.ZORA_MEDIA_CONTRACT_BY_NETWORK[this.networkId].toLowerCase()) { let mediaContract = new ethcall_1.Contract(address, erc721abi_1.ZORA_MEDIA_METADATA_URI_ABI); let metadatacall = mediaContract.tokenMetadataURI(tokenId); calls.push(metadatacall); } let ownerOfCall = erc721Contract.ownerOf(tokenId); calls.push(ownerOfCall); let erc721MetadataContract = new ethcall_1.Contract(address, erc721abi_1.ERC721_METADATA_ABI); let tokenUriCall = erc721MetadataContract.tokenURI(tokenId); calls.push(tokenUriCall); //todo optimize, group name and symbol per address. //todo possible remove this, currently this info not used in components let nameCall = erc721MetadataContract.name(); calls.push(nameCall); let symbolCall = erc721MetadataContract.symbol(); calls.push(symbolCall); } const data = await ethcallProvider.tryAll(calls); let callIndex = 0; let result = nftAddresses.map((addressWithTokenId) => { var _a, _b, _c, _d, _e; let [address, tokenId] = addressWithTokenId.split(':'); let zoraMetadataUri = undefined; let isZoraMedia = address.toLowerCase() == addresses_1.ZORA_MEDIA_CONTRACT_BY_NETWORK[this.networkId].toLowerCase(); if (isZoraMedia) { zoraMetadataUri = (_a = data[callIndex++]) === null || _a === void 0 ? void 0 : _a.toString(); } let owner = (_b = data[callIndex++]) === null || _b === void 0 ? void 0 : _b.toString(); if (owner == undefined) { return new ErrorUtils_1.NotFoundError('Did not find token'); } let tokenUri = (_c = data[callIndex++]) === null || _c === void 0 ? void 0 : _c.toString(); let name = (_d = data[callIndex++]) === null || _d === void 0 ? void 0 : _d.toString(); let symbol = (_e = data[callIndex++]) === null || _e === void 0 ? void 0 : _e.toString(); let response = { address: address, token_id: tokenId, zoraMetadataUri: zoraMetadataUri, contract_name: name, contract_symbol: symbol, owner: owner, uri: tokenUri, }; return response; }); return result; } /** * Internal fetch function to retrieve currency information from TheGraph * * @function fetchCurrenciesGraph * @private * @param currencyContracts list of Ethereum addresses of currency contract data to retrieve * @returns mapped transformed list of ETH currency mapping data */ async fetchCurrenciesGraph(currencyContracts) { const fetchWithTimeout = new FetchWithTimeout_1.FetchWithTimeout(this.timeouts.Graph); const client = new graphql_request_1.GraphQLClient(urls_1.THEGRAPH_UNISWAP_URL_BY_NETWORK[this.networkId], { fetch: fetchWithTimeout.fetch, }); const currencies = (await client.request(uniswap_1.GET_TOKEN_VALUES_QUERY, { currencyContracts: currencyContracts.filter((contract) => contract !== TransformFetchResults_1.NULL_ETH_CURRENCY_ID), })); return currencyContracts.map((key) => TransformFetchResults_1.transformCurrencyForKey(currencies, key)); } /** * Fetch method to query metadata from IPFS. Not cached * * @function fetchIPFSMetadataCached * @public * @param url Metadata Source * @returns IPFS Metadata Fetch * @throws RequestError */ async fetchIPFSMetadata(url) { var _a, _b; if (url.startsWith("data:application/json;base64,")) { let byteString = url.split(',')[1]; try { let metadata = JSON.parse(atob_1.universalAtob(byteString)); return metadata; } catch (error) { throw new RequestError_1.RequestError("Fail load metadata", error); } } // TODO(iain): Properly parse metadata from `ourzora/media-metadata-schemas` try { const metadataRaw = await FetchWithTimeout_1.fetchEx(UriUtils_1.convertURIToHTTPS(url), { responseMaxLimitInBytes: 100000, responseRequiredContentType: 'application/json', timeoutMs: this.timeouts.IPFS }); try { return JSON.parse(metadataRaw); } catch (e) { throw new RequestError_1.RequestError('Cannot read JSON metadata from IPFS'); } } catch (error) { if (error instanceof FetchWithTimeout_1.FetchDifferentContentTypeError) { if ((_a = error.actualContentType) === null || _a === void 0 ? void 0 : _a.startsWith("video/")) { return { _mimeType: error.actualContentType, animation_url: url }; } if ((_b = error.actualContentType) === null || _b === void 0 ? void 0 : _b.startsWith("image/") /* || error.actualContentType == "application/svg+xml"*/) { return { _mimeType: error.actualContentType, image: url }; } } throw new RequestError_1.RequestError("Fail load metadata", error); } } } exports.MediaFetchAgent = MediaFetchAgent;