Skip to content

Repair Highlight Base ingestor - #72

Open
EmergentKnowledgeGroup wants to merge 2 commits into
floornfts:mainfrom
EmergentKnowledgeGroup:fix-highlight-ingestor
Open

Repair Highlight Base ingestor#72
EmergentKnowledgeGroup wants to merge 2 commits into
floornfts:mainfrom
EmergentKnowledgeGroup:fix-highlight-ingestor

Conversation

@EmergentKnowledgeGroup

Copy link
Copy Markdown

Fixes #62.\n\nThis updates the Highlight ingestor to use Highlight's current public GraphQL collection details API instead of the old marketplace/reservoir endpoints, and derives the mint vector id, creator metadata, price including mint fee, image, and sale timestamps from that API response.\n\nVerification:\n- FLOOR_PACKAGES_AUTH_TOKEN=dummy yarn build-types\n- FLOOR_PACKAGES_AUTH_TOKEN=dummy yarn lint\n- FLOOR_PACKAGES_AUTH_TOKEN=dummy ALCHEMY_API_KEY=dummy SIMULATE_DURING_TESTS=false NODE_OPTIONS="--loader ts-node/esm" ./node_modules/.bin/mocha --no-config --require ts-node/register --extension ts --timeout 20000 --exit test/ingestors/highlight.test.ts\n\nNote: the full repo test command currently exercises unrelated live-network ingestors and fails here without real Alchemy/Rarible credentials; the targeted Highlight suite passes 11/11.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Migrate Highlight ingestor to use current GraphQL API

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Migrate from deprecated Reservoir/Marketplace endpoints to Highlight's GraphQL API
• Extract mint vector, pricing, and creator metadata directly from API response
• Remove redundant async calls for vector ID and pricing data
• Simplify timestamp handling by parsing dates from API response
Diagram
flowchart LR
  A["Old Approach:<br/>Multiple API Calls"] -->|"Deprecated Endpoints"| B["Reservoir/Marketplace<br/>APIs"]
  B --> C["Separate Calls for<br/>Vector ID & Price"]
  C --> D["On-chain Metadata<br/>Lookups"]
  
  E["New Approach:<br/>Single GraphQL Call"] -->|"Current API"| F["Highlight GraphQL<br/>getPublicCollectionDetails"]
  F --> G["Complete Data:<br/>Vector, Price, Creator"]
  G --> H["Direct Parsing<br/>No Extra Calls"]
  
  style A fill:#ff9999
  style E fill:#99ff99
Loading

Grey Divider

File Changes

1. src/ingestors/highlight/index.ts 🐞 Bug fix +12/-22

Remove deprecated API calls and simplify data extraction

• Removed imports for deprecated onchain-metadata functions
• Updated imports to use new getHighlightMintVectorId and getHighlightVectorPriceInWei functions
• Eliminated async calls to getHighlightCollectionOwnerDetails and getHighlightVectorId
• Replaced on-chain metadata lookup with direct parsing of collection.mintVector timestamps
• Simplified creator metadata extraction from collection response
• Removed unused chain variable extraction from URL parsing

src/ingestors/highlight/index.ts


2. src/ingestors/highlight/offchain-metadata.ts ✨ Enhancement +113/-117

Consolidate GraphQL queries and add vector processing utilities

• Consolidated multiple GraphQL queries into single getHighlightCollectionDetails function
• Added mintVectors and creatorAccountSettings fields to GraphQL query
• Introduced getHighlightPrimaryMintVector to filter active mint vectors on Base chain
• Added getHighlightMintVectorId to extract vector ID from onchain identifier
• Added highlightEthToWei utility for precise decimal-to-wei conversion
• Added getHighlightVectorPriceInWei to calculate total price including mint fee
• Refactored getHighlightCollectionByAddress to use new unified API approach
• Removed separate getHighlightCollectionOwnerDetails and getHighlightVectorId functions

src/ingestors/highlight/offchain-metadata.ts


