Skip to content

Manifold Migration#1984

Merged
prxt6529 merged 186 commits intomainfrom
manifold-migration
Mar 12, 2026
Merged

Manifold Migration#1984
prxt6529 merged 186 commits intomainfrom
manifold-migration

Conversation

@prxt6529
Copy link
Copy Markdown
Collaborator

@prxt6529 prxt6529 commented Feb 25, 2026

Summary by CodeRabbit

  • New Features

    • Full Drop Forge experience: landing, Craft & Launch pages, claim editors, listings, explorer links, media previews (image/video/GLB/HTML), status pills, accordions, and transaction modal.
    • Testnet toggle & visible TESTNET indicator, plus chain-switch control in header/account.
    • Improved airdrop/phase views and distribution reporting; explorer links include address query.
  • Bug Fixes

    • Safer trait/value handling, field validation, count formatting, and more robust permission/loading states.

Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds the Drop Forge feature (craft & launch) with pages, client components, hooks, API client/endpoints, Arweave upload workflows, multi-chain/testnet support, manifold minting refactor (chain-aware, new metadata types), UI primitives/icons, and extensive test updates and reorganizations.

Changes

Cohort / File(s) Summary
Env & Runtime
\.env.sample, config/env.schema.ts, next.config.ts
Added DROP_FORGE_TESTNET env/schema and exposed it at runtime; centralized assetPrefix helper in next.config.
API Surface & Client
openapi.yaml, services/api/memes-minting-claims-api.ts, services/api/common-api.ts
Large MintingClaims and distribution schemas/endpoints added; new memes-minting-claims API client, multipart upload helpers, and commonApiPatch + signal support.
Pages (Drop Forge)
app/drop-forge/page.tsx, app/drop-forge/craft/page.tsx, app/drop-forge/craft/[id]/page.tsx, app/drop-forge/launch/page.tsx, app/drop-forge/launch/[id]/page.tsx
Added Drop Forge app routes and generateMetadata exports for landing, craft, launch lists and detail pages.
Drop Forge Feature Components & Helpers
components/drop-forge/*, components/drop-forge/launch/*, components/drop-forge/claimTraitsData.ts, components/drop-forge/drop-forge-status.helpers.ts, components/drop-forge/drop-forge-config.ts, components/drop-forge/drop-forge.constants.ts
New claims list, craft/launch page clients, editors, launch helpers, status/phase helpers, minting config, constants and large supporting utilities.
UI Primitives & Icons
components/drop-forge/DropForgeAccordionSection.tsx, DropForgeFieldBox.tsx, DropForgeMediaTypePill.tsx, DropForgeStatusPill.tsx, DropForgeTestnetIndicator.tsx, DropForgeExplorerLink.tsx, components/common/icons/*
Added reusable UI primitives and three Drop Forge icons.
Manifold Minting Refactor & Types
components/manifold-minting/*, components/manifoldMinting/manifold-types.ts (deleted), components/manifold-minting/manifold-mint-metadata.ts, hooks/useManifoldClaim.ts, hooks/useDropForgeManifoldClaim.ts
Refactored manifold minting to accept chain + mintMetadata, introduced Arweave metadata types, rewrote useManifoldClaim to param-object and richer return, removed legacy manifold type file.
Minting Countdown / Now Minting
hooks/useMintCountdownState.ts, components/home/now-minting/*, components/the-memes/*
useMintCountdownState now requires opts with contract/chainId/abi; NowMintingCountdown accepts contract+chainId; callers updated to source config via Drop Forge hook.
Permissions & Admins
hooks/useDropForgePermissions.ts, hooks/useIsDropForgeAdmin.ts, contexts/SeizeSettingsContext.tsx, openapi.yaml
Added Drop Forge permissions hooks and is-admin hook; SeizeSettings extended with distribution/claims admin wallet lists and API schema updated.
Distribution & Subscriptions
components/distribution-plan-tool/..., components/distribution/Distribution.tsx, components/user/subscriptions/UserPageSubscriptionsTopUp.tsx
Wired distribution admin checks to runtime settings, formatted counts, and emoji-based top-up modal UI tweaks.
Multi-chain / Chain Switcher & AppKit
components/header/useChainSwitcher.ts, components/providers/AppKitAdapterManager.ts, utils/appkit-initialization.utils.ts, components/providers/WagmiSetup.tsx
Added useChainSwitcher, chain-aware AppKit/adapter manager and initialization, testnet (sepolia) option controlled by env and passed through initialization.
Navigation / Header / Sidebar
components/layout/sidebar/WebSidebarNav.tsx, WebSidebar.tsx, components/header/*, components/navigation/*, components/header/header-search/*
Added Drop Forge nav entries, header search aliases, chain-switch UI in header, BottomNavigation wave DM wiring, NavItem prop additions and unread indicators.
Tests — Reorg & New Suites
__tests__/components/manifold-minting/*, __tests__/components/manifoldMinting/* (deleted), __tests__/hooks/useManifoldClaim.test.tsx, __tests__/config/env.base-endpoint.test.ts
Reorganized manifold tests, added new ManifoldMintingConnect/Widget tests, updated useManifoldClaim tests to new API, added DROP_FORGE_TESTNET parsing test, and other test mocks for chain switching and permissions.
Traits / Forms
components/waves/memes/traits/*, components/waves/memes/MemesArtSubmissionTraits.tsx, components/waves/memes/traits/schema.ts
Per-field readOnly support and readOnlyOverrides plumbing, refined filled logic and minor UI class tweaks.
Misc Types & Small Changes
entities/INFT.ts, helpers/manifoldDisplayHelpers.ts, components/manifold-minting/ManifoldMintingConnect.tsx
IAttribute.value widened to `string

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant Client as DropForge<br/>Launch/Craft Client
    participant API as Minting-Claims API
    participant Storage as Arweave/Upload
    participant Chain as Blockchain/Manifold

    User->>Browser: Open Drop Forge page / perform upload or init
    Browser->>Client: render with claimId & config (chain/contract)
    Client->>API: GET /minting-claims/{id} (roots/proofs/metadata)
    API-->>Client: MintingClaim + roots/proofs

    User->>Client: Upload image/animation
    Client->>API: POST /minting-claims/{id}/arweave-upload or multipart upload
    API->>Storage: store file
    Storage-->>API: location
    API-->>Client: stored location

    User->>Client: Initialize/update on-chain
    Client->>API: GET proofs/roots by address or merkleRoot
    API-->>Client: proofs
    Client->>Chain: send tx (using chain id & contract)
    Chain-->>Client: txHash / receipt
    Client->>API: PATCH /minting-claims/{id} (update state)
    API-->>Client: updated claim
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • #1697 — multipart upload / distribution photo S3 multipart flow; overlaps with Arweave/multipart upload and openapi changes.
  • #1623 — appkit initialization / adapter manager chain-aware changes; related to createAdapter/chains changes.
  • #1720 — next.config and asset/runtime configuration refactor; related to centralized assetPrefix and runtime env exposure.

Poem

🐰 I hopped into the forge at dawn,
Crafting claims on chain and lawn,
Proofs and pixels stitched with care,
Testnet flags flutter in the air.
Mint and blossom — hop, rejoice, and yawn!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch manifold-migration

@prxt6529
Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
components/navigation/NavItem.tsx (1)

58-68: ⚠️ Potential issue | 🟠 Major

Two correctness issues in the useEffect: stale closures and wrong title on reset.

1 — Incomplete dependency array (stale closure risk)

setTitle and removeAllDeliveredNotifications are missing from the dependency array. If either function changes identity between renders (especially removeAllDeliveredNotifications, whose stability is unknown), the effect closes over a stale reference and silently does nothing or calls the wrong version.

2 — Hardcoded "6529.io" overrides the current-page title everywhere

When !haveUnreadNotifications, setTitle("6529.io") is called unconditionally. NavItem lives in a persistent navigation bar, so this fires on any page when unread count drops to zero—clobbering wave titles, profile titles, etc. It also produces the wrong title on staging (where DEFAULT_TITLE is "6529 Staging").

The else-branch should either be removed entirely (letting TitleContext manage the reset) or use DEFAULT_TITLE:

+import { DEFAULT_TITLE, useTitle } from "@/contexts/TitleContext";
 ...
 useEffect(() => {
   if (item.name !== "Notifications") return;
-  setTitle(
-    haveUnreadNotifications
-      ? `(${notifications?.unread_count}) Notifications | 6529.io`
-      : "6529.io"
-  );
+  if (haveUnreadNotifications) {
+    setTitle(`(${notifications?.unread_count}) Notifications | ${DEFAULT_TITLE}`);
+  }
   if (!haveUnreadNotifications) {
     removeAllDeliveredNotifications();
   }
 }, [
   haveUnreadNotifications,
   notifications?.unread_count,
   item.name,
   setTitle,
   removeAllDeliveredNotifications,
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/navigation/NavItem.tsx` around lines 58 - 68, The effect in
NavItem (useEffect watching haveUnreadNotifications and
notifications?.unread_count) is missing setTitle and
removeAllDeliveredNotifications from its dependency array and also
unconditionally resets the title to the hardcoded "6529.io" which clobbers
page-specific titles; update the dependency array to include setTitle and
removeAllDeliveredNotifications to avoid stale closures, and change the branch
that resets the title so it does not use the hardcoded string — either remove
the reset entirely and let TitleContext manage it, or use the DEFAULT_TITLE
constant (or call the TitleContext reset API) when !haveUnreadNotifications;
keep the effect gated by item.name === "Notifications" as-is.
openapi.yaml (1)

9792-9814: ⚠️ Potential issue | 🟡 Minor

Constrain wallet fields to Ethereum address format.

New wallet-typed fields currently accept arbitrary strings, weakening contract validation.

🛡️ Proposed validation constraints
     ApiSeizeSettings:
@@
         distribution_admin_wallets:
           type: array
           items:
             type: string
+            pattern: ^0x[a-fA-F0-9]{40}$
         claims_admin_wallets:
           type: array
           items:
             type: string
+            pattern: ^0x[a-fA-F0-9]{40}$

     PhaseAirdrop:
@@
         wallet:
           type: string
+          pattern: ^0x[a-fA-F0-9]{40}$

Also applies to: 11837-11847

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 9792 - 9814, The wallet array properties
distribution_admin_wallets and claims_admin_wallets currently accept any string;
update their item schema in openapi.yaml to validate Ethereum addresses by
replacing the unconstrained string type with a patterned string (e.g., add
pattern: "^0x[a-fA-F0-9]{40}$" and optionally format: "ethereum-address") for
the items of both arrays (and apply the same change at the other occurrence
noted), ensuring each array item must match the 0x-prefixed 40-hex-character
Ethereum address format.
components/providers/AppKitAdapterManager.ts (1)

147-170: ⚠️ Potential issue | 🔴 Critical

Cache key ignores chains, risking stale adapter reuse with wrong network configuration.

getCacheKey (line 222) only incorporates wallet addresses into the cache key. When createAdapterWithCache is called with the same wallets but different chains (e.g., switching from mainnet to testnet), the cache returns an adapter configured for the original chains instead of the requested ones.

Include chain IDs in the cache key to ensure network configuration matches the request:

🐛 Proposed fix
- private getCacheKey(wallets: AppWallet[]): string {
+ private getCacheKey(wallets: AppWallet[], chains: Chain[]): string {
    if (!Array.isArray(wallets)) {
      throw new AdapterError(
        "ADAPTER_013: Cannot generate cache key: wallets must be an array"
      );
    }

    const addresses = wallets.map((w) => {
      if (!w?.address) {
        throw new AdapterError(
          "ADAPTER_014: Cannot generate cache key: invalid wallet object"
        );
      }
      return w.address;
    });

    if (addresses.length === 0) {
-     return "empty-wallets";
+     return `empty-wallets:${chains.map((c) => c.id).sort().join(",")}`;
    }

    const sortedAddresses = addresses.toSorted((a, b) => a.localeCompare(b));
-   return sortedAddresses.join(",");
+   const chainIds = chains.map((c) => c.id).sort().join(",");
+   return `${sortedAddresses.join(",")}:${chainIds}`;
  }

And update the call site at line 156:

- const cacheKey = this.getCacheKey(appWallets);
+ const cacheKey = this.getCacheKey(appWallets, chains);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/providers/AppKitAdapterManager.ts` around lines 147 - 170, The
cache key currently ignores chains causing adapters to be reused across
different network configs; update getCacheKey to accept chains (e.g.,
getCacheKey(appWallets, chains)) and incorporate deterministic chain identifiers
(sorted chain IDs or chain.network/chain.id) into the key, then modify
createAdapterWithCache to call this.getCacheKey(appWallets, chains) when looking
up/setting adapterCache and when storing the newly created adapter returned by
createAdapter; ensure any currentAdapter/currentWallets tracking is updated to
also reflect the chains if you track them (e.g., currentChains) so the adapter
returned always matches the requested chains.
components/manifold-minting/ManifoldMintingWidget.tsx (2)

57-96: ⚠️ Potential issue | 🟠 Major

Guard against stale merkle-proof fetch responses.

The async fetch can resolve out of order when mintForAddress changes, and an older response may overwrite newer proofs.

🛠️ Proposed fix (effect-level stale guard)
   useEffect(() => {
+    let active = true;
     mintWrite.reset();
     setMintStatus(<></>);
     setMintError("");
@@
     if (mintForAddress && props.claim.merkleRoot) {
       setIsError(false);
       setFetchingMerkle(true);
       getMemesMintingProofsByAddress(
         props.claim.instanceId,
         props.claim.merkleRoot,
         mintForAddress
       )
         .then((response) => {
+          if (!active) return;
           const mappedProofs: MintingClaimsProofItem[] = (
             response.proofs ?? []
           ).map((proof) => ({
             merkle_proof: proof.merkle_proof ?? [],
             value: Number(proof.value ?? 0),
           }));
           setMerkleProofs(mappedProofs);
         })
         .catch(() => {
+          if (!active) return;
           setIsError(true);
           setMerkleProofs([]);
         })
         .finally(() => {
+          if (!active) return;
           setFetchingMerkle(false);
         });
     }
+    return () => {
+      active = false;
+    };
   }, [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifold-minting/ManifoldMintingWidget.tsx` around lines 57 - 96,
The fetch in the useEffect for merkle proofs can return out-of-order and
overwrite newer data; add an effect-level stale guard: create a local
"cancelled" or "currentRequestId" token inside the useEffect before calling
getMemesMintingProofsByAddress, capture it in then/catch/finally handlers, and
only call setMerkleProofs, setIsError, and setFetchingMerkle if the token still
matches (i.e., request is not stale) — update the handlers around
getMemesMintingProofsByAddress in the useEffect that references mintForAddress,
props.claim.merkleRoot, props.claim.instanceId, and ensure to clean up by
invalidating the token in the effect cleanup to prevent stale responses from
applying.

100-108: ⚠️ Potential issue | 🟠 Major

Replace side-effect-only map in getReadContractsParams.

Line 100 uses map but never returns from the callback; this violates Biome's useIterableCallbackReturn rule and will keep lint red.

🛠️ Proposed fix
   function getReadContractsParams() {
-    const params: any = [];
-    merkleProofs.map((mp) => {
-      params.push({
+    return merkleProofs.map((mp) => ({
         address: MANIFOLD_LAZY_CLAIM_CONTRACT as `0x${string}`,
         abi: props.abi,
         chainId: props.chain.id,
         functionName: "checkMintIndex",
         args: [props.contract, props.claim.instanceId, mp.value],
-      });
-    });
-    return params;
+      }));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifold-minting/ManifoldMintingWidget.tsx` around lines 100 -
108, The current getReadContractsParams uses merkleProofs.map with a side-effect
(pushing into params) which violates the useIterableCallbackReturn rule; change
the iteration to a side-effect-appropriate loop such as merkleProofs.forEach or
a for...of loop and push the same objects into params (using
MANIFOLD_LAZY_CLAIM_CONTRACT, props.abi, props.chain.id, functionName
"checkMintIndex", and args [props.contract, props.claim.instanceId, mp.value])
so the callback returns nothing and the lint rule is satisfied.
components/waves/memes/traits/TextTrait.tsx (1)

71-79: ⚠️ Potential issue | 🟡 Minor

handleBlur comparison doesn't normalize to string, unlike the rest of the component.

Line 72 compares inputRef.current.value (always a string) directly against traits[field] (which could be a number). The rest of the component normalizes via .toString() (Lines 36, 83, 91). When traits[field] is numeric, strict !== between "42" and 42 is always true, causing a spurious updateText call on every blur.

Proposed fix
     const handleBlur = useCallback(() => {
-      if (inputRef.current && inputRef.current.value !== traits[field]) {
+      if (inputRef.current && inputRef.current.value !== ((traits[field] as string) ?? "").toString()) {
         updateText(field, inputRef.current.value);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/memes/traits/TextTrait.tsx` around lines 71 - 79, handleBlur
currently compares inputRef.current.value (string) to traits[field] without
normalizing, causing updates when traits[field] is numeric; change the
comparison in handleBlur to compare inputRef.current.value to
String(traits[field]) (or traits[field]?.toString()) before calling updateText,
keeping the rest of the behavior (calling updateText(field,
inputRef.current.value) and invoking onBlur(field) if present) and preserving
the dependency array that includes field, traits, updateText, and onBlur.
🧹 Nitpick comments (35)
components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableRow.tsx (1)

194-194: Use the shared count formatter instead of a local duplicate.

There is already a project helper (helpers/format.helpers.ts) for this. Reusing it keeps formatting behavior consistent and preserves defensive handling for invalid values.

♻️ Proposed refactor
+import { formatCount } from "@/helpers/format.helpers";
...
-  const formatCount = (count: number) => count.toLocaleString();
...
       <td className={commonClasses}>
-        {isPublic ? "-" : formatCount(item.walletsCount)}
+        {isPublic ? "-" : (formatCount(item.walletsCount) ?? "-")}
       </td>
       <td className={commonClasses}>
-        {isPublic ? "-" : formatCount(item.spotsCount)}
+        {isPublic ? "-" : (formatCount(item.spotsCount) ?? "-")}
       </td>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableRow.tsx`
at line 194, Replace the local const formatCount defined in
ReviewDistributionPlanTableRow.tsx with the shared formatter exported from
helpers/format.helpers.ts: remove the local function (formatCount) and import
the shared formatter (formatCount) from "helpers/format.helpers.ts", then use
that imported function wherever the local one was used so you retain the shared
formatting logic and its defensive handling for invalid values.
services/api/common-api.ts (1)

359-378: commonApiPatch looks good; consider adding signal to commonApiPut for consistency.

commonApiPatch correctly forwards signal to executeApiRequest, but commonApiPut (line 340) has no signal parameter. This inconsistency means PUT requests cannot be aborted, while the equivalent PATCH can.

♻️ Suggested addition to commonApiPut
 export const commonApiPut = async <T, U, Z = Record<string, string>>(param: {
   endpoint: string;
   body: T;
   headers?: Record<string, string> | undefined;
   params?: Z | undefined;
+  signal?: AbortSignal | undefined;
 }): Promise<U> => {
   const url = buildUrl(
     param.endpoint,
     param.params as Record<string, string> | undefined
   );

   return executeApiRequest<U>(
     url,
     "PUT",
     getHeaders(param.headers, true),
-    JSON.stringify(param.body)
+    JSON.stringify(param.body),
+    param.signal
   );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/api/common-api.ts` around lines 359 - 378, commonApiPut is missing
an AbortSignal parameter and doesn't forward it to executeApiRequest, making PUT
requests non-cancellable; add an optional signal?: AbortSignal to the
commonApiPut param type and pass param.signal into the executeApiRequest call
(same as commonApiPatch), and ensure buildUrl/getHeaders usage remains unchanged
so PUT behaves consistently with commonApiPatch.
components/user/subscriptions/UserPageSubscriptionsTopUp.tsx (1)

48-56: Simplify getTopUpModalEmoji — line 55 is dead code.

All four union members are explicitly handled by lines 51–54, making the final return on line 55 unreachable. An object map eliminates the dead fallthrough and makes the mapping clearer at a glance.

♻️ Proposed refactor
-function getTopUpModalEmoji(
-  status: "confirm_wallet" | "submitted" | "success" | "error"
-): string {
-  if (status === "success") return "/emojis/sgt_saluting_face.webp";
-  if (status === "error") return "/emojis/sgt_sob.webp";
-  if (status === "confirm_wallet") return "/emojis/sgt_flushed.webp";
-  if (status === "submitted") return "/emojis/sgt_flushed.webp";
-  return "/emojis/sgt_flushed.webp";
-}
+const TOP_UP_MODAL_EMOJIS = {
+  success: "/emojis/sgt_saluting_face.webp",
+  error: "/emojis/sgt_sob.webp",
+  confirm_wallet: "/emojis/sgt_flushed.webp",
+  submitted: "/emojis/sgt_flushed.webp",
+} as const satisfies Record<"confirm_wallet" | "submitted" | "success" | "error", string>;
+
+function getTopUpModalEmoji(
+  status: keyof typeof TOP_UP_MODAL_EMOJIS
+): string {
+  return TOP_UP_MODAL_EMOJIS[status];
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/user/subscriptions/UserPageSubscriptionsTopUp.tsx` around lines 48
- 56, getTopUpModalEmoji currently uses multiple if statements with a dead
fallback return; replace this with a simple object map keyed by the status union
used in getTopUpModalEmoji to return the emoji string (e.g., map
"confirm_wallet", "submitted", "success", "error" to their respective paths) and
return map[status] (or a typed lookup with a default if needed) so the
unreachable final return is removed and the mapping is clearer and exhaustive;
update the function getTopUpModalEmoji accordingly.
components/navigation/NavItem.tsx (1)

35-40: useWaveData + useWave fire for every NavItem instance, but the result is only used for the active state check.

Every nav entry (Home, Waves, Messages, Notifications, …) independently calls useWaveData and useWave with the same waveIdFromQuery, producing redundant hook subscriptions/queries per render. The isCurrentWaveDmValue result is only needed by isNavItemActive, which in turn is only meaningful for the Messages item.

Consider lifting the wave-DM resolution out of NavItem into its parent (the nav container), passing isCurrentWaveDm as a prop, so the query runs exactly once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/navigation/NavItem.tsx` around lines 35 - 40, NavItem currently
calls useWaveData({ waveId: waveIdFromQuery, onWaveNotFound: () => {} }) and
useWave(waveData) for every instance to compute isCurrentWaveDmValue only used
in isNavItemActive — lift this logic to the nav container (parent) so the
hook/query runs once: call useWaveData and useWave in the parent using
waveIdFromQuery, derive isCurrentWaveDm, then remove useWaveData/useWave from
NavItem and add a new prop (e.g. isCurrentWaveDm) to NavItem and use that inside
isNavItemActive for the Messages item. Ensure you preserve the onWaveNotFound
behavior in the parent and update all NavItem invocations to pass the new prop.
components/header/AppUserConnect.tsx (1)

20-33: Extract shared chain-switch behavior into a reusable hook/helper.

This chain resolution + switch logic is duplicated in components/header/user/proxy/HeaderUserProxyDropdown.tsx (line range 40-78). Consolidating it will avoid behavior drift and keep fixes consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header/AppUserConnect.tsx` around lines 20 - 33, Extract the
duplicated chain resolution and switch logic into a shared hook (e.g.,
useChainSwitcher) that uses useChains, useChainId, and useSwitchChain to expose
currentChainName and a switchToNextChain function; replace the inline logic in
AppUserConnect (currentChain, onSwitchChain) and the duplicate block in
HeaderUserProxyDropdown.tsx with calls to useChainSwitcher to read
currentChainName and invoke switchToNextChain, ensuring the new hook returns the
same behavior (find next chain where c.id !== chainId and call switchChain with
{ chainId: next.id }) and export it for reuse.
components/drops/view/item/content/media/MediaDisplayGLB.tsx (1)

158-161: Re-stroked edges visually thicken shared cube edges.

The three <path> elements at lines 159–161 redraw edges that are already fully stroked by the face paths above (top face, left face, right face each emit stroke="currentColor"). The overlap doubles the apparent stroke weight on every shared silhouette edge and the centre vertical, which may appear unintentionally bold—especially at small button sizes (tw-h-9 tw-w-9).

Consider either removing the re-stroke group entirely, or converting the face paths to fill-only (stroke="none") and relying solely on the re-stroke paths for edge rendering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drops/view/item/content/media/MediaDisplayGLB.tsx` around lines
158 - 161, The SVG in MediaDisplayGLB.tsx currently re-strokes the cube edges
with three extra <path> elements (the paths drawing the silhouette and center
vertical) which doubles stroke weight against the face paths; remove those three
re-stroke <path> elements (the ones with d="M6 9v6l6 3 6-3v-6", d="M6 9l6-3 6
3", and d="M12 6v12") or alternatively change the face paths that currently
include stroke="currentColor" to stroke="none" and keep only the re-stroke paths
for edge rendering so shared edges render at a single intended weight (adjust
whichever approach keeps visual parity at small sizes like tw-h-9 tw-w-9).
openapi.yaml (1)

2428-2450: Use 202 Accepted for queued Arweave uploads.

The description says this operation enqueues async work; 202 is a clearer status contract than 200.

♻️ Proposed response code adjustment
       responses:
-        "200":
-          description: successful operation
+        "202":
+          description: Accepted - upload enqueued
           content:
             application/json:
               schema:
                 $ref: "#/components/schemas/MintingClaim"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 2428 - 2450, The OpenAPI response for the async
queue endpoint postMintingClaimArweaveUpload should return 202 Accepted instead
of 200; update the responses block for operationId postMintingClaimArweaveUpload
to use "202" with a description like "accepted; work queued" (and adjust any
examples/schemas tied to the "200" response) so the contract clearly reflects
that the upload is enqueued asynchronously.
components/common/icons/DropForgeIcon.tsx (1)

6-11: Consider defaulting this SVG to decorative for a11y

If this icon is used next to visible text (e.g., nav labels), adding aria-hidden and focusable="false" avoids redundant screen reader output.

Proposed tweak
   <svg
     xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 32 32"
     preserveAspectRatio="xMidYMid meet"
     className={className}
+    aria-hidden="true"
+    focusable="false"
   >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/common/icons/DropForgeIcon.tsx` around lines 6 - 11, The SVG in
the DropForgeIcon component is missing attributes to mark it decorative; update
the <svg> element inside DropForgeIcon to include aria-hidden="true" and
focusable="false" so screen readers skip it by default (if you need it to be
accessible in some cases, add an optional prop like "ariaLabel" or "decorative"
to conditionally render aria-hidden/role/aria-label instead). Target the
DropForgeIcon component's <svg> element (the one with className={className}) and
add those attributes or a small prop toggle to control them.
contexts/SeizeSettingsContext.tsx (1)

51-52: Good defaults added — consider defensive merge on fetch too

To avoid undefined arrays if the API payload is partial, normalize these fields during setSeizeSettings as well.

Proposed hardening
         setSeizeSettings({
           ...settings,
           memes_wave_id:
             publicEnv.DEV_MODE_MEMES_WAVE_ID ?? settings.memes_wave_id,
           curation_wave_id:
             publicEnv.DEV_MODE_CURATION_WAVE_ID ?? settings.curation_wave_id,
+          distribution_admin_wallets: settings.distribution_admin_wallets ?? [],
+          claims_admin_wallets: settings.claims_admin_wallets ?? [],
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contexts/SeizeSettingsContext.tsx` around lines 51 - 52, When updating state
from the API, ensure you defensive-merge/normalize missing array fields so they
never become undefined: inside the setSeizeSettings updater (or reducer)
coalesce distribution_admin_wallets and claims_admin_wallets to [] when the
incoming payload omits them, e.g. merge the existing state with the fetched
payload and default these two symbols (distribution_admin_wallets,
claims_admin_wallets) to empty arrays before calling setSeizeSettings so the
component always sees arrays.
components/home/now-minting/NowMintingCountdown.tsx (1)

26-33: Unnecessary intermediate object and redundant conditional spread.

The countdownOptions variable is created only to be immediately spread into useMintCountdownState. Since hideMintBtn is an optional parameter, passing it as undefined is identical to omitting it, making the conditional spread needless.

♻️ Suggested simplification
-  const countdownOptions = {
-    contract,
-    chainId,
-    ...(hideMintBtn === undefined ? {} : { hideMintBtn }),
-  };
-  const state = useMintCountdownState(nftId, {
-    ...countdownOptions,
-  });
+  const state = useMintCountdownState(nftId, { contract, chainId, hideMintBtn });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/home/now-minting/NowMintingCountdown.tsx` around lines 26 - 33,
The temporary countdownOptions object and the conditional spread are
unnecessary; pass the props directly to useMintCountdownState instead. Replace
the creation/use of countdownOptions by calling useMintCountdownState(nftId, {
contract, chainId, hideMintBtn }) so hideMintBtn can be undefined (no
special-case spread needed) and remove countdownOptions from the file; update
references to countdownOptions accordingly to use the direct object literal in
the useMintCountdownState call.
components/drop-forge/DropForgeTestnetIndicator.tsx (1)

21-21: Trailing space in className when className is empty.

When className defaults to "", the template literal produces a trailing space (e.g., "...sm:tw-text-base "). This is harmless but unclean.

♻️ Suggested fix
-      className={`tw-inline-flex tw-items-center tw-rounded-md tw-border tw-border-amber-400/60 tw-bg-amber-500/15 tw-px-2.5 tw-py-1 tw-text-sm tw-font-semibold tw-text-amber-100 sm:tw-text-base ${className}`}
+      className={`tw-inline-flex tw-items-center tw-rounded-md tw-border tw-border-amber-400/60 tw-bg-amber-500/15 tw-px-2.5 tw-py-1 tw-text-sm tw-font-semibold tw-text-amber-100 sm:tw-text-base${className ? ` ${className}` : ""}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeTestnetIndicator.tsx` at line 21, The
className string in the DropForgeTestnetIndicator component produces a trailing
space when the prop/className variable is empty; update the className
construction inside the JSX (the className attribute in
DropForgeTestnetIndicator) to avoid the trailing space by conditionally
appending the external className (e.g., use an array/filter/join pattern or a
conditional like adding `' ' + className` only when className is truthy, or
replace with a small utility like clsx) so the rendered class attribute has no
extra trailing space when className === "".
hooks/useIsDropForgeAdmin.ts (1)

28-28: readResult.isLoading is redundant alongside readResult.isFetching.

In TanStack Query v5 (used by wagmi v2), isLoading is derived as isPending && isFetching. This means whenever isLoading is true, isFetching is also true. Therefore, the || readResult.isLoading branch in the OR expression never adds a new condition—isFetching alone covers all cases.

♻️ Suggested simplification
-    isFetching: Boolean(address) && (readResult.isLoading || readResult.isFetching),
+    isFetching: Boolean(address) && readResult.isFetching,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useIsDropForgeAdmin.ts` at line 28, In useIsDropForgeAdmin, simplify
the computed isFetching flag by removing the redundant readResult.isLoading
check: update the expression that sets isFetching (which currently references
Boolean(address) && (readResult.isLoading || readResult.isFetching)) to only
depend on readResult.isFetching together with the address check; keep the
Boolean(address) AND readResult reference, but drop readResult.isLoading to
avoid the redundant condition.
components/drop-forge/DropForgeNoPower.tsx (1)

12-14: Use replace for forced redirects.

On Line 13, router.push("/") keeps the unauthorized page in history. router.replace("/") avoids back-button loops into the no-power screen.

↩️ Suggested change
-      const t = setTimeout(() => router.push("/"), 1000);
+      const t = setTimeout(() => router.replace("/"), 1000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeNoPower.tsx` around lines 12 - 14, In
DropForgeNoPower, the timeout handler currently uses router.push("/") which
leaves the no-power page in history and can cause back-button loops; change the
timeout callback in the seconds === 1 branch (where setTimeout is assigned to t)
to call router.replace("/") instead of router.push("/") and keep the existing
cleanup return () => clearTimeout(t) unchanged so the timeout is properly
cleared if the component unmounts.
components/common/icons/DropForgeCraftIcon.tsx (1)

6-10: Hide decorative SVGs from assistive tech.

On Line 6, this icon component appears decorative; without explicit a11y attributes, screen readers can still encounter it. Add aria-hidden and focusable defaults.

♿ Suggested tweak
   <svg
     xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 48 48"
     className={className}
+    aria-hidden="true"
+    focusable="false"
   >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/common/icons/DropForgeCraftIcon.tsx` around lines 6 - 10, The
DropForgeCraftIcon SVG is decorative and needs accessibility attributes: update
the DropForgeCraftIcon component so the <svg> element includes
aria-hidden="true" and focusable="false" by default (set them directly on the
<svg> element or via the component's props with those defaults) to prevent
screen readers and keyboard focus from exposing the icon.
components/drop-forge/launchClaimHelpers.ts (1)

8-8: Consider rejecting non-positive edition sizes as “missing required”.

Line 8 currently allows 0 or negative values. If launch requires a positive edition size, this check should guard that too.

🛡️ Defensive tweak
-  const noEditionSize = claim.edition_size == null;
+  const noEditionSize =
+    claim.edition_size == null || claim.edition_size <= 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/launchClaimHelpers.ts` at line 8, The current check
"const noEditionSize = claim.edition_size == null" treats 0 or negative values
as present; update the guard to treat non-positive sizes as missing by changing
it to check for null/undefined or <= 0 (e.g. const noEditionSize =
claim.edition_size == null || claim.edition_size <= 0) so that functions using
noEditionSize reject zero/negative edition_size values.
components/layout/sidebar/WebSidebarNav.tsx (2)

97-102: No-op useMemoallSections is already memoized.

useSidebarSections returns a memoized array. Wrapping it in useMemo(() => allSections, [allSections]) is an identity function that adds no value.

♻️ Simplify
- const allSections = useSidebarSections(
+ const sections = useSidebarSections(
    appWalletsSupported,
    capacitor.isIos,
    country
  );
- const sections = useMemo(() => allSections, [allSections]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/sidebar/WebSidebarNav.tsx` around lines 97 - 102, The
variable sections is redundantly created by memoizing allSections again; remove
the no-op useMemo and use the memoized result from useSidebarSections directly.
Replace the lines that declare allSections and sections (the call to
useSidebarSections and the useMemo wrapping it) so that you only assign the hook
result to a single variable (e.g., allSections or sections) and update any
references to use that single identifier; remove the useMemo import/usage tied
to this change.

45-45: _isMobile is accepted but unused.

The isMobile prop is destructured as _isMobile and never referenced. If it's planned for future use, consider removing it until needed to keep the API surface clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/sidebar/WebSidebarNav.tsx` at line 45, The component
WebSidebarNav currently destructures the prop as _isMobile but never uses it;
update the parameter list in the WebSidebarNav functional component (the arrow
function taking ({ isCollapsed = false, isMobile: _isMobile = false }, ref)) to
either remove the unused isMobile/_isMobile from the destructuring or rename and
use the prop where needed; ensure you only change the destructuring so it
becomes ({ isCollapsed = false }, ref) if you remove it, or replace _isMobile
with isMobile and reference it inside the component (e.g., to adjust rendering)
if it’s required.
components/drop-forge/DropForgeStatusPill.tsx (1)

29-29: Nit: Consider a cn/clsx utility for className concatenation.

The inline template literal with conditional classes can become hard to scan. If the project already uses a cn or clsx helper, this would be a cleaner fit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeStatusPill.tsx` at line 29, Replace the long
template literal in the DropForgeStatusPill component's JSX className with the
project's className helper (e.g., cn or clsx): import the helper at the top,
then call cn(...) to combine the base classes, the conditional "tw-cursor-help"
when tooltipText is truthy, and the incoming className prop; target the
className attribute in the DropForgeStatusPill component to make the JSX cleaner
and easier to read.
components/the-memes/TheMemesMint.tsx (1)

34-43: Nit: useMemo depends on entire nft object.

If nft reference isn't stable across renders, the memo provides no benefit. Consider narrowing:

♻️ Narrower deps
  const mintMetadata = useMemo<ManifoldMintMetadata>(
    () => ({
      tokenId: nft.id,
      metadata:
        nft.metadata && typeof nft.metadata === "object"
          ? (nft.metadata as ArweaveMetadata)
          : {},
    }),
-   [nft]
+   [nft.id, nft.metadata]
  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/the-memes/TheMemesMint.tsx` around lines 34 - 43, The useMemo for
mintMetadata currently lists the entire nft object as a dependency, which
defeats memoization if nft's reference changes; update the dependency list for
the useMemo that creates mintMetadata to depend only on stable values used
inside (e.g., nft.id and the specific metadata piece such as nft.metadata or a
serialized form like JSON.stringify(nft.metadata)) so the memo only invalidates
when those actual values change; adjust the dependency array in the mintMetadata
useMemo accordingly.
hooks/useDropForgePermissions.ts (1)

51-58: seizeSettings reference is unstable and causes unnecessary memo recomputation on every settings fetch.

The SeizeSettingsContext provider recreates the entire seizeSettings object on every fetch (via setSeizeSettings({...settings, ...})), invalidating the memo dependency. However, the suggested refactor to depend on specific properties (distribution_admin_wallets, claims_admin_wallets) won't solve the issue—those properties are also recreated with new references on every fetch.

A more effective approach would be to memoize the seizeSettings object itself in the provider or use a deep comparison in the dependency array, rather than swapping to specific properties that remain equally unstable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useDropForgePermissions.ts` around lines 51 - 58, The hook
useDropForgePermissions currently lists seizeSettings as a dependency but
seizeSettings is unstable because the SeizeSettingsContext provider recreates
the object on each fetch; to fix this, make seizeSettings stable in the provider
(wrap the object returned from SeizeSettingsContext in useMemo or ensure its
nested arrays like distribution_admin_wallets and claims_admin_wallets have
stable references) so useDropForgePermissions can safely depend on it, or
alternatively replace the dependency with a deep-compare effect inside
useDropForgePermissions (e.g., useDeepCompareEffect) to avoid recomputation;
update either the SeizeSettingsContext provider (memoize the seizeSettings
object) or the useDropForgePermissions hook (use deep comparison) and reference
seizeSettings, SeizeSettingsContext, and useDropForgePermissions when making the
change.
components/waves/memes/MemesArtSubmissionTraits.tsx (1)

79-105: Remove redundant readOnly re-derivation — getFormSections already applies the override.

getFormSections(userProfile, readOnlyOverrides) merges readOnlyOverrides into each field's readOnly before returning. Lines 80–87 unnecessarily re-derive the same value and spread a new object on every render, which is wasteful even though TraitField's memoization comparison uses value equality on readOnly.

Simplify by passing the field directly (which already has the override applied) and only computing the readOnlyOverride boolean prop:

♻️ Simplified rendering
               {section.fields.map((field, fieldIndex) => {
-                const hasReadOnlyOverride =
-                  readOnlyOverrides?.[field.field] !== undefined;
-                const effectiveReadOnly =
-                  readOnlyOverrides?.[field.field] ?? field.readOnly;
-                const definitionWithReadOnly =
-                  effectiveReadOnly === undefined
-                    ? field
-                    : { ...field, readOnly: effectiveReadOnly };
                 return (
                   <TraitField
                     key={`field-${field.field}-${fieldIndex}`}
-                    definition={definitionWithReadOnly}
-                    {...(hasReadOnlyOverride
-                      ? { readOnlyOverride: Boolean(effectiveReadOnly) }
+                    definition={field}
+                    {...(readOnlyOverrides?.[field.field] !== undefined
+                      ? { readOnlyOverride: Boolean(field.readOnly) }
                       : {})}
                     traits={traits}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/memes/MemesArtSubmissionTraits.tsx` around lines 79 - 105,
getFormSections already merges readOnlyOverrides into each field, so remove the
redundant re-derivation and object spread around field; stop creating
hasReadOnlyOverride/effectiveReadOnly/definitionWithReadOnly and instead pass
the original field directly into TraitField (definition={field}), and only
compute the boolean prop for readOnlyOverride (e.g. when
readOnlyOverrides?.[field.field] !== undefined, pass
readOnlyOverride={Boolean(field.readOnly)}); update the render loop around
TraitField accordingly to avoid recreating objects on every render.
components/drop-forge/DropForgeClaimsListPageClient.tsx (1)

44-48: Prefer shared video URL detection helper instead of duplicating logic.

This local isVideoUrl is narrower than helpers/video.helpers.ts and can drift from the rest of the app behavior over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeClaimsListPageClient.tsx` around lines 44 -
48, Replace the local narrow isVideoUrl implementation with the shared helper
from helpers/video.helpers.ts: remove the isVideoUrl function in
DropForgeClaimsListPageClient.tsx, import the canonical video-detection function
(use the exported name from helpers/video.helpers.ts) and call that helper
wherever isVideoUrl was used so this component uses the app-wide logic and type
signature rather than duplicating detection logic.
components/waves/memes/traits/NumberTrait.tsx (1)

167-170: Consider aligning “filled” state with the component’s configured bounds.

n > 0 hardcodes positivity even though this input is parameterized by min/max. Using the same validity criteria as the blur/change paths would keep filled-state behavior consistent across numeric trait variants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/memes/traits/NumberTrait.tsx` around lines 167 - 170, The
isFieldFilled computation currently treats any positive number as "filled"
(Number.isFinite(n) && n > 0) which ignores the component's min/max
configuration; update isFieldFilled to parse currentInputValue and mark filled
only when the parsed number is finite and within the configured bounds
(respecting min and max) — or reuse the same validation routine used by the
blur/change handlers (the existing onBlur/onChange validation function) so
filled-state logic matches change/blur validity; reference isFieldFilled,
currentInputValue, min, max and the component's blur/change validation when
making the change.
components/waves/memes/traits/TraitField.tsx (1)

42-42: Simplify redundant placeholder conditional.

Line 42 is inside the FieldType.TEXT branch, so the ternary is always true there. You can pass definition.placeholder directly for clarity.

♻️ Suggested cleanup
-        placeholder={definition.type === FieldType.TEXT ? definition.placeholder : undefined}
+        placeholder={definition.placeholder}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/memes/traits/TraitField.tsx` at line 42, Inside
TraitField.tsx, simplify the redundant ternary in the FieldType.TEXT branch by
passing definition.placeholder directly instead of using
placeholder={definition.type === FieldType.TEXT ? definition.placeholder :
undefined}; update the JSX where placeholder is set (within the block handling
FieldType.TEXT) to use placeholder={definition.placeholder} so the code is
clearer and avoids the always-true conditional.
__tests__/components/manifold-minting/ManifoldMintingWidget.test.tsx (1)

59-88: Isolate test cases by clearing mock call history per test.

Add a beforeEach(() => jest.clearAllMocks()) inside the describe block to prevent call-count leakage as this suite grows.

🧪 Suggested hardening
 describe("ManifoldMintingWidget", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
   it("shows mint button after address provided", async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/manifold-minting/ManifoldMintingWidget.test.tsx` around
lines 59 - 88, Add test isolation by inserting a beforeEach hook inside the
describe("ManifoldMintingWidget", ...) block that calls jest.clearAllMocks();
this will reset mock call histories (including writeContract) before each it
test so counts don't leak between tests; locate the describe block in
__tests__/components/manifold-minting/ManifoldMintingWidget.test.tsx and add
beforeEach(() => jest.clearAllMocks()) immediately inside it.
components/drop-forge/DropForgeAccordionSection.tsx (1)

30-39: showHeaderRightWhenOpen and showHeaderRightWhenClosed are mutually exclusive due to else if.

If a caller passes both showHeaderRightWhenOpen={true} and showHeaderRightWhenClosed={true} (intending "always show"), only the showHeaderRightWhenOpen branch executes, so the header-right content hides when closed. Current callers don't set both, so this is fine in practice — just documenting the precedence for future maintainers.

If "always show when both flags are true" is desired
   let showHeaderRight = false;
   if (headerRight) {
-    if (showHeaderRightWhenOpen) {
-      showHeaderRight = isOpen;
-    } else if (showHeaderRightWhenClosed) {
-      showHeaderRight = !isOpen;
-    } else {
+    if (showHeaderRightWhenOpen && showHeaderRightWhenClosed) {
+      showHeaderRight = true;
+    } else if (showHeaderRightWhenOpen) {
+      showHeaderRight = isOpen;
+    } else if (showHeaderRightWhenClosed) {
+      showHeaderRight = !isOpen;
+    } else {
       showHeaderRight = true;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeAccordionSection.tsx` around lines 30 - 39,
The current logic in DropForgeAccordionSection that sets showHeaderRight uses an
else-if chain so when both props showHeaderRightWhenOpen and
showHeaderRightWhenClosed are true it only honors the first; change the
condition around headerRight to explicitly handle the both-true case first
(treat it as always show), then handle the individual flags using
showHeaderRightWhenOpen, showHeaderRightWhenClosed and isOpen to compute
showHeaderRight; ensure you still respect headerRight being falsy to keep
showHeaderRight=false when headerRight is not provided.
__tests__/components/manifold-minting/ManifoldMintingConnect.test.tsx (2)

109-120: if guards around mock callbacks can make assertions vacuous.

Lines 113–115 silently skip calling mockOnWalletSelect if it's null (i.e., RecipientSelector wasn't rendered). If the component doesn't render RecipientSelector in the "mint for me" mode, this test passes without actually exercising the wallet-switch path — the waitFor at Line 117 would then time out or check a stale call.

This same pattern appears at Lines 127, 137, 166, and 181. Replace the if guards with assertions to make failures explicit:

Proposed fix (example for this test)
-    if (mockOnWalletSelect) {
-      mockOnWalletSelect(alternateWallet);
-    }
+    expect(mockOnWalletSelect).not.toBeNull();
+    mockOnWalletSelect!(alternateWallet);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/manifold-minting/ManifoldMintingConnect.test.tsx` around
lines 109 - 120, The test silently skips calling mockOnWalletSelect which makes
the assertion vacuous; replace the conditional guard with an explicit assertion
that mockOnWalletSelect is defined before invoking it (e.g., assert
mockOnWalletSelect is truthy then call mockOnWalletSelect(alternateWallet)) so
failures are explicit; apply the same change for other occurrences referenced in
this file (the calls around lines referencing RecipientSelector /
mockOnWalletSelect at the other tests), ensuring you call mockOnWalletSelect
only after asserting its presence and then waitFor
expect(onMintFor).toHaveBeenLastCalledWith(alternateWallet).

68-85: connectedProfile mock may be missing wallets array.

Line 75 provides connectedProfile with only handle, display, level, and cic. If ManifoldMintingConnect accesses connectedProfile.wallets (e.g., to list user wallets for the "mint for me" selector), the missing property could cause unexpected behavior or mask bugs. Consider adding a wallets array to the mock.

Suggested enhancement
   const auth = {
-    connectedProfile: { handle: "bob", display: "bob", level: 1, cic: 1 },
+    connectedProfile: {
+      handle: "bob",
+      display: "bob",
+      level: 1,
+      cic: 1,
+      wallets: [{ wallet: "0xabc000000000000000000000000000000000abcd" }],
+    },
   } as any;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/manifold-minting/ManifoldMintingConnect.test.tsx` around
lines 68 - 85, The test helper renderConnected provides a connectedProfile
without a wallets array which can hide bugs when ManifoldMintingConnect reads
connectedProfile.wallets; update the mock in renderConnected to include a
realistic wallets array (e.g., one or more wallet objects with expected fields)
so tests exercise the wallet-dependent code paths in ManifoldMintingConnect and
any wallet-selection logic; adjust the returned auth object used by
AuthContext.Provider inside renderConnected to include connectedProfile.wallets
and ensure any assertions or mocks that rely on wallet structure are updated
accordingly.
components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx (1)

374-380: Array reference in dependency array may cause unnecessary effect re-runs.

seizeSettings.distribution_admin_wallets is a new array reference on every settings fetch, since SeizeSettingsContext recreates the seizeSettings object without memoizing individual properties. The effect at line 356 will re-run on every settings update even when the wallets haven't changed. Consider memoizing the distribution_admin_wallets array in the context provider (e.g., via useMemo), or switch to a stable serialized key like JSON.stringify(seizeSettings.distribution_admin_wallets) for comparison.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx`
around lines 374 - 380, The effect in
ReviewDistributionPlanTableSubscriptionFooter that lists
seizeSettings.distribution_admin_wallets in its dependency array is re-running
unnecessarily because SeizeSettingsContext recreates seizeSettings (and thus
distribution_admin_wallets) on each fetch; to fix, either memoize the
distribution_admin_wallets array inside the SeizeSettingsContext provider using
useMemo so the reference is stable, or change the dependency in the useEffect
inside the ReviewDistributionPlanTableSubscriptionFooter component to a stable
serialized key such as JSON.stringify(seizeSettings.distribution_admin_wallets)
(refer to seizeSettings, SeizeSettingsContext, and distribution_admin_wallets
when locating the code).
components/drop-forge/DropForgeCraftClaimPageClient.tsx (2)

2096-2167: Minor: onClose in the lightbox effect deps creates churn.

The parent passes () => setExpandedPhoto(null) inline (line 1959), creating a new function reference each render. This causes the escape-key listener to be torn down and re-attached on every parent render while the lightbox is open. Consider wrapping it in useCallback in the parent or moving the setExpandedPhoto(null) call inside the lightbox.

Stabilize the onClose callback

In the parent (DistributionSection), wrap the handler:

+ const closeLightbox = useCallback(() => setExpandedPhoto(null), []);
  ...
  <DistributionPhotoLightbox
    photo={expandedPhoto}
    photoFileName={...}
-   onClose={() => setExpandedPhoto(null)}
+   onClose={closeLightbox}
  />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeCraftClaimPageClient.tsx` around lines 2096 -
2167, The effect in DistributionPhotoLightbox re-attaches the Escape key
listener because onClose is passed as a new inline function from the parent;
stabilize it by either wrapping the parent's handler in useCallback (the
parent’s setExpandedPhoto(...) caller) or move the closing logic into the
lightbox: replace uses of the incoming onClose in DistributionPhotoLightbox (the
effect and click handlers) with a stable internal function that calls
setExpandedPhoto(null) (or call a memoized prop from the parent) so the
dependency array no longer includes a changing function reference and the key
listener is not repeatedly torn down and re-attached.

1745-1759: Multiple property-name fallbacks suggest the API type could be tighter.

getAllowlistAddresses and getAllowlistTotal access summary.addresses ?? summary.addresses_count and summary.total ?? summary.total_spots. The same pattern appears in renderPhaseSummaryBox with total_airdrops. If ClaimPhaseSummaryItem genuinely supports all these variants, adding an adapter/normalizer would centralize the mapping and avoid scattershot fallbacks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeCraftClaimPageClient.tsx` around lines 1745 -
1759, Create a small normalizer that converts a ClaimPhaseSummaryItem variants
into a canonical shape (e.g., { addresses, total, total_airdrops }) and use it
from getAllowlistAddresses, getAllowlistTotal and renderPhaseSummaryBox;
specifically, add a function (e.g., normalizeClaimPhaseSummary) that reads
addresses/addresses_count and total/total_spots (and total_airdrops variants)
and returns consistent numeric fields, then have getAllowlistAddresses call
normalizeClaimPhaseSummary(summary).addresses and getAllowlistTotal call
normalizeClaimPhaseSummary(summary).total (and update renderPhaseSummaryBox to
read normalized.total_airdrops) so you centralize the mapping instead of using
?? fallbacks throughout.
components/drop-forge/DropForgeLaunchClaimPageClient.tsx (4)

602-705: LaunchAccordionSection duplicates DropForgeAccordionSection.

The Craft page imports DropForgeAccordionSection from a shared module, while this file defines a local LaunchAccordionSection with overlapping functionality (accordion with tone, headerRight, disabled state). Consider extending DropForgeAccordionSection to support the additional props (tone, disabled, onOpen) instead of maintaining two similar implementations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeLaunchClaimPageClient.tsx` around lines 602 -
705, Remove the local LaunchAccordionSection implementation and reuse the shared
DropForgeAccordionSection by adding the missing props and behavior to it: extend
DropForgeAccordionSection to accept tone, disabled, onOpen, headerRight,
showHeaderRightWhenOpen and defaultOpen, implement the same open/close state
logic (including calling onOpen when opening), aria/disabled handling, icon
rotation classes (reuse toneClass for the subtitle badge), and the same
collapsed/expanded DOM structure (SECTION_CARD_CLASS and children wrapper) so
all callers of LaunchAccordionSection can import DropForgeAccordionSection
instead; update any local uses to pass the new props and remove the duplicated
LaunchAccordionSection symbol.

174-194: Consider typing the API response instead of as unknown as casts.

getRootAddressesCount and getRootTotalSpots both use double-cast (as unknown as) to access addresses_count/addresses and total_spots/total. This works but hides the actual shape of MintingClaimsRootItem. If the generated type can be extended, adding those optional fields to MintingClaimsRootItem would be safer and more discoverable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeLaunchClaimPageClient.tsx` around lines 174 -
194, The helper functions getRootAddressesCount and getRootTotalSpots are using
double-casts to access properties that should be part of the
MintingClaimsRootItem shape; instead extend or update the MintingClaimsRootItem
type to include optional fields addresses_count, addresses, total_spots, and
total, then remove the "as unknown as" casts and read those properties directly
in getRootAddressesCount and getRootTotalSpots (keep the same null checks and
typeof number guards). Update the API/type generation or the local type
declaration where MintingClaimsRootItem is defined so the compiler sees those
optional fields.

1016-1042: Effect with state objects in the dependency array could cause redundant runs.

subscriptionAirdropsByPhase and subscriptionAirdropsLoadingByPhase are objects in the dependency array, and every state update creates a new object reference. The guard at lines 1029–1031 prevents actual re-fetching, so this is safe but causes the effect body to execute on each state change. Not a correctness issue, but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeLaunchClaimPageClient.tsx` around lines 1016 -
1042, The effect runs on every state update because object references
subscriptionAirdropsByPhase and subscriptionAirdropsLoadingByPhase are in the
dependency array; change the dependencies to only the specific primitive values
the effect cares about (e.g. selectedPhase plus the lookup results for that
phase) or derive those lookups with useMemo and depend on the memoized
primitives instead; update the useEffect that references selectedPhase,
subscriptionAirdropsByPhase, subscriptionAirdropsLoadingByPhase, and
fetchSubscriptionAirdropsForPhase so it depends on selectedPhase and
subscriptionAirdropsByPhase[selectedPhase] /
subscriptionAirdropsLoadingByPhase[selectedPhase] (or their memoized
equivalents) to avoid redundant runs while keeping the existing guard and
fetchSubscriptionAirdropsForPhase call intact.

707-770: Consider splitting this ~2600-line file into smaller modules.

The main component alone has ~35 useState hooks and ~15 useEffect hooks. Extracting the utility functions (lines 101–600) to a shared helpers module, and the ClaimTransactionModal and LaunchAccordionSection to separate component files, would improve maintainability and testability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeLaunchClaimPageClient.tsx` around lines 707 -
770, The DropForgeLaunchClaimPageClient component is too large and hard to
maintain; split it into smaller modules by extracting utility functions and
heavy subcomponents: move helper functions (the utilities used by
DropForgeLaunchClaimPageClient, originally in the top-of-file region) into a new
helpers module and import them into DropForgeLaunchClaimPageClient, and extract
the ClaimTransactionModal and LaunchAccordionSection JSX/logic into standalone
components (e.g., ClaimTransactionModal.tsx and LaunchAccordionSection.tsx) that
accept props/state handlers from DropForgeLaunchClaimPageClient; ensure you keep
the same prop and ref names (e.g., claim, setClaim, claimTxModal,
setClaimTxModal, handledClaimWriteSuccessTxHashRef) and move related
state-management hooks only when appropriate (lift shared state into the parent
and pass handlers down) so DropForgeLaunchClaimPageClient remains the
orchestrator while the new modules contain presentation and helper logic.

Comment thread __tests__/components/ManifoldMintingWidget.test.tsx
Comment thread __tests__/hooks/useManifoldClaim.test.tsx
Comment thread app/drop-forge/launch/[id]/page.tsx Outdated
Comment thread components/drop-forge/claimTraitsData.ts
Comment thread components/drop-forge/drop-forge-status.helpers.ts Outdated
Comment thread hooks/useManifoldClaim.ts Outdated
Comment thread openapi.yaml
Comment thread openapi.yaml
Comment thread services/api/memes-minting-claims-api.ts
Comment thread utils/appkit-initialization.utils.ts
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/manifold-minting/ManifoldMintingWidget.tsx (1)

413-424: ⚠️ Potential issue | 🟡 Minor

Number.parseInt("") returns NaN, which can crash BigInt() at mint time.

If the user clears this input, mintCount becomes NaN. Later, getValue() returns NaN and BigInt(NaN) throws a TypeError. Consider defaulting to 0 on parse failure.

Proposed fix
-        onChange={(e) => setMintCount(Number.parseInt(e.target.value))}
+        onChange={(e) => {
+          const parsed = Number.parseInt(e.target.value);
+          setMintCount(Number.isNaN(parsed) ? 0 : parsed);
+        }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifold-minting/ManifoldMintingWidget.tsx` around lines 413 -
424, The input handler in printMintCountInput can set mintCount to NaN when the
field is cleared, which later makes getValue() return NaN and BigInt(NaN) throw;
change the onChange in printMintCountInput to parse the value and default to 0
on failure (e.g., const v = Number.parseInt(e.target.value);
setMintCount(Number.isNaN(v) ? 0 : v)), and also add a defensive fallback in the
code that uses mintCount (getValue()/where BigInt is called) to use 0 when
mintCount is falsy or NaN (e.g., BigInt(Number.isNaN(mintCount) ? 0 :
mintCount)) so BigInt never receives NaN.
♻️ Duplicate comments (7)
__tests__/hooks/useManifoldClaim.test.tsx (1)

38-51: Past issue resolved — hook now correctly invoked with object params and claims accessed via result.current.claim.

The test correctly uses the new API shape and the assertions target the nested claim property. The mock data structure (tuple with [instanceId, claimObject]) aligns with the getClaimForToken path (chainId=1 → mainnet).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/hooks/useManifoldClaim.test.tsx` around lines 38 - 51, Update the
test to invoke useManifoldClaim with the object-shaped params and assert the
nested claim fields: ensure the renderHook call uses useManifoldClaim({ chainId:
1, contract: "0x1", proxy: "0x2", abi: [], identifier: 1 }) and that assertions
read from result.current.claim (e.g.,
expect(result.current.claim?.status).toBe(ManifoldClaimStatus.ACTIVE),
expect(result.current.claim?.phase).toBe(ManifoldPhase.PUBLIC),
expect(result.current.claim?.remaining).toBe(1)); also ensure your mock for
getClaimForToken returns the tuple [instanceId, claimObject] for chainId 1 so
the hook resolves the correct claim shape.
components/providers/AppKitAdapterManager.ts (2)

14-14: ⚠️ Potential issue | 🟠 Major

Chain type must be imported from viem, not viem/chains.

Same incorrect import as was fixed in WagmiSetup.tsx and flagged in utils/appkit-initialization.utils.ts. viem/chains exports chain instances; the Chain TypeScript type belongs at the viem root. mainnet stays in viem/chains.

🐛 Proposed fix
-import { Chain, mainnet } from "viem/chains";
+import type { Chain } from "viem";
+import { mainnet } from "viem/chains";
Does viem/chains export the Chain type, or is it only exported from the viem root package?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/providers/AppKitAdapterManager.ts` at line 14, The import
currently pulls the Chain type from "viem/chains"; update the import so the
TypeScript type Chain is imported from the viem root and mainnet remains
imported from "viem/chains" — locate the import line in AppKitAdapterManager.ts
(referencing Chain and mainnet) and split it into two imports: one importing
Chain from "viem" and another importing mainnet from "viem/chains".

14-14: ⚠️ Potential issue | 🟠 Major

Chain type must be imported from viem, not viem/chains.

Same incorrect import as utils/appkit-initialization.utils.ts Line 11 and the prior issue already fixed in WagmiSetup.tsx. The canonical form shown in viem/Wagmi docs is import { type Chain } from 'viem'viem/chains exports chain objects, not the Chain type. mainnet correctly stays in viem/chains.

🐛 Proposed fix
-import { Chain, mainnet } from "viem/chains";
+import type { Chain } from "viem";
+import { mainnet } from "viem/chains";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/providers/AppKitAdapterManager.ts` at line 14, The import is
pulling the Chain type from the wrong module; change the import so the type
symbol Chain is imported from 'viem' while keeping mainnet imported from
'viem/chains' (i.e., replace import of Chain from "viem/chains" with importing
type Chain from "viem" and leave mainnet from "viem/chains") so references to
the Chain type in AppKitAdapterManager (and any other places using Chain) use
the correct type definition.
components/header/AppUserConnect.tsx (1)

47-56: Chain-switching button integration looks good.

The conditional rendering, aria-label, and use of the centralized useChainSwitcher hook are clean. This properly addresses the prior review feedback about cycling through all chains.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header/AppUserConnect.tsx` around lines 47 - 56, No change
required: the chain-switching button correctly conditionally renders when
chains.length > 1, uses the centralized useChainSwitcher via switchToNextChain,
includes an accessible aria-label "Switch Chain", displays currentChainName, and
uses ArrowsRightLeftIcon; keep this implementation as-is.
components/layout/sidebar/WebSidebarNav.tsx (1)

48-48: Drop Forge sidebar integration using useDropForgePermissions — LGTM.

This properly uses the consolidated canAccessLanding from useDropForgePermissions, addressing the prior review feedback about duplicating permission checks. The nav item is correctly gated and positioned.

Also applies to: 349-362

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/sidebar/WebSidebarNav.tsx` at line 48, No code changes
required: the Drop Forge sidebar integration in WebSidebarNav.tsx correctly uses
the consolidated permission hook useDropForgePermissions and its
canAccessLanding alias (showDropForge) to gate the nav item; keep the existing
const { canAccessLanding: showDropForge } = useDropForgePermissions() and the
conditional rendering around the Drop Forge nav entry as-is.
openapi.yaml (2)

2405-2409: ⚠️ Potential issue | 🟡 Minor

Require a non-empty PATCH payload for patchMintingClaim.

At Line 2405 the requestBody is not required, and at Line 11713 MintingClaimUpdateRequest has no required fields or minProperties, so empty/no-op PATCH requests are valid.

Proposed fix
   /minting-claims/{contract}/claims/{claim_id}:
     patch:
       requestBody:
+        required: true
         content:
           application/json:
             schema:
               $ref: "#/components/schemas/MintingClaimUpdateRequest"

     MintingClaimUpdateRequest:
       type: object
+      minProperties: 1
       properties:
         edition_size:
           type: integer
#!/bin/bash
# Verify requestBody required flag and minProperties for PATCH schema
sed -n '2398,2413p' openapi.yaml
sed -n '11710,11740p' openapi.yaml

Also applies to: 11712-11737

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 2405 - 2409, The PATCH operation patchMintingClaim
currently allows empty payloads because its requestBody is not marked required
and the schema MintingClaimUpdateRequest has no required fields or
minProperties; update the operation to set requestBody.required: true for
patchMintingClaim and modify the MintingClaimUpdateRequest schema to include
either specific required properties or add minProperties: 1 so a non-empty JSON
object is enforced.

11510-11557: ⚠️ Potential issue | 🟠 Major

animation_details union is still ambiguous for validators/codegen.

At Line 11511, oneOf has no discriminator, and at Line 11556 the video format is unconstrained (type: string). This can produce nondeterministic matching.

Proposed fix
         animation_details:
           oneOf:
             - $ref: "#/components/schemas/MintingClaimAnimationDetailsVideo"
             - $ref: "#/components/schemas/MintingClaimAnimationDetailsHtml"
             - $ref: "#/components/schemas/MintingClaimAnimationDetailsGlb"
+          discriminator:
+            propertyName: format
+            mapping:
+              VIDEO: "#/components/schemas/MintingClaimAnimationDetailsVideo"
+              HTML: "#/components/schemas/MintingClaimAnimationDetailsHtml"
+              GLB: "#/components/schemas/MintingClaimAnimationDetailsGlb"
           nullable: true

     MintingClaimAnimationDetailsVideo:
       type: object
@@
         format:
           type: string
+          enum:
+            - VIDEO
#!/bin/bash
# Verify oneOf/discriminator and video format constraints
sed -n '11506,11520p' openapi.yaml
sed -n '11540,11562p' openapi.yaml
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 11510 - 11557, The oneOf union animation_details
is ambiguous for validators/codegen; add an OpenAPI discriminator on the parent
schema and constrain the video format so type resolvers can pick the correct
variant: update the animation_details schema to include a discriminator with
propertyName: "format" and mapping entries pointing format values to the
concrete schemas (e.g., "GLB" ->
"#/components/schemas/MintingClaimAnimationDetailsGlb", "HTML" ->
"#/components/schemas/MintingClaimAnimationDetailsHtml", and the chosen video
format tokens -> "#/components/schemas/MintingClaimAnimationDetailsVideo"), then
modify MintingClaimAnimationDetailsVideo.format to be an enum of the allowed
video formats (e.g., MP4, WEBM, MOV or whatever your system accepts) so the
discriminator values match the mapping and the oneOf selection is deterministic.
🟡 Minor comments (10)
components/manifold-minting/ManifoldMintingWidget.tsx-318-328 (1)

318-328: ⚠️ Potential issue | 🟡 Minor

Missing short-error fallback — inconsistent with mintWrite.error handler.

The mintWrite.error handler (lines 302–316) checks resolvedError.length < 5 and falls back to the full message for overly short/empty parsed strings. This handler lacks the same guard, so if waitMintWrite.error.message parses down to an empty or very short string, the user will see a blank or unhelpful error.

Proposed fix
  useEffect(() => {
    if (waitMintWrite.error) {
      setMintStatus(<></>);
      const resolvedError =
        waitMintWrite.error.message
          ?.split("Request Arguments")[0]
          ?.split(".")[0]
-          ?.split("Contract Call")[0] ?? waitMintWrite.error.message;
-      setMintError(resolvedError);
+          ?.split("Contract Call")[0];
+      if (!resolvedError || resolvedError.length < 5) {
+        setMintError(waitMintWrite.error.message);
+      } else {
+        setMintError(resolvedError);
+      }
    }
  }, [waitMintWrite.error]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifold-minting/ManifoldMintingWidget.tsx` around lines 318 -
328, The waitMintWrite.error handler builds a parsed resolvedError from
waitMintWrite.error.message but lacks the same short-string fallback used by the
mintWrite.error handler; update the useEffect handling waitMintWrite.error (the
block that calls setMintStatus and setMintError) to check if the parsed
resolvedError length is less than 5 (or empty) and in that case fall back to the
original waitMintWrite.error.message before calling setMintError so users don’t
see a blank/too-short message.
components/navigation/NavItem.tsx-47-54 (1)

47-54: ⚠️ Potential issue | 🟡 Minor

Reset the title when unread notifications become zero.

At Line 49-54, the effect sets title only in the unread branch. If unread transitions from >0 to 0, the previous "(N) Notifications | 6529.io" title can persist until another setter overrides it.

Suggested adjustment
 useEffect(() => {
   if (item.name !== "Notifications") return;
-  if (haveUnreadNotifications) {
-    setTitle(`(${notifications?.unread_count}) Notifications | 6529.io`);
-  }
-  if (!haveUnreadNotifications) {
+  const unreadCount = notifications?.unread_count ?? 0;
+  if (haveUnreadNotifications && unreadCount > 0) {
+    setTitle(`(${unreadCount}) Notifications | 6529.io`);
+  } else {
+    setTitle("6529.io");
     removeAllDeliveredNotifications();
   }
 }, [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/navigation/NavItem.tsx` around lines 47 - 54, The useEffect
watching notifications only sets the document title when haveUnreadNotifications
is true, so when unread transitions to 0 the previous "(N) Notifications |
6529.io" can remain; update the effect in NavItem (the useEffect that checks
item.name === "Notifications") to explicitly reset the title (e.g., to
"Notifications | 6529.io") when haveUnreadNotifications is false (in the same
branch that calls removeAllDeliveredNotifications), ensuring you still call
removeAllDeliveredNotifications() and use notifications?.unread_count when
constructing the title in the unread branch.
components/drops/view/item/content/media/MediaDisplayGLB.tsx-130-131 (1)

130-131: ⚠️ Potential issue | 🟡 Minor

Avoid static tooltip IDs in a reusable component.

"glb-cube-tooltip" is hard-coded for both the trigger and tooltip (Line [130], Line [173]). If multiple MediaDisplayGLB instances render simultaneously, IDs collide and tooltip targeting becomes unreliable. Please generate a per-instance id and reuse it in both places.

Also applies to: 173-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drops/view/item/content/media/MediaDisplayGLB.tsx` around lines
130 - 131, The component MediaDisplayGLB uses a hard-coded tooltip ID
("glb-cube-tooltip") for both the trigger and the tooltip which will collide
when multiple instances mount; change it to generate a per-instance ID (e.g.,
with React's useId() or a useRef(nanoid()) inside MediaDisplayGLB) and replace
all occurrences of the static string (data-tooltip-id and the tooltip id prop)
with that generated id so each instance targets its own tooltip.
components/drops/view/item/content/media/MediaDisplayGLB.tsx-122-131 (1)

122-131: ⚠️ Potential issue | 🟡 Minor

Set type="button" on the cube toggle.

Line [122] omits an explicit button type. Inside a form, this defaults to submit and can trigger unintended form submission.

💡 Proposed fix
           <button
             onClick={handleCubeToggle}
+            type="button"
             className={`tw-flex tw-h-9 tw-w-9 tw-items-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-transition-all tw-duration-200 ${
               isActive
                 ? "tw-border-primary-600 tw-bg-primary-500 tw-text-white tw-shadow-lg tw-shadow-primary-500/50 hover:tw-bg-primary-600"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drops/view/item/content/media/MediaDisplayGLB.tsx` around lines
122 - 131, The toggle button in the MediaDisplayGLB component lacks an explicit
type which makes it act as a submit button inside forms; update the button
element used for the cube toggle (the one with onClick={handleCubeToggle} and
data-tooltip-id="glb-cube-tooltip") to include type="button" so it won't trigger
form submission when clicked.
components/drop-forge/DropForgeLaunchClaimPageClient.tsx-286-286 (1)

286-286: ⚠️ Potential issue | 🟡 Minor

Use a trimmed check for published metadata state.

Line 286 treats empty-string metadata as published. This can expose launch controls prematurely.

🔧 Suggested fix
-  const hasPublishedMetadata = Boolean(claim?.metadata_location != null);
+  const hasPublishedMetadata = Boolean(claim?.metadata_location?.trim());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeLaunchClaimPageClient.tsx` at line 286, The
current published-metadata check in hasPublishedMetadata uses
Boolean(claim?.metadata_location != null) which treats an empty string as
published; update the check to ensure claim?.metadata_location is a non-empty,
non-whitespace string by trimming and testing length (e.g., use
claim?.metadata_location?.trim() and verify it's not an empty string) so
hasPublishedMetadata only becomes true when metadata_location contains real
content.
components/drop-forge/DropForgeCraftClaimPageClient.tsx-1483-1485 (1)

1483-1485: ⚠️ Potential issue | 🟡 Minor

Treat empty metadata values as unpublished.

Line 1484 considers "" as published metadata. Use a trimmed non-empty check for consistency with launch-status logic.

🔧 Suggested fix
-  const hasPublishedMetadata = claim.metadata_location != null;
+  const hasPublishedMetadata = Boolean(claim.metadata_location?.trim());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeCraftClaimPageClient.tsx` around lines 1483 -
1485, The check for published metadata treats an empty string as published;
update hasPublishedMetadata so it verifies claim.metadata_location is non-null
and non-empty after trimming (e.g., use a trimmed non-empty check on
claim.metadata_location) to match the launch-status logic; ensure any downstream
logic that uses hasPublishedMetadata (such as canPublish) uses this updated
boolean.
components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts-306-329 (1)

306-329: ⚠️ Potential issue | 🟡 Minor

getAnimationMimeType unconditionally falls back to "video/mp4" for unrecognised formats.

If format is a non-video string (e.g. "UNKNOWN") that isn't in formatMimeMap, line 322 returns "video/mp4". Similarly, the final return on line 328 assumes video for any URL that doesn't match known extensions. This could cause incorrect <source type="…"> attributes for non-video content, leading to playback failures or misleading metadata.

Consider returning null for truly unrecognised formats so callers can decide how to handle unknown media.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts` around lines
306 - 329, The function getAnimationMimeType currently defaults to "video/mp4"
for unknown formats and URLs; change it to return null for truly unrecognised
media so callers can handle unknown types. Specifically, in getAnimationMimeType
when reading claim.animation_details.format (and after checking "HTML" and
"GLB"), if format exists but is not in formatMimeMap (and not matched by
normalizedFormat), return null instead of "video/mp4"; likewise, after the
extension checks and isVideoUrl check, return null as the final fallback rather
than "video/mp4". Keep existing checks for HTML/GLB, formatMimeMap, isVideoUrl,
and the .endsWith(...) extension checks but change the default returns to null.
components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts-344-352 (1)

344-352: ⚠️ Potential issue | 🟡 Minor

toArweaveUrl doesn't validate the Arweave transaction ID format.

Any non-empty, non-URL string will be appended to https://arweave.net/. Malicious or malformed input (e.g. "../../foo", "<script>") could produce unexpected URLs. Consider basic validation (e.g. checking the string is a valid base64url Arweave TX ID of 43 characters) or at minimum encoding the path segment.

Minimal hardening
   if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
     return trimmed;
   }
-  return `https://arweave.net/${trimmed}`;
+  return `https://arweave.net/${encodeURIComponent(trimmed)}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts` around lines
344 - 352, The toArweaveUrl function currently appends any non-URL string to
https://arweave.net/, so validate the path before returning: inside
toArweaveUrl, after computing trimmed, first allow full URLs as now, otherwise
verify trimmed matches the Arweave TX ID pattern (a 43-character base64url
string) using a regexp (e.g., /^[A-Za-z0-9-_]{43}$/) and only then return
`https://arweave.net/${trimmed}`; if it fails validation return null (or
alternatively return `https://arweave.net/${encodeURIComponent(trimmed)}` if you
prefer encoding over rejecting). Ensure you reference the trimmed variable and
keep the early returns for null intact.
components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts-48-55 (1)

48-55: ⚠️ Potential issue | 🟡 Minor

isNotFoundError has a case-sensitivity inconsistency.

Lines 51–52 compare against the lowercased normalized, but line 53 compares against the original message with exact casing ("Claim not found"). If the actual message is "claim not found" it won't match line 53, and if it's "Claim not found" it already matches line 51. The exact-case check on line 53 is therefore either dead code or a latent bug.

Proposed fix
 export function isNotFoundError(message: string): boolean {
   const normalized = message.toLowerCase();
   return (
     normalized.includes("not found") ||
-    normalized.includes("status code 404") ||
-    message === "Claim not found"
+    normalized.includes("status code 404")
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts` around lines
48 - 55, The isNotFoundError function mixes casing by comparing normalized
(lowercased) values for two checks but uses the original message for the exact
match "Claim not found"; fix by using the normalized string for that comparison
(e.g., compare normalized === "claim not found" or remove the redundant exact
match since normalized.includes("not found") already covers it) so all
comparisons are case-insensitive; update the function isNotFoundError
accordingly.
components/header/user/proxy/HeaderUserProxyDropdown.tsx-65-66 (1)

65-66: ⚠️ Potential issue | 🟡 Minor

useEffect dependency array is incomplete and the pattern can be simplified with useMemo.

getLabel() also reads activeProfileProxy (indirectly via its closure), but the effect only depends on [profile, address]. If activeProfileProxy changes, the label won't update. Additionally, the useState + useEffect combo for derived state is a React anti-pattern — useMemo is more appropriate here.

Proposed fix
-  const [label, setLabel] = useState(getLabel());
-  useEffect(() => setLabel(getLabel()), [profile, address]);
+  const label = useMemo(getLabel, [profile, address, activeProfileProxy]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header/user/proxy/HeaderUserProxyDropdown.tsx` around lines 65 -
66, The label derivation in HeaderUserProxyDropdown uses useState + useEffect
and calls getLabel() which also reads activeProfileProxy via closure, but the
effect only depends on [profile, address], so label can become stale; replace
the useState/setLabel + useEffect pattern with a useMemo that returns getLabel()
(or the same label logic) and include all dependencies (profile, address,
activeProfileProxy) in the dependency array so the label updates whenever any of
those change.
🧹 Nitpick comments (24)
components/home/now-minting/NowMintingStatsGrid.tsx (1)

33-33: Prefer removing the mainnet fallback here.

Line 33 can pass contract directly. The ?? MEMES_CONTRACT fallback is redundant with current typing and may mask future config issues.

♻️ Suggested cleanup
-import { MEMES_CONTRACT } from "@/constants/constants";
@@
-    contract: contract ?? MEMES_CONTRACT,
+    contract,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/home/now-minting/NowMintingStatsGrid.tsx` at line 33, Remove the
mainnet fallback and pass the incoming prop directly: replace the object entry
that currently uses "contract: contract ?? MEMES_CONTRACT" with a direct
"contract: contract" (or simply rely on the prop where used) inside the
NowMintingStatsGrid component so the MEMES_CONTRACT fallback is not masked;
update any call sites or prop typings if needed to ensure contract remains
required/validated.
components/user/subscriptions/UserPageSubscriptionsTopUp.tsx (1)

48-61: confirm_wallet and submitted share the same emoji — verify if intentional; also consider extracting the union type.

Two separate things worth a quick look:

  1. Shared emoji: Both confirm_wallet and submitted resolve to /emojis/sgt_flushed.webp. If different visual feedback was intended for these two states (e.g., a spinner-adjacent face for "pending wallet action" vs. a "transaction in flight" face), this should be updated.

  2. Duplicated union type: The literal union "confirm_wallet" | "submitted" | "success" | "error" is written twice — once in the parameter and again as the Record key. Extract it to a named type to keep it DRY.

  3. The function wrapper is unnecessary — the body is a static lookup; a const map is clearer.

♻️ Proposed simplification
-function getTopUpModalEmoji(
-  status: "confirm_wallet" | "submitted" | "success" | "error"
-): string {
-  const emojiByStatus: Record<
-    "confirm_wallet" | "submitted" | "success" | "error",
-    string
-  > = {
-    confirm_wallet: "/emojis/sgt_flushed.webp",
-    submitted: "/emojis/sgt_flushed.webp",
-    success: "/emojis/sgt_saluting_face.webp",
-    error: "/emojis/sgt_sob.webp",
-  };
-  return emojiByStatus[status];
-}
+type TopUpModalStatus = "confirm_wallet" | "submitted" | "success" | "error";
+
+const TOP_UP_MODAL_EMOJI: Record<TopUpModalStatus, string> = {
+  confirm_wallet: "/emojis/sgt_flushed.webp",
+  submitted: "/emojis/sgt_flushed.webp",   // ← same as confirm_wallet; intentional?
+  success: "/emojis/sgt_saluting_face.webp",
+  error: "/emojis/sgt_sob.webp",
+};

Call sites become TOP_UP_MODAL_EMOJI["error"], etc.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/user/subscriptions/UserPageSubscriptionsTopUp.tsx` around lines 48
- 61, Replace the duplicated union and function with a single named type and
constant map: extract the union "confirm_wallet" | "submitted" | "success" |
"error" into a type alias (e.g., TopUpModalStatus), create a const
TOP_UP_MODAL_EMOJI: Record<TopUpModalStatus,string> containing the emoji paths,
and remove getTopUpModalEmoji in favor of direct lookups
(TOP_UP_MODAL_EMOJI[status]); while doing this, verify whether confirm_wallet
and submitted should share "/emojis/sgt_flushed.webp" and, if not, update the
appropriate entry in TOP_UP_MODAL_EMOJI to the intended emoji.
components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx (1)

545-550: Inconsistency: render guard bypasses the null-safe distributionAdminWallets memo.

The useEffect guard (line 366) correctly uses the memoized distributionAdminWallets (which falls back to []), but this render guard calls isSubscriptionsAdmin with the raw seizeSettings.distribution_admin_wallets. The two call sites should use the same value for consistency; using the memoized variable here also makes any future null-safety fix automatically apply to both paths.

♻️ Proposed fix
- if (
-   !isSubscriptionsAdmin(
-     connectedProfile,
-     seizeSettings.distribution_admin_wallets
-   )
- ) {
+ if (!isSubscriptionsAdmin(connectedProfile, distributionAdminWallets)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/distribution-plan-tool/review-distribution-plan/table/ReviewDistributionPlanTableSubscriptionFooter.tsx`
around lines 545 - 550, The render guard currently calls
isSubscriptionsAdmin(connectedProfile, seizeSettings.distribution_admin_wallets)
bypassing the null-safe memo; update that call to use the memoized
distributionAdminWallets instead (i.e., isSubscriptionsAdmin(connectedProfile,
distributionAdminWallets)) so both the useEffect and render guard use the same
null-safe value and future null-safety fixes apply consistently; locate the
check around the if(...) block and replace the second argument accordingly,
keeping connectedProfile and the isSubscriptionsAdmin invocation intact.
components/manifold-minting/ManifoldMintingWidget.tsx (3)

17-17: Use import type for type-only import.

Chain is only used as a type annotation (line 32). Other type-only imports in this file already use import type (lines 10, 13). This should be consistent.

Proposed fix
-import { Chain } from "viem";
+import type { Chain } from "viem";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifold-minting/ManifoldMintingWidget.tsx` at line 17, Change the
value import of Chain to a type-only import: replace the current import of Chain
with an "import type { Chain }" so it is treated as a type-only import used in
the ManifoldMintingWidget component (Chain is only used as a type annotation
around the props/variable on line where Chain is referenced). This keeps import
style consistent with the other type-only imports in the file and avoids
emitting a runtime import for the Chain symbol.

57-96: mintWrite used inside effect but missing from dependency array.

Line 58 calls mintWrite.reset(), but mintWrite is not listed in the dependency array (lines 91–96). This will trigger an react-hooks/exhaustive-deps lint warning. Including mintWrite directly would cause re-runs on every render (wagmi returns a new object), so the common workaround is to extract reset into a stable ref or suppress the warning with an inline comment. This is a known trade-off and not a bug, but worth acknowledging with a suppression comment to avoid future confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifold-minting/ManifoldMintingWidget.tsx` around lines 57 - 96,
The useEffect in ManifoldMintingWidget calls mintWrite.reset() but mintWrite is
omitted from the dependency array, triggering react-hooks/exhaustive-deps; fix
by either extracting the reset function into a stable reference (e.g., const
resetMintRef = useRef(() => mintWrite.reset()) and call resetMintRef.current()
inside the effect) or explicitly suppress the lint rule with an inline comment
above the dependency array, and keep references to mintWrite.reset, the
useEffect block, and the dependency array unchanged otherwise so the intent is
clear.

199-202: Redundant ?? [] after .map() calls.

Array.prototype.map() always returns an array, so the nullish coalescing fallback on lines 200 and 201 is dead code. Removing it improves clarity.

Proposed fix
       mintArgs.push(
-          selectedMerkleProofs.map((mp) => mp.value) ?? [],
-          selectedMerkleProofs.map((mp) => mp.merkle_proof) ?? []
+          selectedMerkleProofs.map((mp) => mp.value),
+          selectedMerkleProofs.map((mp) => mp.merkle_proof)
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifold-minting/ManifoldMintingWidget.tsx` around lines 199 -
202, The call to Array.prototype.map on selectedMerkleProofs returns an array,
so remove the redundant nullish coalescing fallbacks after those maps in the
mintArgs.push call; update the mintArgs.push invocation that uses
selectedMerkleProofs.map((mp) => mp.value) and selectedMerkleProofs.map((mp) =>
mp.merkle_proof) to omit the trailing "?? []" so the push uses the mapped arrays
directly.
__tests__/components/navigation/BottomNavigation.test.tsx (1)

45-69: Add assertions for the new isCurrentWaveDm prop wiring.

The test scaffolding mocks useWave, but it never verifies the value passed to NavItem. Add an assertion for both false and true to lock in this PR behavior.

Test coverage enhancement
 describe('BottomNavigation', () => {
   it('registers mobileNav ref and renders nav items', () => {
@@
     const notificationsItem = passedItems.find((item: { name: string }) => item.name === 'Notifications');
     expect(notificationsItem?.href).toBe(getNotificationsRoute(false));
+    expect(navItemCalls.every((call) => call[0].isCurrentWaveDm === false)).toBe(true);
   });
+
+  it('passes DM state to NavItem when current wave is DM', () => {
+    (useWave as jest.Mock).mockReturnValue({ isDm: true });
+    render(<BottomNavigation />);
+    const navItemCalls = (NavItem as jest.Mock).mock.calls;
+    expect(navItemCalls).toHaveLength(7);
+    expect(navItemCalls.every((call) => call[0].isCurrentWaveDm === true)).toBe(true);
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/navigation/BottomNavigation.test.tsx` around lines 45 -
69, The test must assert that BottomNavigation wires the useWave value into
NavItem via the isCurrentWaveDm prop; update the test in
BottomNavigation.test.tsx to inspect (NavItem as jest.Mock).mock.calls for the
nav item whose name is 'Messages' (or for all calls) and add expectations that
.isCurrentWaveDm is false for the current mocked useWave state, then re-mock
useWave to return the alternate state and re-render BottomNavigation and assert
.isCurrentWaveDm is true; reference NavItem, useWave, BottomNavigation, and
registerRef to locate the code and ensure you check the nav call arguments'
isCurrentWaveDm property in both false and true scenarios.
components/providers/WagmiSetup.tsx (2)

103-103: Consider hoisting enableTestnet outside the component.

publicEnv is a module-level constant, so enableTestnet never changes at runtime. Declaring it inside the component body makes it look like reactive state and adds a redundant entry to the useCallback dep array at Line 157.

♻️ Proposed refactor
+const enableTestnet = publicEnv.DROP_FORGE_TESTNET === true;
+
 export default function WagmiSetup({
   children,
 }: {
   readonly children: React.ReactNode;
 }) {
-  const enableTestnet = publicEnv.DROP_FORGE_TESTNET === true;
-
   const appWalletPasswordModal = useAppWalletPasswordModal();

Then remove enableTestnet from the useCallback dependency array:

-    [adapterManager, isCapacitor, enableTestnet]
+    [adapterManager, isCapacitor]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/providers/WagmiSetup.tsx` at line 103, Hoist the computed boolean
enableTestnet out of the WagmiSetup component to module scope since publicEnv is
module-level and never changes; move the declaration next to publicEnv and then
remove enableTestnet from the dependency array of the useCallback inside
WagmiSetup so the callback deps no longer include a non-reactive value.

103-103: Consider hoisting enableTestnet outside the component.

publicEnv is a module-level constant, so enableTestnet is a build-time boolean that never changes at runtime. Placing it inside the component body makes it look like reactive state and adds a redundant entry to the useCallback dependency array at Line 157.

♻️ Proposed refactor
+const enableTestnet = publicEnv.DROP_FORGE_TESTNET === true;
+
 export default function WagmiSetup({
   children,
 }: {
   readonly children: React.ReactNode;
 }) {
-  const enableTestnet = publicEnv.DROP_FORGE_TESTNET === true;

Then remove enableTestnet from the useCallback dependency array:

-    [adapterManager, isCapacitor, enableTestnet]
+    [adapterManager, isCapacitor]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/providers/WagmiSetup.tsx` at line 103, enableTestnet is computed
from module-level publicEnv but is declared inside the WagmiSetup component,
making it appear reactive and unnecessarily included in the useCallback
dependency array; hoist the constant declaration (const enableTestnet =
publicEnv.DROP_FORGE_TESTNET === true) to module scope above the WagmiSetup
component, update any references inside WagmiSetup to use the hoisted symbol,
and then remove enableTestnet from the dependency array of the affected
useCallback (the callback defined in WagmiSetup that currently lists
enableTestnet).
components/providers/AppKitAdapterManager.ts (2)

264-277: getSortedChainIdentifiers reuses the empty-array error code/message for a structurally different failure.

Line 272 throws "ADAPTER_021: chains must be a non-empty array" when a chain element in a non-empty array has a missing or non-numeric id. This is a distinct failure mode from an empty array (Line 265-267) but is surfaced with the same code, making the two indistinguishable in logs.

♻️ Proposed fix
       return chains
         .map((chain) => {
           if (!chain || typeof chain.id !== "number") {
-            throw new AdapterError("ADAPTER_021: chains must be a non-empty array");
+            throw new AdapterError("ADAPTER_022: chains contains an invalid chain object (missing or non-numeric id)");
           }
           return `${chain.id}`;
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/providers/AppKitAdapterManager.ts` around lines 264 - 277,
getSortedChainIdentifiers currently throws the same AdapterError ("ADAPTER_021:
chains must be a non-empty array") for two different failure modes
(empty/non-array input and invalid chain elements), which makes diagnostics
ambiguous; change the inner validation that checks each chain element (inside
the .map) to throw a distinct AdapterError with a new code/message (e.g.,
"ADAPTER_022: chain elements must be objects with numeric id") instead of
reusing ADAPTER_021, so that callers can distinguish "empty or non-array" errors
from "malformed chain element" errors; update any tests or error handling that
expect ADAPTER_021 accordingly.

264-277: getSortedChainIdentifiers reuses the wrong error message for an invalid chain element.

Line 272 throws "ADAPTER_021: chains must be a non-empty array" when a chain object within a non-empty array lacks a numeric id. The failure mode is a malformed chain element, not an empty array — the same code and message used at Line 265-267 for a genuinely empty array. This makes the two distinct failure modes indistinguishable in logs.

♻️ Proposed fix
     return chains
       .map((chain) => {
         if (!chain || typeof chain.id !== "number") {
-          throw new AdapterError("ADAPTER_021: chains must be a non-empty array");
+          throw new AdapterError("ADAPTER_022: chains contains an invalid chain object (missing or non-numeric id)");
         }
         return `${chain.id}`;
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/providers/AppKitAdapterManager.ts` around lines 264 - 277, The
function getSortedChainIdentifiers currently rethrows ADAPTER_021 for both an
empty/non-array chains input and for invalid individual chain elements; change
the error thrown inside the map validation to a distinct AdapterError (e.g.
ADAPTER_022) with a clear message like "ADAPTER_022: chain elements must be
objects with a numeric id" to distinguish the malformed-element case from the
non-empty-array check, keeping the initial Array.isArray/chains.length check
as-is and only modifying the throw inside the map callback that checks !chain ||
typeof chain.id !== "number".
components/layout/sidebar/WebSidebarNav.tsx (2)

36-42: isMobile prop is accepted but unused.

The _isMobile parameter (prefixed with _ to suppress lint warnings) is destructured with a default value but never referenced in the component body. If it's a placeholder for upcoming work, consider adding a brief comment; otherwise, remove it to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/sidebar/WebSidebarNav.tsx` around lines 36 - 42, The
isMobile prop is destructured as _isMobile in the WebSidebarNav forwardRef
component but never used; either remove it from the WebSidebarNavProps
destructuring and the parameter list to avoid dead code, or keep it and add a
brief explanatory comment above the destructuring (e.g., "// reserved for future
mobile behavior") and reference _isMobile where intended; update the
WebSidebarNavProps type and the forwardRef signature accordingly (symbols:
WebSidebarNav, WebSidebarNavProps, _isMobile).

75-80: No-op useMemosections is just a pass-through of allSections.

useSidebarSections already returns a memoized value. Wrapping it in another useMemo(() => allSections, [allSections]) adds no benefit — it returns the same reference whenever the dependency changes, which is exactly when allSections itself changes. You can simply use allSections directly (or keep the rename inline):

♻️ Simplification
-  const allSections = useSidebarSections(
+  const sections = useSidebarSections(
     appWalletsSupported,
     capacitor.isIos,
     country
   );
-  const sections = useMemo(() => allSections, [allSections]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/sidebar/WebSidebarNav.tsx` around lines 75 - 80, The
variable sections is a no-op wrapper around the already-memoized value from
useSidebarSections; remove the redundant useMemo and use the returned value
directly (replace the useMemo call that creates sections with direct use of
allSections or rename allSections to sections). Update any references to
sections to use the original return (allSections) or the new direct variable,
and delete the import/use of useMemo if it becomes unused. Ensure you adjust
references so there are no remaining unused variables (allSections or sections)
and run a type check.
components/waves/memes/traits/NumberTrait.tsx (1)

21-24: Stale JSDoc comment.

The comment says "Without min/max constraints," but the component clearly uses min/max as required props and enforces them in multiple handlers. Consider updating or removing this misleading doc.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/memes/traits/NumberTrait.tsx` around lines 21 - 24, The
JSDoc for NumberTrait is stale/misleading: it claims "Without min/max
constraints" while the NumberTrait component and its handlers (props min, max,
onChange, validateValue, handleInputBlur, handleArrowChange) treat min and max
as required and enforce them; update or remove the JSDoc to reflect that min and
max are required and enforced (or document the exact behavior and constraints),
and ensure the comment references the prop contract for NumberTrait rather than
claiming absence of min/max.
components/header/useChainSwitcher.ts (1)

14-35: switchToNextChain and currentChainName are recomputed every render.

Both values are re-created on each render. Since switchToNextChain is passed as a click handler to child components, this could cause unnecessary re-renders in consumers that rely on referential equality (e.g., React.memo'd buttons). Consider wrapping in useCallback/useMemo:

♻️ Suggested memoization
+import { useCallback, useMemo } from "react";
 import { useChainId, useChains, useSwitchChain } from "wagmi";

 // ...

-  const currentChainName =
-    chains.find((chain) => chain.id === chainId)?.name ?? chainId.toString();
+  const currentChainName = useMemo(
+    () => chains.find((chain) => chain.id === chainId)?.name ?? chainId.toString(),
+    [chains, chainId]
+  );

-  const switchToNextChain = (): boolean => {
+  const switchToNextChain = useCallback((): boolean => {
     // ... existing body ...
-  };
+  }, [chains, chainId, switchChain]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header/useChainSwitcher.ts` around lines 14 - 35, currentChainName
and switchToNextChain are recreated on every render causing potential
unnecessary re-renders for consumers relying on referential equality; memoize
currentChainName with useMemo (depend on chains and chainId) and wrap
switchToNextChain with useCallback (depend on chains, chainId, switchChain) so
the computed name and the click handler keep stable identities across renders.
components/waves/memes/traits/TextTrait.tsx (1)

96-99: Minor inconsistency: defaultValue doesn't use .toString().

Line 36 normalizes via .toString() for all comparisons, but line 132's defaultValue={(traits[field] as string) ?? ""} skips it. This works because React coerces defaultValue to a string for <input type="text">, but for consistency with the rest of the file, consider:

-          defaultValue={(traits[field] as string) ?? ""}
+          defaultValue={currentTraitValue}

This also avoids computing the same value twice.

Also applies to: 132-132

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/memes/traits/TextTrait.tsx` around lines 96 - 99, The
input's defaultValue in TextTrait is inconsistent: it sets
defaultValue={(traits[field] as string) ?? ""} while elsewhere you normalize
with .toString(); update the input's defaultValue to use the same normalization
(e.g., (traits[field] as any)?.toString() ?? "") and reuse the already-computed
normalized value (the variable used for comparisons like
currentInputValue/isFieldFilled) to avoid duplicate computation.
components/the-memes/TheMemesMint.tsx (1)

33-33: Testnet contract support is intentional; consider adding user warnings.

The useDropForgeMintingConfig() hook is explicitly designed to return DROP_FORGE_TESTNET_CONTRACT when a user is connected to Sepolia and MEMES_CONTRACT for other chains. This testnet fallback is part of the Drop Forge workflow and appears intentional. However, there are no upstream chain guards in app/the-memes/mint/page.tsx and no user-facing warnings about the contract/chain being used. A user connected to Sepolia will silently attempt to mint from the testnet contract, which could cause confusion. Consider adding either a chain validation guard upstream or a UI warning when testnet contracts are detected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/the-memes/TheMemesMint.tsx` at line 33, The hook
useDropForgeMintingConfig can return the testnet fallback
(DROP_FORGE_TESTNET_CONTRACT) when on Sepolia, so add a guard or warning: in the
mint page component that renders TheMemesMint (the app/the-memes/mint page)
check the returned chain/contract from useDropForgeMintingConfig and either
block the mint flow for unsupported chains or surface a clear UI banner/modal
when the contract equals DROP_FORGE_TESTNET_CONTRACT or chain indicates Sepolia;
update TheMemesMint component to render the banner and disable the mint button
(or redirect to a supported chain) when the testnet contract is detected, and
ensure the check uses the same identifiers (useDropForgeMintingConfig,
DROP_FORGE_TESTNET_CONTRACT, MEMES_CONTRACT) so behavior stays consistent.
components/drop-forge/DropForgeClaimsListPageClient.tsx (1)

33-90: Deduplicate media-format helpers into a shared module.

Lines 33-90 re-implement logic that already exists in components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts (normalize/extension/image/animation/media label helpers). This is likely to drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/DropForgeClaimsListPageClient.tsx` around lines 33 -
90, This file duplicates media-format helper logic already implemented in
dropForgeLaunchClaimPageClient.helpers.ts; remove the local implementations of
normalizeFormat, getUrlExtension, getImageFormat, getAnimationInfo and
getMediaTypeLabel and import the corresponding helpers from the shared module
(or re-export/align names there if necessary), then update any local references
to call the imported functions so only one canonical implementation remains and
the helper code doesn't drift.
components/drop-forge/drop-forge-status.helpers.ts (1)

60-95: Make context explicit instead of inferring from undefined vs null.

Line 60 uses manifoldClaim === undefined to infer craft context. That API contract is easy to break and can flip published vs pending_initialization unexpectedly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/drop-forge-status.helpers.ts` around lines 60 - 95, The
code infers craft context by checking manifoldClaim === undefined which is
brittle; change the logic to accept and use an explicit boolean flag (e.g.,
isCraftContext) passed into the helper instead of relying on manifoldClaim's
undefined/null shape, then replace the current local isCraftContext assignment
with that parameter and update all uses (the ternary that returns "published" vs
"pending_initialization") to consult the explicit isCraftContext flag; ensure
callers that invoke the helper are updated to provide the correct boolean.
openapi.yaml (1)

8095-8097: Reuse ApiEthereumAddress in new wallet/address fields for consistent validation.

You introduced ApiEthereumAddress at Line 8095, but MintingClaimsProofsByAddressEntry.address (Line 11656) and PhaseAirdrop.wallet (Line 11845) are still plain strings.

Proposed fix
     MintingClaimsProofsByAddressEntry:
       type: object
@@
       properties:
         address:
-          type: string
+          $ref: "#/components/schemas/ApiEthereumAddress"
           description: Wallet address
@@
     PhaseAirdrop:
       type: object
@@
       properties:
         wallet:
-          type: string
+          $ref: "#/components/schemas/ApiEthereumAddress"
         amount:
           type: integer
           format: int64

Also applies to: 11656-11658, 11845-11846

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 8095 - 8097, Replace the plain string type for
Ethereum addresses with the new reusable schema ApiEthereumAddress: update the
MintingClaimsProofsByAddressEntry.address and PhaseAirdrop.wallet fields to
reference ApiEthereumAddress instead of type: string so they inherit the
^0x[a-fA-F0-9]{40}$ pattern and ensure consistent validation across the spec.
__tests__/components/header/user/proxy/HeaderUserProxyDropdown.test.tsx (1)

8-10: Mock setup is correct but no test exercises the chain-switching UI.

The useChainSwitcher mock is properly structured. However, since chains: [] means the "Switch Chain" button never renders, there's no test coverage for the new chain-switching feature added to HeaderUserProxyDropdown. Consider adding a test that provides multiple chains and verifies the button appears and calls switchToNextChain.

Also applies to: 33-37

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/header/user/proxy/HeaderUserProxyDropdown.test.tsx`
around lines 8 - 10, The test currently mocks useChainSwitcher but returns
chains: [] so the "Switch Chain" button never renders; update the mock in
HeaderUserProxyDropdown.test.tsx to return a non-empty chains array (e.g., two
chain objects) and a jest.fn() for switchToNextChain, render
HeaderUserProxyDropdown, assert that the "Switch Chain" button (or its label) is
present, simulate a click on that button, and verify switchToNextChain was
called; reference the useChainSwitcher mock, the HeaderUserProxyDropdown
component, and the switchToNextChain mock when making these changes.
services/api/memes-minting-claims-api.ts (1)

198-211: uploadClaimMedia ignores _claimId and _field — the upload is not associated with a specific claim or media slot.

The function accepts claimId and field parameters (prefixed with _ to suppress unused-variable warnings) but passes neither to multipartUploadCore. The upload goes to a generic drop-media/multipart-upload endpoint with no claim context. If the intent is a two-step flow (upload → get URL → patch claim), this works but the unused parameters are misleading and may cause bugs if a future caller assumes the upload is automatically bound to the claim.

Either remove the unused parameters or document the two-step intent:

Option A: Remove unused params
-export async function uploadClaimMedia(
-  _claimId: number,
-  _field: ClaimMediaField,
-  file: File
-): Promise<string> {
+export async function uploadClaimMedia(file: File): Promise<string> {
   return multipartUploadCore({
     file,
     endpoints: {
Option B: Keep params and add JSDoc
+/**
+ * Uploads media to a generic storage endpoint and returns the hosted URL.
+ * The caller is responsible for patching the claim with the returned URL
+ * via `patchClaim(claimId, { [field]: url })`.
+ */
 export async function uploadClaimMedia(
   _claimId: number,
   _field: ClaimMediaField,
   file: File
 ): Promise<string> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/api/memes-minting-claims-api.ts` around lines 198 - 211, The
uploadClaimMedia function currently accepts _claimId and _field but never uses
them (it just calls multipartUploadCore with generic endpoints); remove the
unused parameters from the uploadClaimMedia signature (and update any callers)
so the function becomes uploadClaimMedia(file: File): Promise<string>, or
alternatively keep the parameters but add a clear JSDoc to uploadClaimMedia
stating this is a two-step flow (upload returns a media URL/id which must then
be patched onto a claim) and/or pass claimId/field through to
multipartUploadCore and backend endpoints if you intend the upload to be bound
to a claim. Ensure references to multipartUploadCore and the endpoint names
("drop-media/multipart-upload" etc.) are updated accordingly.
components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts (2)

234-243: isVideoUrl can false-positive on URLs containing extensions mid-path or in query strings.

lower.includes(".mp4") matches URLs like https://example.com/.mp4-proxy/image.png or ?file=test.mp4&type=thumb. Consider anchoring to the path's trailing extension:

Proposed fix
 export function isVideoUrl(url: string | null | undefined): boolean {
   if (!url) return false;
-  const lower = url.toLowerCase();
-  return (
-    lower.includes(".mp4") ||
-    lower.includes(".webm") ||
-    lower.includes(".mov") ||
-    lower.includes(".m4v")
-  );
+  const ext = getUrlExtension(url);
+  return ext === "mp4" || ext === "webm" || ext === "mov" || ext === "m4v";
 }

This also reuses the already-defined getUrlExtension helper for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts` around lines
234 - 243, isVideoUrl currently does a substring check which false-positives on
extensions appearing in query strings or path segments; change it to use the
existing getUrlExtension helper to extract the real file extension from the URL
and compare that extension (lowercased) against the allowed set
("mp4","webm","mov","m4v"), returning false for null/empty input. Update the
isVideoUrl implementation to call getUrlExtension(url) and perform a strict
equality check against the set rather than using includes.

