@@ -19,7 +19,7 @@ import {
1919 UndiciCauseTypeError ,
2020} from './errors' ;
2121import { RetryableJobError } from '../queue/errors' ;
22- import { normalizeImageUri , processImageCache } from '../images/image-cache' ;
22+ import { processImageCache } from '../images/image-cache' ;
2323import {
2424 RawMetadataLocale ,
2525 RawMetadataLocalizationCType ,
@@ -40,6 +40,22 @@ const METADATA_FETCH_HTTP_AGENT = new Agent({
4040 } ,
4141} ) ;
4242
43+ /**
44+ * A metadata URL that was analyzed and normalized into a fetchable URL. Specifies the URL, the
45+ * gateway type, and any extra headers that may be required to fetch the metadata.
46+ */
47+ export type FetchableMetadataUrl = {
48+ url : URL ;
49+ gateway : 'ipfs' | 'arweave' | null ;
50+ fetchHeaders ?: Record < string , string > ;
51+ } ;
52+
53+ /**
54+ * List of public IPFS gateways that will be replaced with the value of `ENV.PUBLIC_GATEWAY_IPFS`
55+ * whenever a metadata URL has these gateways hard coded in `http:` or `https:` URLs.
56+ */
57+ const PUBLIC_GATEWAY_IPFS_REPLACED = ENV . PUBLIC_GATEWAY_IPFS_REPLACED . split ( ',' ) ;
58+
4359/**
4460 * Fetches all the localized metadata JSONs for a token. First, it downloads the default metadata
4561 * JSON and parses it looking for other localizations. If those are found, each of them is then
@@ -172,9 +188,8 @@ async function parseMetadataForInsertion(
172188 let cachedImage : string | undefined ;
173189 let cachedThumbnailImage : string | undefined ;
174190 if ( image && typeof image === 'string' && ENV . IMAGE_CACHE_PROCESSOR_ENABLED ) {
175- const normalizedUrl = normalizeImageUri ( image ) ;
176191 [ cachedImage , cachedThumbnailImage ] = await processImageCache (
177- normalizedUrl ,
192+ image ,
178193 contract . principal ,
179194 token . token_number
180195 ) ;
@@ -243,14 +258,16 @@ async function parseMetadataForInsertion(
243258export async function fetchMetadata (
244259 httpUrl : URL ,
245260 contract_principal : string ,
246- token_number : bigint
261+ token_number : bigint ,
262+ headers ?: Record < string , string >
247263) : Promise < string | undefined > {
248264 const url = httpUrl . toString ( ) ;
249265 try {
250266 logger . info ( `MetadataFetch for ${ contract_principal } #${ token_number } from ${ url } ` ) ;
251267 const result = await request ( url , {
252268 method : 'GET' ,
253269 throwOnError : true ,
270+ headers,
254271 dispatcher :
255272 // Disable during tests so we can inject a global mock agent.
256273 process . env . NODE_ENV === 'test' ? undefined : METADATA_FETCH_HTTP_AGENT ,
@@ -304,10 +321,13 @@ export async function getMetadataFromUri(
304321 return parseJsonMetadata ( token_uri , content ) ;
305322 }
306323
307- // Support HTTP/S URLs otherwise
308- const httpUrl = getFetchableDecentralizedStorageUrl ( token_uri ) ;
324+ // Support HTTP/S URLs otherwise.
325+ // Transform the URL to use a public gateway if necessary.
326+ const { url : httpUrl , fetchHeaders } = getFetchableMetadataUrl ( token_uri ) ;
309327 const urlStr = httpUrl . toString ( ) ;
310- const content = await fetchMetadata ( httpUrl , contract_principal , token_number ) ;
328+
329+ // Fetch the metadata.
330+ const content = await fetchMetadata ( httpUrl , contract_principal , token_number , fetchHeaders ) ;
311331 return parseJsonMetadata ( urlStr , content ) ;
312332}
313333
@@ -332,31 +352,55 @@ function parseJsonMetadata(url: string, content?: string): RawMetadata {
332352
333353/**
334354 * Helper method for creating http/s url for supported protocols.
335- * * URLs with `http` or `https` protocols are returned as-is.
355+ * * URLs with `http` or `https` protocols are returned as-is. But if they are public IPFS gateways,
356+ * they are replaced with `ENV.PUBLIC_GATEWAY_IPFS`.
336357 * * URLs with `ipfs` or `ipns` protocols are returned with as an `https` url using a public IPFS
337358 * gateway.
338359 * * URLs with `ar` protocols are returned as `https` using a public Arweave gateway.
339360 * @param uri - URL to convert
340361 * @returns Fetchable URL
341362 */
342- export function getFetchableDecentralizedStorageUrl ( uri : string ) : URL {
363+ export function getFetchableMetadataUrl ( uri : string ) : FetchableMetadataUrl {
343364 try {
344365 const parsedUri = new URL ( uri ) ;
345- if ( parsedUri . protocol === 'http:' || parsedUri . protocol === 'https:' ) return parsedUri ;
346- if ( parsedUri . protocol === 'ipfs:' ) {
366+ const result : FetchableMetadataUrl = {
367+ url : parsedUri ,
368+ gateway : null ,
369+ fetchHeaders : undefined ,
370+ } ;
371+ if ( parsedUri . protocol === 'http:' || parsedUri . protocol === 'https:' ) {
372+ // If this is a known public IPFS gateway, replace it with `ENV.PUBLIC_GATEWAY_IPFS`.
373+ if ( PUBLIC_GATEWAY_IPFS_REPLACED . includes ( parsedUri . hostname ) ) {
374+ result . url = new URL ( `${ ENV . PUBLIC_GATEWAY_IPFS } ${ parsedUri . pathname } ` ) ;
375+ result . gateway = 'ipfs' ;
376+ } else {
377+ result . url = parsedUri ;
378+ }
379+ } else if ( parsedUri . protocol === 'ipfs:' ) {
347380 const host = parsedUri . host === 'ipfs' ? 'ipfs' : `ipfs/${ parsedUri . host } ` ;
348- return new URL ( `${ ENV . PUBLIC_GATEWAY_IPFS } /${ host } ${ parsedUri . pathname } ` ) ;
349- }
350- if ( parsedUri . protocol === 'ipns:' ) {
351- return new URL ( `${ ENV . PUBLIC_GATEWAY_IPFS } /${ parsedUri . host } ${ parsedUri . pathname } ` ) ;
381+ result . url = new URL ( `${ ENV . PUBLIC_GATEWAY_IPFS } /${ host } ${ parsedUri . pathname } ` ) ;
382+ result . gateway = 'ipfs' ;
383+ } else if ( parsedUri . protocol === 'ipns:' ) {
384+ result . url = new URL ( `${ ENV . PUBLIC_GATEWAY_IPFS } /${ parsedUri . host } ${ parsedUri . pathname } ` ) ;
385+ result . gateway = 'ipfs' ;
386+ } else if ( parsedUri . protocol === 'ar:' ) {
387+ result . url = new URL ( `${ ENV . PUBLIC_GATEWAY_ARWEAVE } /${ parsedUri . host } ${ parsedUri . pathname } ` ) ;
388+ result . gateway = 'arweave' ;
389+ } else {
390+ throw new MetadataParseError ( `Unsupported uri protocol: ${ uri } ` ) ;
352391 }
353- if ( parsedUri . protocol === 'ar:' ) {
354- return new URL ( `${ ENV . PUBLIC_GATEWAY_ARWEAVE } /${ parsedUri . host } ${ parsedUri . pathname } ` ) ;
392+
393+ if ( result . gateway === 'ipfs' && ENV . PUBLIC_GATEWAY_IPFS_EXTRA_HEADER ) {
394+ const [ key , value ] = ENV . PUBLIC_GATEWAY_IPFS_EXTRA_HEADER . split ( ':' ) ;
395+ result . fetchHeaders = {
396+ [ key . trim ( ) ] : value . trim ( ) ,
397+ } ;
355398 }
399+
400+ return result ;
356401 } catch ( error ) {
357402 throw new MetadataParseError ( `Invalid uri: ${ uri } ` ) ;
358403 }
359- throw new MetadataParseError ( `Unsupported uri protocol: ${ uri } ` ) ;
360404}
361405
362406export function parseDataUrl (
0 commit comments