3. src/ingestors/highlight/types.ts ✨ Enhancement +32/-33

Restructure types to match unified GraphQL response

• Added creatorAddresses and creatorAccountSettings fields to Collection type
• Added mintVectors array field to Collection type
• Introduced new HighlightMintVector type with mint details and payment currency info
• Simplified CollectionByAddress type to include mintVector and creatorAccountSettings
• Removed complex nested type unions (CollectionByAddress1, CollectionByAddress2,
 CollectionByAddress3)

src/ingestors/highlight/types.ts


View more (1)
4. test/ingestors/highlight.test.ts 🧪 Tests +2/-2

Update test expectations for new image URLs

• Updated expected featured image URLs to use new Highlight CDN format
• Changed from Reservoir image URLs to highlight-creator-assets.highlight.xyz URLs
• Test assertions remain functionally equivalent with updated image sources

test/ingestors/highlight.test.ts


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. base: prefix hardcoded ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
The updated Highlight GraphQL integration is hardcoded to Base by querying base:${contractAddress}
and selecting mint vectors where vector.chainId === 8453, so Ethereum Highlight mints cannot be
ingested. This breaks the requirement to ingest Highlight mints on both Base and ETH (including Open
Edition and Generative drops).
Code

src/ingestors/highlight/offchain-metadata.ts[R84-134]

+export const getHighlightPrimaryMintVector = (
+  collection: Collection,
+): HighlightMintVector | undefined => {
+  return collection.mintVectors?.find(
+    (vector) =>
+      vector.chainId === 8453 &&
+      !vector.paused &&
+      vector.currency.toLowerCase() === NATIVE_ETH_ADDRESS &&
+      !!vectorIdFromOnchainId(vector.onchainMintVectorId),
+  );
+};

-  const data = {
-    operationName: 'GetCollectionSaleDetails',
-    variables: {
-      collectionId: `base:${id}`,
-    },
-    query: `
-    query GetCollectionSaleDetails($collectionId: String!) {
-      getPublicCollectionDetails(collectionId: $collectionId) {
-        size
-        mintVectors {
-          name
-          start
-          end
-          paused
-          price
-          currency
-          chainId
-          paymentCurrency {
-            address
-            decimals
-            symbol
-            type
-            mintFee
-          }
-          onchainMintVectorId
-        }
-      }
-    }
-  `,
-  };
+export const getHighlightMintVectorId = (vector: HighlightMintVector): string | undefined => {
+  return vectorIdFromOnchainId(vector.onchainMintVectorId);
+};

-  try {
-    const resp = await resources.fetcher.post(url, data, { headers });
-    const vectorString = resp.data.data.getPublicCollectionDetails.mintVectors.find(
-      (c: { chainId: number }) => c.chainId === 8453,
-    ).onchainMintVectorId;
-    const vectorId = vectorString.split(':').pop();
-    return vectorId;
-  } catch (error) {}
+export const highlightEthToWei = (amount: string | undefined): bigint => {
+  if (!amount) {
+    return 0n;
+  }
+  const [wholePart, fractionalPart = ''] = amount.split('.');
+  const whole = BigInt(wholePart || '0') * 10n ** 18n;
+  const fractional = BigInt((fractionalPart + '0'.repeat(18)).slice(0, 18));
+  return whole + fractional;
};

