@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
JavaScript
;
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;