15-20: MintingClaimsRootItemWithAliases re-declares fields already present on the base type.

MintingClaimsRootItem already defines addresses_count: number and total_spots: number as required. The intersection adds them again as optional, which means the intersection type silently resolves to the required variant for those two keys. This is benign at runtime but misleading — readers may think addresses_count and total_spots can be absent on this type when in fact the base type already guarantees them.

Consider declaring only the truly additional aliases:

Proposed fix
-type MintingClaimsRootItemWithAliases = MintingClaimsRootItem & {
-  addresses_count?: number;
-  addresses?: number;
-  total_spots?: number;
-  total?: number;
-};
+type MintingClaimsRootItemWithAliases = MintingClaimsRootItem & {
+  addresses?: number;
+  total?: number;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/drop-forge/dropForgeLaunchClaimPageClient.helpers.ts` around lines
15 - 20, The intersection type MintingClaimsRootItemWithAliases re-declares
existing required fields from MintingClaimsRootItem (addresses_count and
total_spots) making the alias type misleading; update
MintingClaimsRootItemWithAliases to only add the actual alias properties (e.g.
keep addresses?: number and total?: number) and remove the redundant
addresses_count?: number and total_spots?: number declarations so the type reads
MintingClaimsRootItem & { addresses?: number; total?: number; }.

Comment thread components/drop-forge/launch/ClaimTransactionModal.tsx Outdated
Comment thread components/drop-forge/DropForgeAccordionSection.tsx
Comment thread components/drop-forge/DropForgeClaimsListPageClient.tsx Outdated
Comment thread components/drop-forge/craft/DropForgeCraftClaimPageClient.tsx
Comment thread hooks/useDropForgePermissions.ts
Comment thread hooks/useManifoldClaim.ts Outdated
Comment thread hooks/useMintCountdownState.ts
Comment thread openapi.yaml Outdated
Comment thread utils/appkit-initialization.utils.ts Outdated
prxt6529 added 25 commits March 9, 2026 13:54
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
@sonarqubecloud
Copy link
Copy Markdown

@prxt6529 prxt6529 merged commit 92e2a61 into main Mar 12, 2026
7 checks passed
This was referenced Mar 13, 2026
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.

2 participants