-export const getHighlightCollectionOwnerDetails = async (resources: MintIngestorResources, id: string) => {
-  const url = 'https://api.highlight.xyz:8080/';
-  const data = {
-    operationName: 'GetCollectionCreatorDetails',
-    variables: {
-      withEns: true,
-      collectionId: `base:${id}`,
-    },
-    query: `query GetCollectionCreatorDetails($collectionId: String!, $withEns: Boolean) {
-    getPublicCollectionDetails(collectionId: $collectionId) {
-      id
-      creatorAddresses {
-        address
-        name
-      }
-      creatorEns
-      creatorAccountSettings(withEns: $withEns) {
-        verified
-        imported
-        displayAvatar
-        displayName
-        walletAddresses
-      }
-    }
-  }`,
-  };
+export const getHighlightVectorPriceInWei = (vector: HighlightMintVector): string => {
+  const mintFee = highlightEthToWei(vector.paymentCurrency?.mintFee);
+  const price = highlightEthToWei(vector.price);
+  return (price + mintFee).toString();
+};

-  const headers = {
-    accept: 'application/json',
-    'content-type': 'application/json',
-  };
+export const getHighlightCollectionById = async (
+  resources: MintIngestorResources,
+  id: string,
+): Promise<Collection | undefined> => {
+  return getHighlightCollectionDetails(resources, id);
+};

+export const getHighlightCollectionByAddress = async (
+  resources: MintIngestorResources,
+  contractOptions: MintContractOptions,
+): Promise<CollectionByAddress | undefined> => {
 try {
-    const resp = await resources.fetcher.post(url, data, { headers });
-    if (resp.data.errors) {
-      throw new Error("Error fetching owner");
+    const collection = await getHighlightCollectionDetails(
+      resources,
+      `base:${contractOptions.contractAddress}`,
+    );
+    if (!collection || collection.chainId !== 8453) {
+      return undefined;
   }
-    return resp.data.data.getPublicCollectionDetails;
Evidence
Rule 1 and Rule 2 require Highlight ingestion to work on both Base and Ethereum. The new
implementation explicitly limits ingestion to Base by (1) filtering mint vectors to `vector.chainId
=== 8453 and (2) always querying collection details using the base:` prefix, so ETH
collections/vectors will never be selected/ingested.

Repair Highlight Base/ETH ingestor to work with updated Highlight APIs
Ingest Highlight mints on Base and ETH for both Open Edition and Generative drops
src/ingestors/highlight/offchain-metadata.ts[84-94]
src/ingestors/highlight/offchain-metadata.ts[123-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Highlight ingestion is currently Base-only: the GraphQL `collectionId` is always prefixed with `base:` and mint vectors are filtered to `chainId === 8453`, preventing Ethereum Highlight mints from being ingested.
## Issue Context
Compliance requires ingesting Highlight mints on both Base and Ethereum, including Open Edition and Generative drops.
## Fix Focus Areas
- src/ingestors/highlight/offchain-metadata.ts[84-94]
- src/ingestors/highlight/offchain-metadata.ts[123-134]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Empty creator passes support ✓ Resolved 🐞 Bug ≡ Correctness
Description
getHighlightCollectionByAddress() can return a collection with creator set to an empty string, which
makes supportsContract() return true but later causes createMintForContract() to throw
MissingRequiredData on the same contract. This breaks the contract of supportsContract() and can
lead to runtime failures after a contract has already been accepted as “supported”.
Code

src/ingestors/highlight/offchain-metadata.ts[R139-153]

+    const creator = collection.creatorAddresses?.[0]?.address.toLowerCase() || '';
+
+    return {
+      id: collection.id,
+      chainId: collection.chainId,
+      name: collection.name,
+      description: collection.description,
+      image: collection.collectionImage,
+      sampleImages: [collection.collectionImage],
+      creator,
+      contract: collection.address,
+      primaryContract: collection.address,
+      mintVector,
+      creatorAccountSettings: collection.creatorAccountSettings,
+    };
Evidence
The address resolver explicitly falls back to an empty creator string, while the ingestor later
throws if creator is falsy; meanwhile supportsContract() only checks for a non-undefined collection
object.

src/ingestors/highlight/offchain-metadata.ts[123-153]
src/ingestors/highlight/index.ts[34-43]
src/ingestors/highlight/index.ts[75-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getHighlightCollectionByAddress()` builds a `CollectionByAddress` even when the Highlight API response lacks `creatorAddresses`, setting `creator` to `''`. `supportsContract()` only checks that a collection object exists, so it can return `true` for contracts that will deterministically fail later in `createMintForContract()`.
### Issue Context
`createMintForContract()` treats a falsy creator as missing required data and throws, so `supportsContract()` should not return true unless the creator is present (or the resolver should return `undefined` when creator data is missing).
### Fix Focus Areas
- src/ingestors/highlight/offchain-metadata.ts[123-153]
- src/ingestors/highlight/index.ts[34-43]
- src/ingestors/highlight/index.ts[75-88]
### Suggested fix
- In `getHighlightCollectionByAddress()`, if `creatorAddresses` is empty/missing, either:
- return `undefined` (so `supportsContract()` returns `false`), **or**
- derive `creator` from a more reliable field (e.g., `creatorAccountSettings.walletAddresses[0]`) and only return a collection when it’s non-empty.
- Optionally harden `supportsContract()` to require `collection.creator` (and potentially `mintVector`) to be present before returning `true`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Missing mintFee undercharges price ✓ Resolved 🐞 Bug ≡ Correctness
Description
highlightEthToWei() converts an undefined amount to 0n, so getHighlightVectorPriceInWei() silently
drops mint fees (and can also mask missing price data) when the API omits nullable fields like
paymentCurrency. This can produce an incorrect (too-low) priceWei while still passing the later
“Price not available” check because the result is always a non-empty string (e.g., '0').
Code

src/ingestors/highlight/offchain-metadata.ts[R100-114]

+export const highlightEthToWei = (amount: string | undefined): bigint => {
+  if (!amount) {
+    return 0n;
+  }
+  const [wholePart, fractionalPart = ''] = amount.split('.');
+  const whole = BigInt(wholePart || '0') * 10n ** 18n;
+  const fractional = BigInt((fractionalPart + '0'.repeat(18)).slice(0, 18));
+  return whole + fractional;
};

-export const getHighlightCollectionOwnerDetails = async (resources: MintIngestorResources, id: string) => {
-  const url = 'https://api.highlight.xyz:8080/';
-  const data = {
-    operationName: 'GetCollectionCreatorDetails',
-    variables: {
-      withEns: true,
-      collectionId: `base:${id}`,
-    },
-    query: `query GetCollectionCreatorDetails($collectionId: String!, $withEns: Boolean) {
-    getPublicCollectionDetails(collectionId: $collectionId) {
-      id
-      creatorAddresses {
-        address
-        name
-      }
-      creatorEns
-      creatorAccountSettings(withEns: $withEns) {
-        verified
-        imported
-        displayAvatar
-        displayName
-        walletAddresses
-      }
-    }
-  }`,
-  };
+export const getHighlightVectorPriceInWei = (vector: HighlightMintVector): string => {
+  const mintFee = highlightEthToWei(vector.paymentCurrency?.mintFee);
+  const price = highlightEthToWei(vector.price);
+  return (price + mintFee).toString();
+};
Evidence
paymentCurrency is explicitly optional/nullable, but the conversion treats missing amounts as 0, and
the final price is always a string so it won’t be caught by the falsy check in the ingestor.

src/ingestors/highlight/offchain-metadata.ts[100-114]
src/ingestors/highlight/types.ts[25-41]
src/ingestors/highlight/index.ts[98-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`highlightEthToWei()` returns `0n` when the input is `undefined`, and `getHighlightVectorPriceInWei()` uses it on `vector.paymentCurrency?.mintFee` even though `paymentCurrency` is optional/nullable. This silently undercharges when `mintFee` is absent instead of throwing.
### Issue Context
The ingestor’s downstream check `if (!totalPriceWei)` in `createMintForContract()` won’t catch this because `'0'` is truthy.
### Fix Focus Areas
- src/ingestors/highlight/offchain-metadata.ts[100-114]
- src/ingestors/highlight/types.ts[25-41]
- src/ingestors/highlight/index.ts[98-103]
### Suggested fix
- Change `highlightEthToWei()` to return `undefined` (or throw) when `amount` is missing/empty, rather than returning `0n`.
- Update `getHighlightVectorPriceInWei()` to return `undefined` (or throw a typed error) when required fields are missing (e.g., `vector.price`, `vector.paymentCurrency?.mintFee` if mint fee is required).
- Keep `createMintForContract()`’s existing missing-price error path by propagating `undefined` so it throws `MissingRequiredData`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. NaN start yields Invalid Date ✓ Resolved 🐞 Bug ☼ Reliability
Description
createMintForContract() derives startTimestamp via new Date(mintVector.start) and uses the result to
compute liveDate without validating it, so an unparseable start string propagates NaN into new
Date(NaN) (Invalid Date). Because MintTemplateBuilder only checks that liveDate is non-null (not
that it’s valid), an invalid timestamp can slip into templates silently.
Code

src/ingestors/highlight/index.ts[R114-119]

+    const { mintVector } = collection;
+    const startTimestamp = Math.floor(new Date(mintVector.start).getTime() / 1000);
+    const endTimestamp = mintVector.end ? Math.floor(new Date(mintVector.end).getTime() / 1000) : 1893456000;

   const liveDate = +new Date() > startTimestamp * 1000 ? new Date() : new Date(startTimestamp * 1000);
   mintBuilder
Evidence
The ingestor computes start/end timestamps from Date parsing without checking for NaN, and the
template validator does not verify Date validity beyond being non-null.

src/ingestors/highlight/index.ts[114-122]
src/lib/builder/mint-template-builder.ts[41-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Timestamp parsing from `mintVector.start/end` is not validated; if Highlight returns an unexpected format (or empty string), `startTimestamp` becomes `NaN` and `liveDate` becomes `Invalid Date`.
### Issue Context
`MintTemplateBuilder.validateMintTemplate()` checks `liveDate` is present but does not validate that it’s a valid Date, so this failure mode can be silent.
### Fix Focus Areas
- src/ingestors/highlight/index.ts[114-122]
- src/lib/builder/mint-template-builder.ts[41-68]
### Suggested fix
- Guard parsing:
- `const startMs = Date.parse(mintVector.start)` and `if (Number.isNaN(startMs)) throw MissingRequiredData('Missing/invalid start time')`.
- Similarly validate `mintVector.end` when present.
- Compute `liveDate` from validated milliseconds (or ensure it falls back consistently, e.g., to `new Date()` if start is invalid).
- (Optional hardening) Add Date-validity checks in `MintTemplateBuilder.validateMintTemplate()` for `liveDate/availableForPurchaseStart/availableForPurchaseEnd` (e.g., `!Number.isNaN(date.getTime())`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/ingestors/highlight/offchain-metadata.ts Outdated
Comment thread src/ingestors/highlight/offchain-metadata.ts Outdated
Comment thread src/ingestors/highlight/offchain-metadata.ts Outdated
@EmergentKnowledgeGroup

Copy link
Copy Markdown
Author

Follow-up pushed in d3e7350 addressing the Qodo findings:\n\n- resolves Highlight on-chain URLs through getCollectionByOnChainId and supports both Ethereum (1) and Base (8453)\n- derives the mint contract from onchainMintVectorId instead of hardcoding Base\n- returns unsupported when creator metadata is missing rather than accepting then failing later\n- treats missing/invalid price or mintFee as missing required data\n- validates start/end timestamps before building template dates\n- adds an Ethereum Highlight fixture covering URL and contract ingestion\n\nVerified locally:\n- FLOOR_PACKAGES_AUTH_TOKEN=dummy yarn build-types\n- FLOOR_PACKAGES_AUTH_TOKEN=dummy yarn lint\n- FLOOR_PACKAGES_AUTH_TOKEN=dummy ALCHEMY_API_KEY=dummy SIMULATE_DURING_TESTS=false NODE_OPTIONS="--loader ts-node/esm" ./node_modules/.bin/mocha --no-config --require ts-node/register --extension ts --timeout 20000 --exit test/ingestors/highlight.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repair the Highlight Base Ingestor

1 participant