Skip to content

Conversation

@dtfiedler
Copy link
Contributor

@dtfiedler dtfiedler commented Sep 22, 2025

Summary by CodeRabbit

  • New Features

    • Listings area: Active/Completed tabs, search-by-name, pagination, listing details with buy/bid/confirm flows, bids view, settlement/recovery actions, Sell action from My ANTs, multi-step new-listing flow with price-schedule preview, and new My ANs/Listings routes.
  • Style

    • Global style imports updated to include marketplace UI stylesheet; minor spacing/text-node cleanups.
  • Chores

    • Added marketplace UI/data and UI libs, date picker, utility libs; build-time DEBUG set to false; new marketplace utilities and query tooling.

@dtfiedler dtfiedler requested a review from a team as a code owner September 22, 2025 19:11
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

Walkthrough

Adds a marketplace feature: Listings and Listing Details (with buy/bid/settle flows), My ANTs pages and new listing wizard, many listing-detail subcomponents and hooks, marketplace utilities and query keys, new routes (/listings, /my-ants), dependency additions, and a Vite define tweak.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Adds @blockydevs/arns-marketplace-data, @blockydevs/arns-marketplace-ui, @radix-ui/react-popover, @radix-ui/react-tabs, class-variance-authority, react-day-picker.
App wiring & global styles
src/App.tsx, src/main.tsx, src/components/pages/index.ts
Removes old ./index.css import, adds @blockydevs/arns-marketplace-ui/style.css and @src/index.css, lazy-loads Listings/MyANTs pages, invokes usePrefetchMarketplaceData(), and re-exports Listings from pages index.
Routing & config
src/utils/routes.tsx, vite.config.ts
Adds ROUTES entries for /listings and /my-ants (wrapped icons) and sets process.env.DEBUG = false in Vite define.
Marketplace utilities
src/utils/marketplace.ts
New module exporting duration/decrease interval options, time helpers (mergeDateAndTime, getMsFromInterval/getMsFromDuration), status variant mapping, price helper (getCurrentListingArioPrice), explorer helper, BLOCKYDEVS_* constants, AO explorer URL, and marketplaceQueryKeys.
Listings pages & flow
src/components/pages/Listings/*
New pages/components: Listings, SearchListingByName, ActiveListingsTab, CompletedListingsTab (cursor pagination, 15s refetch), and Confirm (buy/bid wiring, mutations).
Listing details & subcomponents
src/components/pages/Listings/Details/*
New Details page plus subcomponents: ListingMetadata, ListingPriceSection, EnglishListingPriceSection, DutchListingPriceSection, FixedListingPriceSection, EnglishListingBidsSection, EnglishListingSettlementSection, ListingBuyerSection, ListingExpiredSection.
My ANTs pages & modal
src/components/pages/MyANTs/*
New MyANTs page, MyANTsNewListing (3-step wizard with validation and createListing mutation), and PriceScheduleModal computing Dutch schedules.
Hooks for marketplace actions
src/hooks/listings/*
New hooks: useAntsMetadata, usePrefetchMarketplaceData, usePrepareListings, useBidListing, and useBuyListing (fetch/cache metadata, prefetch, prepare listing data, and wrap buy/bid mutations).
Data-table action
src/components/data-display/tables/DomainsTable.tsx
Adds a "Sell" action for owners linking to /my-ants/new-listing/{processId}, imports BookCopy icon, and minor spacing tweak.
Minor UI spacing fixes
src/components/cards/NavMenuCard/NavMenuCard.tsx, src/components/layout/Navbar/NavGroup/NavGroup.tsx
Removes stray inline whitespace text nodes; no behavior changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Browser
  participant App
  participant Listings as Listings UI
  participant API as Marketplace API
  participant Wallet

  User->>Browser: Navigate to /listings
  Browser->>App: route -> lazy-load Listings
  App->>Listings: render Tabs & Search
  Listings->>API: fetchActiveListings / fetchCompletedListings (poll every 15s)
  API-->>Listings: listings + cursors
  User->>Listings: open listing -> /listings/:id
  Browser->>App: route -> lazy-load Details
  App->>API: fetchListingDetails(id)
  API-->>App: listing details
  User->>App: Click Buy/Bid -> /listings/:id/confirm-purchase
  App->>Wallet: request signature (createAoSigner)
  Wallet-->>App: signed tx
  App->>API: buyListing()/bidListing()
  API-->>App: success
  App->>API: refetch listings & myANTs
Loading
sequenceDiagram
  autonumber
  actor User
  participant Browser
  participant App
  participant Wizard as NewListing Wizard
  participant Wallet
  participant API as Marketplace API

  User->>Browser: Open /my-ants/new-listing/:antProcessId
  Browser->>App: route -> lazy-load NewListing
  App->>Wizard: render Step 1 (form)
  User->>Wizard: set price/type/duration
  Wizard->>Wizard: compute schedule / validate
  User->>Wizard: Confirm -> request wallet signature
  Wizard->>Wallet: request signature
  Wallet-->>Wizard: signed tx
  Wizard->>API: createListing(config)
  API-->>Wizard: success
  Wizard->>API: refetch marketplace & myANTs
  Wizard-->>User: show success / view listing
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Potential review hotspots:

  • src/components/pages/Listings/Details/* (many interdependent components with business logic and conditional rendering).
  • src/hooks/listings/useAntsMetadata.ts (localStorage cache, deduped refetch logic).
  • src/components/pages/MyANTs/NewListing.tsx (multi-step form, date computations, createListing mutation).
  • Integration points: buy/bid/settle mutations (useBuyListing, useBidListing, confirm flow) and query refetch consistency.

Possibly related PRs

Suggested reviewers

  • atticusofsparta
  • kunstmusik

Poem

Thump-thump, I hop from route to route,
Listings bloom where carrots once grew.
Bids and schedules, a wizard’s delight,
My ANTs parade into the light.
A rabbit signs off: code — hooray, whoo! 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "chore: ArNS Marketplace UI" is clearly related to the changeset's primary objective. The PR introduces extensive marketplace functionality including new pages (Listings, MyANTs, Details, Confirm), adds marketplace-related dependencies (@blockydevs/arns-marketplace-data and @blockydevs/arns-marketplace-ui), integrates new routing, and provides marketplace utilities and hooks. The title directly describes this main change, and while it could be more specific about certain aspects, it accurately conveys that the PR is focused on integrating ArNS Marketplace UI features into the application.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines 366 to 375
</span>
<span className="flex gap-2.5">
<TokenIcon fill="var(--text-grey)" width={16} height={16} />
<Link
to="/my-ants"
className="navbar-link hover gap-2.5 items-center text-white text-sm"
>
My ANTs
</Link>
</span>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove and let's add this to the Action Items on the Manage table

Choose a reason for hiding this comment

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

@dtfiedler should every row in Manage table should have the "My ANTs" button that will redirect to My ANTs view?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i think it makes sense to have a top level navigation item for Marketplace (next to Prices/Returned Names) that navigates to the marketplace, which by default shows your ants, and then an action item for each ant to "Sell this ANT/Name" on each row of the manage table

Copy link
Contributor

@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: 10

🧹 Nitpick comments (43)
vite.config.ts (1)

41-48: Use a string (empty) for process.env.DEBUG in vite.config.ts

Repo search found only the define in vite.config.ts (lines 41–48); no other references detected.

-    'process.env.DEBUG': false,
+    // Keep as string like in Node; empty string disables debug cleanly
+    'process.env.DEBUG': JSON.stringify(''),

Optional (unify keys into single process.env define):

-    'process.env.DEBUG': false,
-    'process.env': {
-      // DO NOT EXPOSE THE ENTIRE process.env HERE - sensitive information on CI/CD could be exposed.
-      URL: process.env.URL,
-    },
+    'process.env': {
+      // DO NOT EXPOSE THE ENTIRE process.env HERE - sensitive information on CI/CD could be exposed.
+      URL: process.env.URL,
+      DEBUG: '',
+    },
src/components/pages/Listings/Details/ListingBuyerSection.tsx (3)

24-34: Add accessible label to the explorer button

Expose the full address for screen readers.

         <Button
           variant="link"
           className="px-0 gap-1"
           icon={<ExternalLink width={16} height={16} />}
           iconPlacement="right"
+          aria-label={`Open ${buyerAddress} in Ao Link Explorer`}
           onClick={() => {
             openAoLinkExplorer(buyerAddress);
           }}
         >

35-37: Remove conflicting text color classes

text-white is overridden by text-[var(--ar-color-neutral-400)]. Drop the redundant class.

-        <span className="text-white text-sm font-normal text-[var(--ar-color-neutral-400)]">
+        <span className="text-sm font-normal text-[var(--ar-color-neutral-400)]">

29-31: Harden window.open usage to prevent tab‑nabbing

Update openAoLinkExplorer to pass noopener,noreferrer.

Outside this file (src/utils/marketplace.ts):

export const openAoLinkExplorer = (address: string) => {
  window.open(`${AO_LINK_EXPLORER_URL}/${address}`, '_blank', 'noopener,noreferrer');
};
src/components/pages/Listings/Details/EnglishListingPriceSection.tsx (3)

21-22: Validate numeric input robustly (NaN/Infinity safe)

Use a parsed number and guard against non‑numeric input.

-  const isBidPriceValid = Number(bidPrice) >= minBid;
+  const bid = Number(bidPrice);
+  const isBidPriceValid = Number.isFinite(bid) && bid >= minBid;

48-54: Disable button when input is empty/invalid

Current check misses the empty‑string case.

         disabled={
           !walletAddress ||
-          bidPrice === undefined ||
+          bidPrice === '' ||
+          !Number.isFinite(bid) ||
           // if highest bid exists, bid must be strictly greater than it
           // if no bids yet, bid must be at least equal to starting price
           !isBidPriceValid
         }

28-31: URL‑encode query params to avoid broken navigation

Names/ids may contain characters that break URLs.

-    navigate(
-      `/listings/${orderId}/confirm-purchase?price=${bidPrice}&type=english&name=${name}&antProcessId=${antProcessId}`,
-    );
+    navigate(
+      `/listings/${encodeURIComponent(
+        orderId,
+      )}/confirm-purchase?price=${encodeURIComponent(
+        bidPrice,
+      )}&type=english&name=${encodeURIComponent(
+        name,
+      )}&antProcessId=${encodeURIComponent(antProcessId)}`,
+    );
src/components/pages/Listings/Details/ListingExpiredSection.tsx (3)

24-41: Add AO client guard and clearer errors

Prevent undefined client usage; normalize error messages.

   const mutationCancelListing = useMutation({
     mutationFn: async ({ listingId }: { listingId: string }) => {
-      if (!wallet || !walletAddress) {
+      if (!wallet || !walletAddress) {
         throw new Error('No wallet connected');
       }
 
       if (!wallet.contractSigner) {
         throw new Error('No wallet signer available');
       }
 
+      if (!aoClient) {
+        throw new Error('AO client not initialized');
+      }
+
       return await cancelListing({
         ao: aoClient,
         orderId: listingId,
         marketplaceProcessId: BLOCKYDEVS_MARKETPLACE_PROCESS_ID,
         signer: createAoSigner(wallet.contractSigner),
       });
     },
   });

67-76: Prefer invalidation over direct refetch; drop console.log

Invalidation batches better; avoid stray logs in prod.

-              console.log(`cancel success`, { data });
-              void Promise.allSettled([
-                queryClient.refetchQueries({
-                  queryKey: [marketplaceQueryKeys.listings.all],
-                }),
-                queryClient.refetchQueries({
-                  queryKey: [marketplaceQueryKeys.myANTs.all],
-                }),
-              ]);
+              await Promise.allSettled([
+                queryClient.invalidateQueries({
+                  queryKey: [marketplaceQueryKeys.listings.all],
+                }),
+                queryClient.invalidateQueries({
+                  queryKey: [marketplaceQueryKeys.myANTs.all],
+                }),
+              ]);

49-55: Optional: add mutationKey for clarity/dedupe

Helps React Query devtools and avoids duplicate in‑flight mutations.

-  const mutationCancelListing = useMutation({
+  const mutationCancelListing = useMutation({
+    mutationKey: ['cancel-listing', listing.orderId],
src/components/pages/Listings/Listings.tsx (1)

21-33: Optional: semantic tab values and deep‑linking

Consider values like "active"/"completed" and reading initial tab from URL (query/hash) for shareable links.

src/App.tsx (2)

352-411: Optional: add breadcrumbs for new pages.

Consider adding handle.crumbs for /listings, /listings/:id, and /my-ants/* to match the Manage flows.


2-2: Remove duplicate global CSS import from src/App.tsx

style.css is imported in src/main.tsx (line 1) and src/App.tsx (line 2); keep the import only in the app entry (src/main.tsx) — remove this line from src/App.tsx:
import '@blockydevs/arns-marketplace-ui/style.css';

src/components/pages/Listings/Details/DutchListingPriceSection.tsx (2)

5-5: Build search params safely (encode values).

Manual string interpolation risks broken URLs for special chars and is harder to maintain. Use createSearchParams.

Apply:

-import { useNavigate } from 'react-router-dom';
+import { createSearchParams, useNavigate } from 'react-router-dom';
@@
-    navigate(
-      `/listings/${orderId}/confirm-purchase?price=${currentPrice}&type=dutch&name=${name}&antProcessId=${antProcessId}`,
-    );
+    navigate({
+      pathname: `/listings/${orderId}/confirm-purchase`,
+      search: createSearchParams({
+        price: String(currentPrice),
+        type: 'dutch',
+        name,
+        antProcessId,
+      }).toString(),
+    });

Also applies to: 21-23


33-35: Minor: simplify onClick.

No need for an inline wrapper.

-          onClick={() => {
-            navigateToConfirmPurchase();
-          }}
+          onClick={navigateToConfirmPurchase}
src/components/pages/Listings/Details/FixedListingPriceSection.tsx (2)

5-5: Build search params safely (encode values).

Same concern as Dutch section.

-import { useNavigate } from 'react-router-dom';
+import { createSearchParams, useNavigate } from 'react-router-dom';
@@
-    navigate(
-      `/listings/${orderId}/confirm-purchase?price=${currentPrice}&type=fixed&name=${name}&antProcessId=${antProcessId}`,
-    );
+    navigate({
+      pathname: `/listings/${orderId}/confirm-purchase`,
+      search: createSearchParams({
+        price: String(currentPrice),
+        type: 'fixed',
+        name,
+        antProcessId,
+      }).toString(),
+    });

Also applies to: 21-23


31-33: Minor: simplify onClick.

-      onClick={() => {
-        navigateToConfirmPurchase();
-      }}
+      onClick={navigateToConfirmPurchase}
src/components/pages/MyANTs/MyANTs.tsx (2)

16-16: Encode query params when navigating to new listing.

Use createSearchParams to safely pass name.

-import { useNavigate } from 'react-router-dom';
+import { createSearchParams, useNavigate } from 'react-router-dom';
@@
-              navigate(
-                `/my-ants/new-listing/${domain.processId}?name=${domain.name}`,
-              );
+              navigate({
+                pathname: `/my-ants/new-listing/${domain.processId}`,
+                search: createSearchParams({ name: domain.name }).toString(),
+              });

Also applies to: 45-47


54-74: Consider centralizing price computation.

You already expose getCurrentListingArioPrice in @src/utils/marketplace. Reuse it here to avoid drift vs. Details/Confirm.

Would you like me to draft a follow-up change that adds optional endedAt support to getCurrentListingArioPrice so both places share identical logic?

src/components/pages/Listings/Details/EnglishListingSettlementSection.tsx (2)

60-65: Harden error handling.

error.message may be undefined if error isn’t an Error. Normalize the message.

-            onError: (error) => {
-              eventEmitter.emit('error', {
-                name: 'Failed to settle listing',
-                message: error.message,
-              });
-            },
+            onError: (error) => {
+              const message =
+                error instanceof Error ? error.message : String(error);
+              eventEmitter.emit('error', {
+                name: 'Failed to settle listing',
+                message,
+              });
+            },

66-75: Prefer invalidateQueries over refetchQueries.

Invalidation is the recommended pattern; it also dedupes concurrent refetches.

-            onSuccess: async (data) => {
-              console.log(`settlement success`, { data });
-              void Promise.allSettled([
-                queryClient.refetchQueries({
-                  queryKey: [marketplaceQueryKeys.listings.all],
-                }),
-                queryClient.refetchQueries({
-                  queryKey: [marketplaceQueryKeys.myANTs.all],
-                }),
-              ]);
-            },
+            onSuccess: async (data) => {
+              console.log(`settlement success`, { data });
+              await Promise.allSettled([
+                queryClient.invalidateQueries({
+                  queryKey: [marketplaceQueryKeys.listings.all],
+                }),
+                queryClient.invalidateQueries({
+                  queryKey: [marketplaceQueryKeys.myANTs.all],
+                }),
+              ]);
+            },
src/components/pages/MyANTs/PriceScheduleModal.tsx (2)

31-37: Memoize schedule computation to avoid recompute on each render.

Use useMemo and add a minimal import.

+import { useMemo } from 'react';
@@
-  const dutchPriceSchedule: Schedule[] = (() => {
+  const dutchPriceSchedule = useMemo<Schedule[]>(() => {
@@
-  })();
+  }, [startingPrice, minimumPrice, decreaseInterval, dateFrom, dateTo]);

Also applies to: 49-59, 1-1


45-47: Ensure non-negative, integer MARIO step; avoid double Number cast later.

-      const decreaseStepMario = Math.round(
-        Number(arioToMario(decreaseStepArio)),
-      ).toString();
+      const decreaseStepMario = Math.max(
+        0,
+        Math.round(Number(arioToMario(decreaseStepArio)))
+      ).toString();
src/components/pages/Listings/Details/Details.tsx (2)

45-51: Handle unknown error types safely.

Avoid assuming .message exists on error.

-  if (queryDetails.error) {
+  if (queryDetails.error) {
+    const errMsg =
+      queryDetails.error instanceof Error
+        ? queryDetails.error.message
+        : String(queryDetails.error);
     return (
       <p className="text-error text-center">
-        Failed to load listing details: {queryDetails.error.message}
+        Failed to load listing details: {errMsg}
       </p>
     );
   }

55-66: Provide a safe default status variant.

getStatusVariantFromListing can return undefined; pass a fallback to avoid UI regressions.

-  const status = getStatusVariantFromListing(listing);
+  const status = getStatusVariantFromListing(listing) ?? 'processing';

Please confirm DetailsCard accepts this fallback.

src/components/pages/Listings/SearchListingByName.tsx (2)

27-33: Defensive error handling.

error may not be an Error. Use a safe message.

-    onError: (error) => {
+    onError: (error) => {
+      const message = error instanceof Error ? error.message : String(error);
       eventEmitter.emit('error', {
         name: `Failed search for "${searchValue}"`,
-        message: error.message,
+        message,
       });
     },

40-44: Trim and normalize input; guard missing network context.

Prevents empty/whitespace searches and avoids firing when clients are not ready.

-  const handleSearchSubmit = (e: React.FormEvent) => {
+  const handleSearchSubmit = (e: React.FormEvent) => {
     e.preventDefault();
-    if (!searchValue) return;
-    mutationSearch.mutate(searchValue);
+    const term = searchValue.trim().toLowerCase();
+    if (!term) return;
+    if (!aoClient) {
+      eventEmitter.emit('error', {
+        name: 'Search unavailable',
+        message: 'Network client is not initialized.',
+      });
+      return;
+    }
+    mutationSearch.mutate(term);
   };
src/components/pages/Listings/ActiveListingsTab.tsx (2)

25-67: Avoid side effects inside select; move cursor storage to onSuccess.

select should be pure. Persist cursors in onSuccess.

   const queryActiveListings = useQuery({
     refetchInterval: 15 * 1000,
     structuralSharing: false,
@@
-    select: (data) => {
-      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
-
-      return {
+    select: (data) => {
+      return {
         ...data,
         items: data.items.map((item): Domain => {
           const currentPrice = getCurrentListingArioPrice(item);
@@
         }),
       };
     },
+    onSuccess: (data) => {
+      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
+    },
   });

69-78: Safer totals and error message extraction.

-  const { totalItems } = queryActiveListings.data ?? {};
-  const totalPages = pagination.getTotalPages(totalItems);
+  const { totalItems } = queryActiveListings.data ?? {};
+  const totalPages = pagination.getTotalPages(totalItems ?? 0);
@@
-        error={queryActiveListings.error?.message}
+        error={
+          queryActiveListings.error
+            ? (queryActiveListings.error instanceof Error
+                ? queryActiveListings.error.message
+                : String(queryActiveListings.error))
+            : undefined
+        }
       />
src/components/pages/Listings/Confirm.tsx (3)

187-205: Don’t throw on missing price; disable and no-op instead.

Prevents noisy runtime errors on click.

-          <Row label="Price" value={`${price} ARIO`} />
+          <Row label="Price" value={price ? `${price} ARIO` : '-'} />
@@
-              disabled={
-                !walletAddress || isMutationPending || isMutationSuccess
-              }
+              disabled={
+                !walletAddress || isMutationPending || isMutationSuccess || !price
+              }
@@
-                if (!price) {
-                  throw new Error('price is missing');
-                }
+                if (!price) return;

Also applies to: 209-216


214-221: Defensive error handling when emitting.

-                    onError: (error) => {
+                    onError: (error) => {
+                      const message =
+                        error instanceof Error ? error.message : String(error);
                       eventEmitter.emit('error', {
                         name: `Failed to ${operation}`,
-                        message: error.message,
+                        message,
                       });
                     },

35-101: Deduplicate wallet preflight checks.

mutationBuyListing and mutationBidListing repeat the same guards. Extract a small ensureWalletReady() helper to reduce duplication and potential drift.

src/components/pages/Listings/CompletedListingsTab.tsx (2)

39-66: Avoid side effects inside select().

select should be pure. Move pagination.storeNextCursor to onSuccess (see previous diff).

-    select: (data) => {
-      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
-
+    select: (data) => {
       return {
         ...data,
         items: data.items.map((item): Domain => {

72-86: Optional: smoother pagination UX.

Consider keepPreviousData: true to avoid UI flicker when changing pages.

src/components/pages/Listings/Details/ListingMetadata.tsx (2)

30-36: Fix endedAt fallback logic.

Using 'in' checks property existence, not value. Prefer nullish coalescing to safely fall back to expiresAt.

-          endedAt: new Date(
-            'endedAt' in listing ? listing.endedAt : listing.expiresAt,
-          ).getTime(),
+          endedAt: new Date(listing.endedAt ?? listing.expiresAt).getTime(),

21-38: Memoize heavy schedule computation.

Wrap dutchPriceSchedule in useMemo with listing inputs as deps to avoid recomputation on every render.

src/components/pages/MyANTs/NewListing.tsx (4)

17-17: Remove unused side‑effect import.

This import does nothing and can be dropped.

-import '@src/utils/marketplace';

241-247: Use controlled Select.

Bind value instead of defaultValue so UI reflects state changes.

-                <Select
+                <Select
                   placeholder="Type of listing"
                   className="w-full"
-                  defaultValue={form.type}
+                  value={form.type || undefined}
                   onValueChange={(value) => updateForm('type', value)}
                   options={typeOptions}
                 />

297-304: Gate schedule preview on valid numeric inputs.

Avoid generating schedule with NaN or invalid ranges.

-                    <PriceScheduleModal
-                      startingPrice={Number(form.price)}
-                      minimumPrice={Number(form.minimumPrice)}
-                      dateFrom={now}
-                      dateTo={new Date(endDate)}
-                      decreaseInterval={form.decrease}
-                    />
+                    {form.decrease &&
+                      Number(form.price) > 0 &&
+                      Number(form.minimumPrice) > 0 &&
+                      Number(form.price) > Number(form.minimumPrice) && (
+                        <PriceScheduleModal
+                          startingPrice={Number(form.price)}
+                          minimumPrice={Number(form.minimumPrice)}
+                          dateFrom={now}
+                          dateTo={new Date(endDate)}
+                          decreaseInterval={form.decrease}
+                        />
+                      )}

363-391: Add basic validation to confirmation.

Before enabling Confirm, ensure price > 0; for dutch, minimumPrice > 0 and < price; for english, duration is set. Disabling the button based on these checks reduces server‑side errors.

src/utils/marketplace.ts (3)

100-104: Clamp negative custom durations.

Return a non‑negative duration to avoid accidental past expiries.

-      if (!customDate) return 0;
-      return customDate.getTime() - Date.now();
+      if (!customDate) return 0;
+      return Math.max(0, customDate.getTime() - Date.now());

14-37: Remove test‑only options before release or gate by env.

Options marked // FIXME: remove test code should be pruned or conditionally included for non‑production builds.


164-190: Query key shape is fine; optionally avoid inline objects.

Objects in keys are hashed by React Query, but using primitives (page, pageSize) at top level can make keys more explicit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d6f42 and 1649e0e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (28)
  • .npmrc (1 hunks)
  • package.json (3 hunks)
  • src/App.tsx (3 hunks)
  • src/components/cards/NavMenuCard/NavMenuCard.tsx (1 hunks)
  • src/components/layout/Navbar/NavGroup/NavGroup.tsx (0 hunks)
  • src/components/pages/Listings/ActiveListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/CompletedListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/Confirm.tsx (1 hunks)
  • src/components/pages/Listings/Details/Details.tsx (1 hunks)
  • src/components/pages/Listings/Details/DutchListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/EnglishListingBidsSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/EnglishListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/EnglishListingSettlementSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/FixedListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingBuyerSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingExpiredSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingMetadata.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Listings.tsx (1 hunks)
  • src/components/pages/Listings/SearchListingByName.tsx (1 hunks)
  • src/components/pages/MyANTs/MyANTs.tsx (1 hunks)
  • src/components/pages/MyANTs/NewListing.tsx (1 hunks)
  • src/components/pages/MyANTs/PriceScheduleModal.tsx (1 hunks)
  • src/components/pages/index.ts (2 hunks)
  • src/main.tsx (1 hunks)
  • src/utils/marketplace.ts (1 hunks)
  • src/utils/routes.tsx (3 hunks)
  • vite.config.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/components/layout/Navbar/NavGroup/NavGroup.tsx
🧰 Additional context used
🧬 Code graph analysis (20)
src/components/pages/Listings/Details/EnglishListingPriceSection.tsx (2)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (127-151)
src/components/pages/Listings/Details/Details.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (4)
  • marketplaceQueryKeys (164-190)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (157-158)
  • getCurrentListingArioPrice (127-151)
  • getStatusVariantFromListing (111-125)
src/components/pages/MyANTs/MyANTs.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (2)
  • marketplaceQueryKeys (164-190)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (157-158)
src/components/pages/Listings/Details/ListingBuyerSection.tsx (2)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (1)
  • openAoLinkExplorer (153-155)
src/components/pages/MyANTs/NewListing.tsx (2)
src/utils/marketplace.ts (12)
  • Duration (6-9)
  • DecreaseInterval (11-12)
  • mergeDateAndTime (39-53)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (157-158)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (159-160)
  • BLOCKYDEVS_SWAP_TOKEN_ID (161-162)
  • getMsFromInterval (55-76)
  • getMsFromDuration (78-109)
  • dutchDurationOptions (22-29)
  • dutchDecreaseIntervalOptions (31-37)
  • englishDurationOptions (14-20)
  • marketplaceQueryKeys (164-190)
src/components/pages/MyANTs/PriceScheduleModal.tsx (1)
  • PriceScheduleModal (24-85)
src/components/pages/Listings/Listings.tsx (1)
src/components/pages/index.ts (1)
  • Listings (12-12)
src/components/pages/Listings/Details/ListingExpiredSection.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (2)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (159-160)
  • marketplaceQueryKeys (164-190)
src/App.tsx (2)
src/components/pages/index.ts (3)
  • Manage (13-13)
  • Home (11-11)
  • Listings (12-12)
src/utils/routes.tsx (1)
  • Route (28-36)
src/components/pages/MyANTs/PriceScheduleModal.tsx (2)
src/utils/marketplace.ts (2)
  • DecreaseInterval (11-12)
  • getMsFromInterval (55-76)
src/utils/common/common.tsx (1)
  • formatDate (12-14)
src/components/pages/Listings/Details/FixedListingPriceSection.tsx (2)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (127-151)
src/components/pages/Listings/SearchListingByName.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (1)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (157-158)
src/components/cards/NavMenuCard/NavMenuCard.tsx (1)
src/components/icons/index.ts (1)
  • TokenIcon (134-134)
src/components/pages/Listings/Confirm.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (3)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (159-160)
  • BLOCKYDEVS_SWAP_TOKEN_ID (161-162)
  • marketplaceQueryKeys (164-190)
src/components/pages/Listings/Details/EnglishListingSettlementSection.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (2)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (159-160)
  • marketplaceQueryKeys (164-190)
src/components/pages/Listings/CompletedListingsTab.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (3)
  • marketplaceQueryKeys (164-190)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (157-158)
  • getCurrentListingArioPrice (127-151)
src/components/pages/Listings/Details/EnglishListingBidsSection.tsx (2)
src/utils/marketplace.ts (1)
  • AO_LINK_EXPLORER_URL (163-163)
src/utils/common/common.tsx (1)
  • formatDate (12-14)
src/components/pages/Listings/Details/ListingMetadata.tsx (2)
src/utils/common/common.tsx (1)
  • formatDate (12-14)
src/utils/marketplace.ts (1)
  • openAoLinkExplorer (153-155)
src/components/pages/Listings/ActiveListingsTab.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (3)
  • marketplaceQueryKeys (164-190)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (157-158)
  • getCurrentListingArioPrice (127-151)
src/components/pages/Listings/Details/DutchListingPriceSection.tsx (2)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (127-151)
src/utils/routes.tsx (1)
src/components/pages/index.ts (1)
  • Listings (12-12)
🪛 Gitleaks (8.28.0)
src/utils/marketplace.ts

[high] 161-162: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (14)
src/components/pages/Listings/Details/ListingBuyerSection.tsx (1)

15-17: Guard against empty/invalid buyerAddress

If upstream ever passes an empty string, shortenAddress may misbehave. Consider a fallback render or early return.

src/components/pages/Listings/Details/EnglishListingPriceSection.tsx (2)

35-44: Constrain the input with min/step and resilient placeholder

Improves UX and prevents negative/too‑small entries at the control level.
[uggest_recommended_refactor]

       <Input
         type="number"
         value={bidPrice}
         onChange={(e) => {
           setBidPrice(e.target.value);
         }}
-        placeholder={`${minBid} and up`}
+        min={minBid}
+        step={1}
+        placeholder={`${Number.isFinite(minBid) ? minBid : ''} and up`}
         label="Name your price"
         suffix="ARIO"
       />

18-20: Confirm min‑increment business rule

Adding a flat +1 ARIO might not match expected tick sizes for English auctions. If fractional prices are allowed, consider a relative/min‑tick rule.

src/main.tsx (1)

1-2: LGTM: CSS import order

UI styles before app overrides is correct.

src/components/pages/index.ts (1)

2-2: Expose Listings page

Export wiring looks good.

Also applies to: 12-12

package.json (1)

32-33: Deps look consistent with UI additions — quick verification

package.json lists: @blockydevs/arns-marketplace-data@^0.1.84, @blockydevs/arns-marketplace-ui@^0.1.107, @radix-ui/react-popover@^1.1.15, @radix-ui/react-tabs@^1.1.13, class-variance-authority@^0.7.1, react-day-picker@^9.9.0. Still verify the lockfile/installed tree for duplicate Radix/DayPicker peer deps and run npm/yarn/pnpm dedupe or npm ls to confirm a single version.

src/components/cards/NavMenuCard/NavMenuCard.tsx (1)

366-375: New “My ANTs” link: confirm route exists and is protected

Repo search returned no matches. Confirm /my-ants route is registered and is behind the intended auth wrapper (ProtectedRoute/RequireAuth or equivalent).

src/App.tsx (1)

352-411: LGTM: routes and lazy boundaries for Listings/MyANTs.

Route structure, Suspense fallbacks, and code-splitting look correct and consistent with the rest of the app.

src/components/pages/Listings/Details/DutchListingPriceSection.tsx (1)

21-23: Don’t trust client-provided price.

Treat price in the query string as a display hint only; recompute on the Confirm page from authoritative listing data to avoid race conditions on Dutch price ticks.

src/utils/routes.tsx (1)

62-69: LGTM: Listings route addition is consistent.

Icon wrapper matches established pattern; route props align with existing config.

src/components/pages/Listings/Details/ListingPriceSection.tsx (1)

16-60: LGTM: clear, type‑driven rendering.

Sectioning by listing.type and status looks correct; props wired to detail sections appropriately.

src/components/pages/Listings/Details/EnglishListingBidsSection.tsx (1)

44-47: Verify Pagination indexing (1‑based vs 0‑based).

bidPage is 1‑based (useState(1)). Confirm Pagination.activeIndex expects 1‑based — Pagination implementation wasn't found in repo/node_modules. If Pagination is 0‑based, change to:

  • activeIndex={bidPage - 1}
  • onPageChange={(i) => setBidPage(i + 1)}

Location: src/components/pages/Listings/Details/EnglishListingBidsSection.tsx (lines 42–47)

src/components/pages/Listings/Confirm.tsx (1)

21-34: Validate type vs actual listing server-side or by fetching details.

Relying solely on a query param is brittle. Consider confirming the listing’s type before deciding bid vs buy.

Also applies to: 205-243

src/utils/marketplace.ts (1)

157-163: Verify constants are non‑sensitive.

Gitleaks flagged these IDs as a potential secret. If they’re public process/token IDs, add a comment noting they’re safe. If not, move them to env vars and rotate.

 export const BLOCKYDEVS_ACTIVITY_PROCESS_ID =
   'Jj8LhgFLmCE_BAMys_zoTDRx8eYXsSl3-BMBIov8n9E';
 export const BLOCKYDEVS_MARKETPLACE_PROCESS_ID =
   'a3jqBgXGAqefY4EHqkMwXhkBSFxZfzVdLU1oMUTQ-1M';
 export const BLOCKYDEVS_SWAP_TOKEN_ID =
   'agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA';
+// Note: These are public process IDs (not secrets). If that’s incorrect, migrate to env vars and rotate.

Comment on lines +178 to +200
const name = searchParams.get('name') ?? '-';
const endDate =
form.duration === 'custom'
? form.date
? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
: now.toISOString()
: addMilliseconds(
now,
form.duration ? getMsFromDuration(form.duration) : 0,
).toISOString();

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix endDate preview formatting.

formatDate here appears misused; construct ISO using mergeDateAndTime for custom, otherwise add duration from now.

-  const endDate =
-    form.duration === 'custom'
-      ? form.date
-        ? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
-        : now.toISOString()
-      : addMilliseconds(
-          now,
-          form.duration ? getMsFromDuration(form.duration) : 0,
-        ).toISOString();
+  const endDate =
+    form.duration === 'custom'
+      ? mergeDateAndTime(form.date, form.time)?.toISOString() ?? now.toISOString()
+      : addMilliseconds(
+          now,
+          form.duration ? getMsFromDuration(form.duration, form.date, form.time) : 0,
+        ).toISOString();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const name = searchParams.get('name') ?? '-';
const endDate =
form.duration === 'custom'
? form.date
? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
: now.toISOString()
: addMilliseconds(
now,
form.duration ? getMsFromDuration(form.duration) : 0,
).toISOString();
const name = searchParams.get('name') ?? '-';
const endDate =
form.duration === 'custom'
? (mergeDateAndTime(form.date, form.time)?.toISOString() ?? now.toISOString())
: addMilliseconds(
now,
form.duration ? getMsFromDuration(form.duration, form.date, form.time) : 0,
).toISOString();
🤖 Prompt for AI Agents
In src/components/pages/MyANTs/NewListing.tsx around lines 178 to 188, the
endDate preview is built incorrectly by misusing formatDate and concatenating
strings; replace that logic so that when form.duration === 'custom' you use
mergeDateAndTime(form.date, form.time) (or equivalent utility) to produce a Date
and call toISOString() on it, and when not custom compute addMilliseconds(now,
getMsFromDuration(form.duration ?? '')) then toISOString(); also add null checks
so if form.date or form.time are missing you fall back to now.toISOString() or a
sensible default.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (4)
src/components/pages/Listings/CompletedListingsTab.tsx (4)

25-68: Avoid side-effects in select; gate by cursor readiness; include cursor in queryKey.

  • select should be pure; move pagination.storeNextCursor to onSuccess.
  • For cursor-based paging, avoid fetching non-first pages until the cursor for that page exists.
  • Include cursor in the queryKey options to fully represent inputs and reduce dedupe/mismatch risks.

Apply:

   const queryCompletedListings = useQuery({
     refetchInterval: 15 * 1000,
-    enabled: Boolean(aoClient),
+    enabled: Boolean(aoClient && (pagination.page === 0 || pagination.cursor)),
     queryKey: marketplaceQueryKeys.listings.list('completed', {
       page: pagination.page,
       pageSize: pagination.pageSize,
+      cursor: pagination.cursor,
     }),
     queryFn: () => {
       return fetchCompletedListings({
         ao: aoClient,
         activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
         limit: pagination.pageSize,
         cursor: pagination.cursor,
       });
     },
+    onSuccess: (data) => {
+      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
+    },
-    select: (data) => {
-      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
-
-      return {
+    select: (data) => {
+      return {
         ...data,
         items: data.items.map((item): Domain => {
           const currentPrice = getCurrentListingArioPrice(item);

Based on learnings


78-79: Harden error extraction (error is unknown in v5).

Guard against non-Error values to avoid undefined property access.

-        error={queryCompletedListings.error?.message}
+        error={
+          queryCompletedListings.error instanceof Error
+            ? queryCompletedListings.error.message
+            : undefined
+        }

Based on learnings


80-86: Hide pagination when only one page.

Avoid rendering controls when totalPages <= 1.

-      {!queryCompletedListings.isPending && (
+      {!queryCompletedListings.isPending && totalPages > 1 && (
         <Pagination
           totalPages={totalPages}
           activeIndex={pagination.page}
           onPageChange={pagination.setPage}
         />
       )}

54-57: Confirm price precision and formatting.

Number(currentPrice) may lose precision if ARIO uses decimals/strings. If getCurrentListingArioPrice returns a string/decimal, prefer keeping it as string or a decimal library and format in the table.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1649e0e and 16e407e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (2)
  • package.json (3 hunks)
  • src/components/pages/Listings/CompletedListingsTab.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/pages/Listings/CompletedListingsTab.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (3)
  • marketplaceQueryKeys (164-190)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (157-158)
  • getCurrentListingArioPrice (127-151)
🔇 Additional comments (1)
src/components/pages/Listings/CompletedListingsTab.tsx (1)

25-28: Resolved: Query is now gated by aoClient (good).

The enabled guard prevents firing before aoClient is available.

Copy link
Contributor

@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: 4

🧹 Nitpick comments (2)
src/utils/marketplace.ts (1)

153-159: Null out the opener as a safety net.

Nice job adding noopener,noreferrer. Some WebKit builds ignore the feature string, so we still need the defensive newWin.opener = null after the call.

-export const openAoLinkExplorer = (address: string) => {
-  window.open(
+export const openAoLinkExplorer = (address: string) => {
+  const newWindow = window.open(
     `${AO_LINK_EXPLORER_URL}/${address}`,
     '_blank',
     'noopener,noreferrer',
   );
+  if (newWindow) {
+    newWindow.opener = null;
+  }
 };
src/components/pages/MyANTs/NewListing.tsx (1)

301-308: Reuse computed endDate instead of reparsing.

We already compute endDate above (and after the fixes it will be a Date). Converting back and forth via new Date(endDate) risks Invalid Date when the preview string drifts. Pass the Date directly to keep the schedule math accurate.

                   <PriceScheduleModal
                     startingPrice={Number(form.price)}
                     minimumPrice={Number(form.minimumPrice)}
-                    dateFrom={now}
-                    dateTo={new Date(endDate)}
+                    dateFrom={now}
+                    dateTo={endDate}
                     decreaseInterval={form.decrease}
                   />

(Apply the same change in the confirmation step.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16e407e and 2b48da4.

📒 Files selected for processing (4)
  • src/components/pages/Listings/Confirm.tsx (1 hunks)
  • src/components/pages/MyANTs/MyANTs.tsx (1 hunks)
  • src/components/pages/MyANTs/NewListing.tsx (1 hunks)
  • src/utils/marketplace.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/pages/MyANTs/MyANTs.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/pages/Listings/Confirm.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (3)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
  • marketplaceQueryKeys (168-194)
src/components/pages/MyANTs/NewListing.tsx (5)
src/utils/marketplace.ts (12)
  • Duration (6-9)
  • DecreaseInterval (11-12)
  • mergeDateAndTime (39-53)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
  • getMsFromInterval (55-76)
  • getMsFromDuration (78-109)
  • dutchDurationOptions (22-29)
  • dutchDecreaseIntervalOptions (31-37)
  • englishDurationOptions (14-20)
  • marketplaceQueryKeys (168-194)
src/utils/network.ts (1)
  • queryClient (39-45)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/components/pages/MyANTs/PriceScheduleModal.tsx (1)
  • PriceScheduleModal (24-85)
🪛 Gitleaks (8.28.0)
src/utils/marketplace.ts

[high] 165-166: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (1)
src/components/pages/MyANTs/NewListing.tsx (1)

183-191: Fix end-date preview for custom listings.

This still string-concats the custom date using formatDate(form.date.toString(), …), so new Date(endDate) feeds "Invalid DateT12:00" into the modal and schedule math. Please reuse mergeDateAndTime for the custom branch and pass the Date directly; for preset durations, forward form.date/form.time into getMsFromDuration so both preview and payload stay consistent.

-  const endDate =
-    form.duration === 'custom'
-      ? form.date
-        ? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
-        : now.toISOString()
-      : addMilliseconds(
-          now,
-          form.duration ? getMsFromDuration(form.duration) : 0,
-        ).toISOString();
+  const endDate =
+    form.duration === 'custom'
+      ? mergeDateAndTime(form.date, form.time) ?? now
+      : addMilliseconds(
+          now,
+          form.duration
+            ? getMsFromDuration(form.duration, form.date, form.time)
+            : 0,
+        );

(Remember to update the two new Date(endDate) call sites to use endDate directly.)

Comment on lines +223 to +232
console.log(`${operation} success`, { data });
void Promise.allSettled([
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.listings.all],
}),
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.myANTs.all],
}),
]);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface the actual error message.

error is unknown in React Query v5 callbacks. Accessing error.message throws when the rejection is a string or custom shape, masking the original failure and breaking our error snackbar. Either narrow with instanceof Error or coerce safely before emitting.

-                    onError: (error) => {
-                      eventEmitter.emit('error', {
-                        name: `Failed to ${operation}`,
-                        message: error.message,
-                      });
+                    onError: (error) => {
+                      const message =
+                        error instanceof Error
+                          ? error.message
+                          : typeof error === 'string'
+                            ? error
+                            : 'Unknown error';
+                      eventEmitter.emit('error', {
+                        name: `Failed to ${operation}`,
+                        message,
+                      });
                     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(`${operation} success`, { data });
void Promise.allSettled([
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.listings.all],
}),
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.myANTs.all],
}),
]);
},
console.log(`${operation} success`, { data });
void Promise.allSettled([
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.listings.all],
}),
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.myANTs.all],
}),
]);
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: 'Unknown error';
eventEmitter.emit('error', {
name: `Failed to ${operation}`,
message,
});
},
🤖 Prompt for AI Agents
In src/components/pages/Listings/Confirm.tsx around lines 223-232, the onError
handler currently assumes error has a .message property but React Query v5
supplies error as unknown; change the handler to safely obtain a string by
checking instanceof Error and falling back to String(error) (e.g., const msg =
error instanceof Error ? error.message : String(error)) and then pass that msg
to the snackbar/logging; ensure you don't directly access error.message and keep
the error parameter typed as unknown.

Copy link
Contributor

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b48da4 and b47371b.

📒 Files selected for processing (1)
  • src/components/pages/MyANTs/PriceScheduleModal.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/pages/MyANTs/PriceScheduleModal.tsx (2)
src/utils/marketplace.ts (2)
  • DecreaseInterval (11-12)
  • getMsFromInterval (55-76)
src/utils/common/common.tsx (1)
  • formatDate (12-14)

Copy link
Contributor

@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: 3

🧹 Nitpick comments (9)
src/utils/marketplace.ts (3)

45-50: Harden time parsing to avoid NaN and out-of-range values.

Defensive parsing prevents Invalid Date when time is malformed and clamps values.

Apply this diff:

-  const [hours, minutes, seconds = 0] = time.split(':').map(Number);
+  const [h = '0', m = '0', s = '0'] = time.split(':');
+  const hours = Math.min(Math.max(parseInt(h, 10) || 0, 0), 23);
+  const minutes = Math.min(Math.max(parseInt(m, 10) || 0, 0), 59);
+  const seconds = Math.min(Math.max(parseInt(s, 10) || 0, 0), 59);
   const merged = new Date(date);
 
   merged.setHours(hours);
   merged.setMinutes(minutes);
   merged.setSeconds(seconds);

168-187: Ensure query keys remain serializable and stable.

Passing an arbitrary options object can break caching if it contains non‑serializable values or unstable identity.

  • Confirm options only includes JSON‑serializable primitives/arrays/objects (no Dates, functions).
  • If instability is observed, consider hashing or stringifying: JSON.stringify(options ?? {}) as the key segment (or a stable-hash helper). Based on learnings (TanStack Query v5 recommends serializable, stable keys).

14-37: Remove “FIXME: remove test code” from user-facing labels.

Avoid leaking test markers in production UI.

src/components/pages/MyANTs/NewListing.tsx (6)

17-17: Remove unused side‑effect import.

@src/utils/marketplace has no side effects; the typed imports below suffice.

Apply this diff:

-import '@src/utils/marketplace';

309-315: Pass a Date to PriceScheduleModal.

After fixing endDate to be a Date, avoid reparsing.

Apply this diff:

-                    <PriceScheduleModal
+                    <PriceScheduleModal
                       startingPrice={Number(form.price)}
                       minimumPrice={Number(form.minimumPrice)}
-                      dateFrom={now}
-                      dateTo={new Date(endDate)}
+                      dateFrom={now}
+                      dateTo={endDate}
                       decreaseInterval={form.decrease}
                     />

391-397: Same as above: avoid new Date(endDate).

Apply this diff:

-                  <PriceScheduleModal
+                  <PriceScheduleModal
                     startingPrice={Number(form.price)}
                     minimumPrice={Number(form.minimumPrice)}
-                    dateFrom={now}
-                    dateTo={new Date(endDate)}
+                    dateFrom={now}
+                    dateTo={endDate}
                     decreaseInterval={form.decrease}
                   />

159-168: Validate English custom duration requires date/time.

Avoids 0 duration from missing inputs.

Apply this diff:

               if (!form.duration) {
                 throw new Error('duration is missing');
               }
 
+              if (form.duration === 'custom' && (!form.date || !form.time)) {
+                throw new Error('date/time is missing for custom duration');
+              }

256-259: Prefer controlled Select with value over defaultValue.

Ensures the UI reflects state updates reliably.

If Select supports it, switch to value={form.type}; otherwise confirm the component is intended to be uncontrolled in this flow.


449-455: Harden error handling: support non-Error throws.

Avoids runtime when error isn’t an Error instance.

Apply this diff:

-                        onError: (error) => {
-                          console.error(error);
-                          eventEmitter.emit('error', {
-                            name: 'Failed to create listing',
-                            message: error.message,
-                          });
-                        },
+                        onError: (error) => {
+                          const message =
+                            error instanceof Error ? error.message : String(error);
+                          console.error(error);
+                          eventEmitter.emit('error', {
+                            name: 'Failed to create listing',
+                            message,
+                          });
+                        },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b47371b and 36b2811.

📒 Files selected for processing (2)
  • src/components/pages/MyANTs/NewListing.tsx (1 hunks)
  • src/utils/marketplace.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/pages/MyANTs/NewListing.tsx (4)
src/utils/marketplace.ts (12)
  • Duration (6-9)
  • DecreaseInterval (11-12)
  • mergeDateAndTime (39-53)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
  • getMsFromInterval (55-76)
  • getMsFromDuration (78-109)
  • dutchDurationOptions (22-29)
  • dutchDecreaseIntervalOptions (31-37)
  • englishDurationOptions (14-20)
  • marketplaceQueryKeys (168-194)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/components/pages/MyANTs/PriceScheduleModal.tsx (1)
  • PriceScheduleModal (24-86)
🪛 Gitleaks (8.28.0)
src/utils/marketplace.ts

[high] 165-166: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (3)
src/utils/marketplace.ts (2)

153-159: Complete tab‑nabbing defense with explicit opener nulling.

Good adding noopener,noreferrer. Add the fallback for older browsers.

Apply this diff:

-export const openAoLinkExplorer = (address: string) => {
-  window.open(
-    `${AO_LINK_EXPLORER_URL}/${address}`,
-    '_blank',
-    'noopener,noreferrer',
-  );
-};
+export const openAoLinkExplorer = (address: string) => {
+  const win = window.open(
+    `${AO_LINK_EXPLORER_URL}/${address}`,
+    '_blank',
+    'noopener,noreferrer',
+  );
+  if (win) win.opener = null;
+};

160-167: Gitleaks false positive? Verify these are public IDs, not secrets.

These look like public AO process/token IDs, but were flagged.

  • If public: rename to ..._PUBLIC_ID or add an allowlist entry in .gitleaks.toml to avoid noise.
  • If sensitive: move to env/secret store and inject at build time.
src/components/pages/MyANTs/NewListing.tsx (1)

190-199: Fix end date preview; build a Date via helpers (not string concat).

Current logic misuses formatDate and can produce invalid ISO strings.

Apply this diff:

-  const endDate =
-    form.duration === 'custom'
-      ? form.date
-        ? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
-        : now.toISOString()
-      : addMilliseconds(
-          now,
-          form.duration ? getMsFromDuration(form.duration) : 0,
-        ).toISOString();
+  const endDate: Date =
+    form.duration === 'custom'
+      ? mergeDateAndTime(form.date, form.time) ?? now
+      : addMilliseconds(
+          now,
+          form.duration ? getMsFromDuration(form.duration, form.date, form.time) : 0,
+        );

Comment on lines +409 to +415
form.date
? `${formatDate(form.date.toString(), 'dd.MM.yy')} ${
form.time
}`
: '-'
}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix date preview formatting for fixed listing.

Pass a Date to formatDate using mergeDateAndTime.

Apply this diff:

-                      value={
-                        form.date
-                          ? `${formatDate(form.date.toString(), 'dd.MM.yy')} ${form.time}`
-                          : '-'
-                      }
+                      value={
+                        form.date
+                          ? `${formatDate(mergeDateAndTime(form.date, form.time) ?? form.date)}`
+                          : '-'
+                      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/pages/MyANTs/NewListing.tsx around lines 409 to 415, the
preview passes form.date.toString() to formatDate which yields incorrect
formatting for fixed listings; use mergeDateAndTime(form.date, form.time) to
produce a Date and pass that Date into formatDate (e.g.,
formatDate(mergeDateAndTime(form.date, form.time), 'dd.MM.yy')) and keep the
time portion as before so the preview shows the correct combined date/time.

Copy link
Contributor

@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: 3

🧹 Nitpick comments (6)
src/utils/marketplace.ts (2)

170-197: Stabilize React Query keys; avoid object identity churn.

Including raw options in a key can thrash cache. Serialize or pick primitives.

Apply:

-    list: (type: 'active' | 'completed', options?: Record<string, unknown>) => [
-      marketplaceQueryKeys.listings.all,
-      type,
-      options,
-    ],
+    list: (type: 'active' | 'completed', options?: Record<string, unknown>) => [
+      marketplaceQueryKeys.listings.all,
+      type,
+      options ? JSON.stringify(options) : undefined,
+    ],

14-20: Remove “test code” options or gate them by env.

The “1 hour” and “5 minutes” entries are marked FIXME. Clean up before release or conditionally expose in dev.

Apply (if not needed):

-  { label: '1 hour', value: '1h' }, // FIXME: remove test code
+  // { label: '1 hour', value: '1h' },
-  { label: '1 hour', value: '1h' }, // FIXME: remove test code
+  // { label: '1 hour', value: '1h' },
-  { label: '5 minutes', value: '5m' }, // FIXME: remove test code
+  // { label: '5 minutes', value: '5m' },

Also applies to: 22-30, 31-37

src/components/pages/MyANTs/PriceScheduleModal.tsx (1)

33-65: Memoize schedule computation.

Compute once per relevant input change to avoid repeated heavy work.

Example:

+import { useMemo } from 'react';
...
-  const dutchPriceSchedule: Schedule[] = (() => {
+  const dutchPriceSchedule: Schedule[] = useMemo(() => {
     try {
       ...
     } catch (err) {
       console.warn('Error generating dutch price schedule', err);
       return [];
     }
-  })();
+  }, [startingPrice, minimumPrice, decreaseInterval, dateFrom, dateTo]);
src/components/pages/MyANTs/NewListing.tsx (3)

17-17: Remove unused side‑effect import.

This bare import has no side effects and duplicates the named import below.

Apply:

-import '@src/utils/marketplace';

315-321: Avoid stale “now” in schedule; compute at render.

Use a fresh Date for schedule dateFrom.

Apply:

-                    <PriceScheduleModal
+                    <PriceScheduleModal
                       startingPrice={Number(form.price)}
                       minimumPrice={Number(form.minimumPrice)}
-                      dateFrom={now}
+                      dateFrom={new Date()}
                       dateTo={new Date(endDate)}
                       decreaseInterval={form.decrease}
                     />

Repeat the same change in the summary section.

Also applies to: 398-403


41-50: Tighten types for form.state.type.

Use a union for safety.

Apply:

-interface FormState {
-  type: string;
+interface FormState {
+  type: '' | 'fixed' | 'english' | 'dutch';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36b2811 and c90a98c.

📒 Files selected for processing (3)
  • src/components/pages/MyANTs/NewListing.tsx (1 hunks)
  • src/components/pages/MyANTs/PriceScheduleModal.tsx (1 hunks)
  • src/utils/marketplace.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/pages/MyANTs/PriceScheduleModal.tsx (2)
src/utils/marketplace.ts (2)
  • DecreaseInterval (11-12)
  • getMsFromInterval (55-76)
src/utils/common/common.tsx (1)
  • formatDate (12-14)
src/components/pages/MyANTs/NewListing.tsx (4)
src/utils/marketplace.ts (12)
  • Duration (6-9)
  • DecreaseInterval (11-12)
  • mergeDateAndTime (39-53)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (164-165)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (166-167)
  • BLOCKYDEVS_SWAP_TOKEN_ID (168-169)
  • getMsFromInterval (55-76)
  • getMsFromDuration (78-109)
  • dutchDurationOptions (22-29)
  • dutchDecreaseIntervalOptions (31-37)
  • englishDurationOptions (14-20)
  • marketplaceQueryKeys (171-197)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/components/pages/MyANTs/PriceScheduleModal.tsx (1)
  • PriceScheduleModal (24-86)
🪛 Gitleaks (8.28.0)
src/utils/marketplace.ts

[high] 168-169: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (4)
src/utils/marketplace.ts (2)

156-162: Add opener-null fallback for older browsers.

You’ve added noopener/noreferrer; also nullify opener for robustness.

Apply:

 export const openAoLinkExplorer = (address: string) => {
-  window.open(
+  const win = window.open(
     `${AO_LINK_EXPLORER_URL}/${address}`,
     '_blank',
     'noopener,noreferrer',
   );
+  if (win) win.opener = null;
 };

164-170: Verify non-secret & externalize hard-coded IDs
Confirm the process/token IDs in src/utils/marketplace.ts (lines 164–170) are public; if they’re safe to commit, add a comment stating so. Otherwise, move them to env vars (e.g. import.meta.env VITE_*) instead of hard-coding.

src/components/pages/MyANTs/NewListing.tsx (2)

411-421: Fix expiration preview formatting.

Build the Date with mergeDateAndTime and format that value.

Apply:

-                    <Row
-                      label="Expiration time"
-                      value={
-                        form.date
-                          ? `${formatDate(form.date.toString(), 'dd.MM.yy')} ${form.time}`
-                          : '-'
-                      }
-                    />
+                    <Row
+                      label="Expiration time"
+                      value={
+                        form.date
+                          ? `${formatDate(mergeDateAndTime(form.date, form.time) ?? form.date)}`
+                          : '-'
+                      }
+                    />

196-206: Fix endDate preview; pass date/time to duration and avoid misusing formatDate.

Current logic builds invalid ISO for custom and ignores date/time for duration.

Apply:

-  const endDate =
-    form.duration === 'custom'
-      ? form.date
-        ? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
-        : now.toISOString()
-      : addMilliseconds(
-          now,
-          form.duration ? getMsFromDuration(form.duration) : 0,
-        ).toISOString();
+  const endDate =
+    form.duration === 'custom'
+      ? (mergeDateAndTime(form.date, form.time)?.toISOString() ??
+        new Date().toISOString())
+      : addMilliseconds(
+          new Date(),
+          form.duration
+            ? getMsFromDuration(form.duration, form.date, form.time)
+            : 0,
+        ).toISOString();

Comment on lines +131 to +157
if (!form.minimumPrice) {
throw new Error('minimum price is missing');
}

if (!form.decrease) {
throw new Error('decrease interval is missing');
}

if (!form.duration) {
throw new Error('duration is missing');
}

const decreaseIntervalMs = getMsFromInterval(form.decrease);
const durationMs = getMsFromDuration(
form.duration,
form.date,
form.time,
);

return {
type: form.type,
price: form.price.toString(),
minimumPrice: form.minimumPrice.toString(),
decreaseInterval: decreaseIntervalMs.toString(),
...(durationMs && { expiresAt: Date.now() + durationMs }),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Move min≤price and custom date/time validation to Dutch; remove from English.

The min≤price rule and custom date/time requirement belong to the Dutch flow, not English.

Apply:

             case 'dutch': {
               if (!form.minimumPrice) {
                 throw new Error('minimum price is missing');
               }
 
               if (!form.decrease) {
                 throw new Error('decrease interval is missing');
               }
 
               if (!form.duration) {
                 throw new Error('duration is missing');
               }
 
+              if (form.duration === 'custom' && (!form.date || !form.time)) {
+                throw new Error('date/time is missing for custom duration');
+              }
+
+              if (Number(form.minimumPrice) > Number(form.price)) {
+                throw new Error('minimum price cannot exceed starting price');
+              }
+
               const decreaseIntervalMs = getMsFromInterval(form.decrease);
               const durationMs = getMsFromDuration(
                 form.duration,
                 form.date,
                 form.time,
               );
 
               return {
                 type: form.type,
                 price: form.price.toString(),
                 minimumPrice: form.minimumPrice.toString(),
                 decreaseInterval: decreaseIntervalMs.toString(),
                 ...(durationMs && { expiresAt: Date.now() + durationMs }),
               };
             }
             case 'english': {
               if (!form.duration) {
                 throw new Error('duration is missing');
               }
-              if (form.duration === 'custom' && (!form.date || !form.time)) {
-                throw new Error('date/time is missing for custom duration');
-              }
-              if (Number(form.minimumPrice) > Number(form.price)) {
-                throw new Error('minimum price cannot exceed starting price');
-              }
+              if (form.duration === 'custom' && (!form.date || !form.time)) {
+                throw new Error('date/time is missing for custom duration');
+              }

Also applies to: 158-167

🤖 Prompt for AI Agents
In src/components/pages/MyANTs/NewListing.tsx around lines 131-157 (and also
adjust same logic at 158-167), the validations enforcing minimumPrice ≤ price
and requiring custom date/time are currently applied unconditionally; move these
checks into the Dutch-language-specific branch so they only run for the Dutch
flow. Remove or guard the minimumPrice and custom date/time validation from the
English path, and ensure getMsFromInterval/getMsFromDuration usage and the
returned payload remain unchanged for each language branch. Update the
conditional logic so Dutch flow performs the min≤price check and custom
date/time requirement before computing decreaseIntervalMs/durationMs, while
English skips those validations.

Comment on lines +46 to +49
const decreaseStepMario = Math.round(
Number(arioToMario(decreaseStepArio)),
).toString();

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against zero step after rounding (prevents invalid schedule).

Small positive ARIO deltas can round to 0 MARIO and break getDutchListingSchedule.

Apply:

-      const decreaseStepMario = Math.round(
-        Number(arioToMario(decreaseStepArio)),
-      ).toString();
+      const stepMarioNum = Math.round(Number(arioToMario(decreaseStepArio)));
+      if (stepMarioNum < 1) return [];
+      const decreaseStepMario = stepMarioNum.toString();

Also applies to: 50-60

🤖 Prompt for AI Agents
In src/components/pages/MyANTs/PriceScheduleModal.tsx around lines 46 to 49 (and
similarly for the other ARIO→MARIO conversions in lines 50-60), the code rounds
small positive ARIO deltas to 0 MARIO which can produce an invalid schedule;
after converting and rounding, enforce a minimum step of 1 MARIO (e.g. take
Math.max(1, roundedValue)) before converting to string so the decrease/increase
step never becomes "0"; apply the same guard to all other ARIO→MARIO step
conversions in the 50-60 range.

Comment on lines 127 to 151
export const getCurrentListingArioPrice = (listing: ListingDetails) => {
const marioPrice = (() => {
if (listing.type === 'english') {
return listing.highestBid ?? listing.startingPrice;
}

if (listing.type === 'dutch' && listing.status !== 'settled') {
return calculateCurrentDutchListingPrice({
startingPrice: listing.startingPrice,
minimumPrice: listing.minimumPrice,
decreaseInterval: listing.decreaseInterval,
decreaseStep: listing.decreaseStep,
createdAt: new Date(listing.createdAt).getTime(),
});
}

if (listing.status === 'settled') {
return listing.finalPrice;
}

return listing.price;
})();

if (marioPrice == null) {
return 0;
}
return marioToArio(marioPrice);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Settled English listings ignore finalPrice; reorder branches.

English listings return highestBid/startingPrice even when settled. Check settled first, then type-specific logic.

Apply:

 export const getCurrentListingArioPrice = (listing: ListingDetails) => {
   const marioPrice = (() => {
-    if (listing.type === 'english') {
-      return listing.highestBid ?? listing.startingPrice;
-    }
-
-    if (listing.type === 'dutch' && listing.status !== 'settled') {
+    if (listing.status === 'settled') {
+      return listing.finalPrice;
+    }
+
+    if (listing.type === 'english') {
+      return listing.highestBid ?? listing.startingPrice;
+    }
+
+    if (listing.type === 'dutch') {
       return calculateCurrentDutchListingPrice({
         startingPrice: listing.startingPrice,
         minimumPrice: listing.minimumPrice,
         decreaseInterval: listing.decreaseInterval,
         decreaseStep: listing.decreaseStep,
         createdAt: new Date(listing.createdAt).getTime(),
       });
     }
-
-    if (listing.status === 'settled') {
-      return listing.finalPrice;
-    }
-
     return listing.price;
   })();
 
   if (marioPrice == null) {
     return 0;
   }
   return marioToArio(marioPrice);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getCurrentListingArioPrice = (listing: ListingDetails) => {
const marioPrice = (() => {
if (listing.type === 'english') {
return listing.highestBid ?? listing.startingPrice;
}
if (listing.type === 'dutch' && listing.status !== 'settled') {
return calculateCurrentDutchListingPrice({
startingPrice: listing.startingPrice,
minimumPrice: listing.minimumPrice,
decreaseInterval: listing.decreaseInterval,
decreaseStep: listing.decreaseStep,
createdAt: new Date(listing.createdAt).getTime(),
});
}
if (listing.status === 'settled') {
return listing.finalPrice;
}
return listing.price;
})();
if (marioPrice == null) {
return 0;
}
return marioToArio(marioPrice);
};
export const getCurrentListingArioPrice = (listing: ListingDetails) => {
const marioPrice = (() => {
if (listing.status === 'settled') {
return listing.finalPrice;
}
if (listing.type === 'english') {
return listing.highestBid ?? listing.startingPrice;
}
if (listing.type === 'dutch') {
return calculateCurrentDutchListingPrice({
startingPrice: listing.startingPrice,
minimumPrice: listing.minimumPrice,
decreaseInterval: listing.decreaseInterval,
decreaseStep: listing.decreaseStep,
createdAt: new Date(listing.createdAt).getTime(),
});
}
return listing.price;
})();
if (marioPrice == null) {
return 0;
}
return marioToArio(marioPrice);
};
🤖 Prompt for AI Agents
In src/utils/marketplace.ts around lines 127 to 154, the current branch order
causes settled English listings to return highestBid/startingPrice instead of
finalPrice; change the logic to check listing.status === 'settled' first and
return listing.finalPrice (or 0 if null) before any type-specific handling, then
handle listing.type === 'english' (return highestBid ?? startingPrice),
listing.type === 'dutch' (calculate current dutch price when status !==
'settled'), and finally fall back to listing.price; ensure null checks remain
and that the function still converts the resolved marioPrice with marioToArio.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (6)
src/utils/marketplace.ts (4)

1-3: Avoid utils → UI dependency; move calculator out of UI.

A shared util importing from a UI package is a layering smell and risks cycles. Prefer housing calculateCurrentDutchListingPrice in data/utility layers and importing it here (or re-exporting it from data).


14-20: Remove or gate “FIXME: test code” options.

These test-only options should either be removed or gated behind a dev flag to avoid leaking into prod UX and types.

Also applies to: 22-29, 31-37


45-51: Harden time parsing against NaN.

Invalid/partial time strings produce NaN and can yield Invalid Date. Default missing/NaN parts to 0.

Apply:

-  const [hours, minutes, seconds = 0] = time.split(':').map(Number);
+  const [h, m, s] = time.split(':');
+  const hours = Number(h) || 0;
+  const minutes = Number(m) || 0;
+  const seconds = Number(s) || 0;

182-187: Stabilize query keys; avoid raw objects in keys.

Passing an object directly can cause cache misses due to identity changes. Serialize or normalize.

Apply:

-    list: (type: 'active' | 'completed', options?: Record<string, unknown>) => [
-      marketplaceQueryKeys.listings.all,
-      type,
-      options,
-    ],
+    list: (type: 'active' | 'completed', options?: Record<string, unknown>) => [
+      marketplaceQueryKeys.listings.all,
+      type,
+      options ? JSON.stringify(options) : undefined,
+    ],

Based on learnings

src/components/pages/MyANTs/NewListing.tsx (2)

17-17: Remove side‑effect import.

import '@src/utils/marketplace'; is unused; drop it.

Apply:

-import '@src/utils/marketplace';

449-455: Type‑safe error handling in onError.

error is unknown in TanStack v5. Narrow before accessing .message.

Apply:

-                        onError: (error) => {
-                          console.error(error);
-                          eventEmitter.emit('error', {
-                            name: 'Failed to create listing',
-                            message: error.message,
-                          });
-                        },
+                        onError: (error) => {
+                          console.error(error);
+                          const err = error instanceof Error ? error : new Error(String(error));
+                          eventEmitter.emit('error', {
+                            name: 'Failed to create listing',
+                            message: err.message,
+                          });
+                        },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c90a98c and f876dd6.

📒 Files selected for processing (2)
  • src/components/pages/MyANTs/NewListing.tsx (1 hunks)
  • src/utils/marketplace.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/pages/MyANTs/NewListing.tsx (4)
src/utils/marketplace.ts (12)
  • Duration (6-9)
  • DecreaseInterval (11-12)
  • mergeDateAndTime (39-53)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
  • getMsFromInterval (55-76)
  • getMsFromDuration (78-109)
  • dutchDurationOptions (22-29)
  • dutchDecreaseIntervalOptions (31-37)
  • englishDurationOptions (14-20)
  • marketplaceQueryKeys (168-194)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/components/pages/MyANTs/PriceScheduleModal.tsx (1)
  • PriceScheduleModal (24-86)
🪛 Gitleaks (8.28.0)
src/utils/marketplace.ts

[high] 165-166: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (7)
src/utils/marketplace.ts (3)

153-159: Add tab‑nabbing fallback: set opener to null.

Keep noopener,noreferrer and also nullify opener when possible.

Apply:

 export const openAoLinkExplorer = (address: string) => {
-  window.open(
-    `${AO_LINK_EXPLORER_URL}/${address}`,
-    '_blank',
-    'noopener,noreferrer',
-  );
+  const newWin = window.open(
+    `${AO_LINK_EXPLORER_URL}/${address}`,
+    '_blank',
+    'noopener,noreferrer',
+  );
+  if (newWin) newWin.opener = null;
 };

161-166: Verify these IDs are public constants, not secrets.

Static analysis flagged them. If they are public process/token IDs, add a comment noting they’re non‑secret. If not, move to env/config.


127-151: Settled listings ignore finalPrice; reorder and add null guard.

Check settled first and guard null before converting. Prevents wrong price for settled English and avoids passing undefined to marioToArio.

Apply:

 export const getCurrentListingArioPrice = (listing: ListingDetails) => {
   const marioPrice = (() => {
-    if (listing.type === 'english') {
-      return listing.highestBid ?? listing.startingPrice;
-    }
-
-    if (listing.type === 'dutch' && listing.status !== 'settled') {
+    if (listing.status === 'settled') {
+      return listing.finalPrice;
+    }
+
+    if (listing.type === 'english') {
+      return listing.highestBid ?? listing.startingPrice;
+    }
+
+    if (listing.type === 'dutch') {
       return calculateCurrentDutchListingPrice({
         startingPrice: listing.startingPrice,
         minimumPrice: listing.minimumPrice,
         decreaseInterval: listing.decreaseInterval,
         decreaseStep: listing.decreaseStep,
         createdAt: new Date(listing.createdAt).getTime(),
       });
     }
-
-    if (listing.status === 'settled') {
-      return listing.finalPrice;
-    }
-
     return listing.price;
   })();
 
-  return marioToArio(marioPrice);
+  return marioToArio(marioPrice ?? 0);
 };
src/components/pages/MyANTs/NewListing.tsx (4)

190-199: Fix endDate preview and use proper merge/time math.

Avoid string concat and pass date/time into duration math to get correct ISO.

Apply:

-  const endDate =
-    form.duration === 'custom'
-      ? form.date
-        ? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
-        : now.toISOString()
-      : addMilliseconds(
-          now,
-          form.duration ? getMsFromDuration(form.duration) : 0,
-        ).toISOString();
+  const endDate =
+    form.duration === 'custom'
+      ? mergeDateAndTime(form.date, form.time)?.toISOString() ??
+        now.toISOString()
+      : addMilliseconds(
+          now,
+          form.duration
+            ? getMsFromDuration(form.duration, form.date, form.time)
+            : 0,
+        ).toISOString();

409-415: Format fixed expiration with merged Date, not string.

Pass a Date to formatter using mergeDateAndTime.

Apply:

-                      value={
-                        form.date
-                          ? `${formatDate(form.date.toString(), 'dd.MM.yy')} ${form.time}`
-                          : '-'
-                      }
+                      value={
+                        form.date
+                          ? `${formatDate(mergeDateAndTime(form.date, form.time) ?? form.date)}`
+                          : '-'
+                      }

131-149: Dutch validation gaps: require custom date/time and enforce min ≤ price.

Prevents silent 0‑duration and invalid schedules.

Apply:

             case 'dutch': {
               if (!form.minimumPrice) {
                 throw new Error('minimum price is missing');
               }

               if (!form.decrease) {
                 throw new Error('decrease interval is missing');
               }

               if (!form.duration) {
                 throw new Error('duration is missing');
               }
+
+              if (form.duration === 'custom' && (!form.date || !form.time)) {
+                throw new Error('date/time is missing for custom duration');
+              }
+
+              if (Number(form.minimumPrice) > Number(form.price)) {
+                throw new Error('minimum price cannot exceed starting price');
+              }

159-167: English custom requires date/time.

Guard to avoid “Unsupported duration: undefined” UX.

Apply:

             case 'english': {
               if (!form.duration) {
                 throw new Error('duration is missing');
               }
+              if (form.duration === 'custom' && (!form.date || !form.time)) {
+                throw new Error('date/time is missing for custom duration');
+              }

@md-blockydevs md-blockydevs force-pushed the feat-data-lib-integration branch from f876dd6 to 5c7693e Compare September 26, 2025 13:15
@md-blockydevs md-blockydevs force-pushed the feat-data-lib-integration branch from 5c7693e to 034797e Compare September 26, 2025 13:15
Copy link
Contributor

@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: 10

🧹 Nitpick comments (15)
src/components/pages/Listings/SearchListingByName.tsx (1)

40-44: Trim input before submitting

Avoid empty/whitespace-only queries and pass the trimmed value to the mutation.

   const handleSearchSubmit = (e: React.FormEvent) => {
     e.preventDefault();
-    if (!searchValue) return;
-    mutationSearch.mutate(searchValue);
+    const term = searchValue.trim();
+    if (!term) return;
+    mutationSearch.mutate(term);
   };
src/components/pages/Listings/Confirm.tsx (1)

23-33: Prefer tuple destructuring for useSearchParams

Improves readability and typing.

-  const searchParams = useSearchParams();
+  const [searchParams] = useSearchParams();
...
-  const name = searchParams[0].get('name') ?? '-';
-  const antProcessId = searchParams[0].get('antProcessId');
-  const price = searchParams[0].get('price');
-  const type = searchParams[0].get('type');
+  const name = searchParams.get('name') ?? '-';
+  const antProcessId = searchParams.get('antProcessId');
+  const price = searchParams.get('price');
+  const type = searchParams.get('type');
src/components/pages/MyANTs/NewListing.tsx (7)

17-17: Remove unused side‑effect import

This pulls the whole module for no reason.

-import '@src/utils/marketplace';

310-315: Avoid reparsing endDate

You already have an ISO string; constructing a Date repeatedly is fine but unnecessary in render. If you keep endDate as ISO string, this is OK. If you switch to a Date later, pass it directly. Optional clean‑up.

-                    <PriceScheduleModal
-                      startingPrice={Number(form.price)}
-                      minimumPrice={Number(form.minimumPrice)}
-                      dateFrom={now}
-                      dateTo={new Date(endDate)}
-                      decreaseInterval={form.decrease}
-                    />
+                    <PriceScheduleModal
+                      startingPrice={Number(form.price)}
+                      minimumPrice={Number(form.minimumPrice)}
+                      dateFrom={now}
+                      dateTo={new Date(endDate)}
+                      decreaseInterval={form.decrease}
+                    />

Note: Keep as-is if you adopt the ISO approach above; if you switch endDate to a Date, pass dateTo={endDate}.


323-329: Make Select controlled (English duration)

Avoid uncontrolled/controlled mismatch by binding value.

-                    <Select
+                    <Select
+                      value={form.duration}
                       placeholder="Choose duration"
                       className="w-full"
                       options={englishDurationOptions}
                       onValueChange={(value) =>
                         updateForm('duration', value as Duration)
                       }
                     />

281-287: Make Select controlled (Dutch duration)

Bind current value.

-                    <Select
+                    <Select
+                      value={form.duration}
                       placeholder="Choose duration"
                       className="w-full"
                       options={dutchDurationOptions}
                       onValueChange={(value) =>
                         updateForm('duration', value as Duration)
                       }
                     />

301-307: Make Select controlled (decrease interval)

Bind current value to prevent desync.

-                    <Select
+                    <Select
+                      value={form.decrease}
                       placeholder="Choose decrease interval"
                       className="w-full"
                       options={dutchDecreaseIntervalOptions}
                       onValueChange={(value) =>
                         updateForm('decrease', value as DecreaseInterval)
                       }
                     />

256-259: Make listing type Select controlled

Use value instead of defaultValue.

-                <Select
+                <Select
                   placeholder="Type of listing"
                   className="w-full"
-                  defaultValue={form.type}
+                  value={form.type}
                   onValueChange={(value) => updateForm('type', value)}
                   options={typeOptions}
                 />

261-268: Validate positive price before proceeding (optional UX guard)

Prevent accidental zero/negative price submissions client-side.

               <Input
                 onChange={(e) => updateForm('price', e.target.value)}
                 value={form.price}
-                min={0}
+                min={0.000001}
                 label="Price"
                 suffix="ARIO"
                 type="number"
               />

Additionally, add a check in mutationFn to ensure Number(form.price) > 0 and emit a clear error.

src/components/pages/Listings/Details/FixedListingPriceSection.tsx (1)

26-36: Optional: guard against non-active listings

If this button can render for non-active listings, consider hiding/disable accordingly.

No code change if upstream already gates rendering.

src/components/pages/Listings/Details/ListingBuyerSection.tsx (2)

35-37: Remove conflicting text color classes.

Both text-white and text-[var(--ar-color-neutral-400)] set color; keep one to avoid unpredictable styling precedence.

-        <span className="text-white text-sm font-normal text-[var(--ar-color-neutral-400)]">
+        <span className="text-sm font-normal text-[var(--ar-color-neutral-400)]">
           {buyerAddress === walletAddress?.toString() && '(Your wallet)'}
         </span>

24-34: Add an accessible label to the external link button.

Improves a11y for screen readers.

         <Button
           variant="link"
           className="px-0 gap-1"
+          aria-label={`Open ${shortenAddress(buyerAddress)} in explorer`}
           icon={<ExternalLink width={16} height={16} />}
           iconPlacement="right"
           onClick={() => {
             openAoLinkExplorer(buyerAddress);
           }}
         >
src/components/pages/Listings/CompletedListingsTab.tsx (1)

25-67: Move side-effects out of select; use onSuccess for cursor management.

select should be pure in React Query v5; put pagination.storeNextCursor in onSuccess.

   const queryCompletedListings = useQuery({
     refetchInterval: 15 * 1000,
     enabled: Boolean(aoClient),
     queryKey: marketplaceQueryKeys.listings.list('completed', {
       page: pagination.page,
       pageSize: pagination.pageSize,
     }),
     queryFn: () => {
       return fetchCompletedListings({
         ao: aoClient,
         activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
         limit: pagination.pageSize,
         cursor: pagination.cursor,
       });
     },
+    onSuccess: (data) => {
+      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
+    },
     select: (data) => {
-      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
-
       return {
         ...data,
         items: data.items.map((item): Domain => {
           const currentPrice = getCurrentListingArioPrice(item);
 
           return {
             name: item.name,
             createdAt: item.createdAt,
             endDate: item.endedAt,
             ownershipType: item.ownershipType,
             price: {
               type: item.type === 'english' ? 'bid' : 'buyout',
               symbol: 'ARIO',
               value: Number(currentPrice),
             },
             type: {
               value: item.type,
             },
             action: () => {
               navigate(`/listings/${item.orderId}`);
             },
           };
         }),
       };
     },
   });

As a follow-up, consider placeholderData to reduce UI flicker during page changes in cursor pagination.

src/components/pages/Listings/ActiveListingsTab.tsx (1)

40-67: Move side-effects out of select; use onSuccess for cursor management.

Keep select pure; storeNextCursor belongs in onSuccess.

     queryFn: () => {
       return fetchActiveListings({
         ao: aoClient,
         activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
         limit: pagination.pageSize,
         cursor: pagination.cursor,
       });
     },
+    onSuccess: (data) => {
+      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
+    },
     select: (data) => {
-      pagination.storeNextCursor(data.nextCursor, !!data.hasMore);
-
       return {
         ...data,
         items: data.items.map((item): Domain => {
           const currentPrice = getCurrentListingArioPrice(item);
 
           return {
             name: item.name,
             endDate: item.expiresAt ?? undefined,
             ownershipType: item.ownershipType,
             price: {
               type: item.type === 'english' ? 'bid' : 'buyout',
               symbol: 'ARIO',
               value: Number(currentPrice),
             },
             type: {
               value: item.type,
             },
             action: () => {
               navigate(`/listings/${item.orderId}`);
             },
           };
         }),
       };
     },
src/components/pages/Listings/Details/ListingMetadata.tsx (1)

21-38: Memoize the Dutch schedule computation.

Avoids recomputing potentially large schedules on every render.

-const ListingMetadata = ({ listing }: Props) => {
-  const dutchPriceSchedule: Schedule[] =
-    listing.type === 'dutch'
-      ? getDutchListingSchedule({
-          startingPrice: listing.startingPrice,
-          minimumPrice: listing.minimumPrice,
-          decreaseInterval: listing.decreaseInterval,
-          decreaseStep: listing.decreaseStep,
-          createdAt: new Date(listing.createdAt).getTime(),
-          endedAt: new Date(
-            'endedAt' in listing ? listing.endedAt : listing.expiresAt,
-          ).getTime(),
-        }).map((item) => ({
-          date: formatDate(item.date),
-          price: Number(marioToArio(item.price)),
-        }))
-      : [];
+const ListingMetadata = ({ listing }: Props) => {
+  const dutchPriceSchedule: Schedule[] = useMemo(() => {
+    if (listing.type !== 'dutch') return [];
+    return getDutchListingSchedule({
+      startingPrice: listing.startingPrice,
+      minimumPrice: listing.minimumPrice,
+      decreaseInterval: listing.decreaseInterval,
+      decreaseStep: listing.decreaseStep,
+      createdAt: new Date(listing.createdAt).getTime(),
+      endedAt: new Date(
+        'endedAt' in listing ? listing.endedAt : listing.expiresAt,
+      ).getTime(),
+    }).map((item) => ({
+      date: formatDate(item.date),
+      price: Number(marioToArio(item.price)),
+    }));
+  }, [listing]);

Add the missing import:

// at top of file
import { useMemo } from 'react';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f876dd6 and 034797e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (27)
  • package.json (3 hunks)
  • src/App.tsx (3 hunks)
  • src/components/cards/NavMenuCard/NavMenuCard.tsx (1 hunks)
  • src/components/layout/Navbar/NavGroup/NavGroup.tsx (0 hunks)
  • src/components/pages/Listings/ActiveListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/CompletedListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/Confirm.tsx (1 hunks)
  • src/components/pages/Listings/Details/Details.tsx (1 hunks)
  • src/components/pages/Listings/Details/DutchListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/EnglishListingBidsSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/EnglishListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/EnglishListingSettlementSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/FixedListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingBuyerSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingExpiredSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingMetadata.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Listings.tsx (1 hunks)
  • src/components/pages/Listings/SearchListingByName.tsx (1 hunks)
  • src/components/pages/MyANTs/MyANTs.tsx (1 hunks)
  • src/components/pages/MyANTs/NewListing.tsx (1 hunks)
  • src/components/pages/MyANTs/PriceScheduleModal.tsx (1 hunks)
  • src/components/pages/index.ts (2 hunks)
  • src/main.tsx (1 hunks)
  • src/utils/marketplace.ts (1 hunks)
  • src/utils/routes.tsx (3 hunks)
  • vite.config.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/components/layout/Navbar/NavGroup/NavGroup.tsx
🚧 Files skipped from review as they are similar to previous changes (12)
  • package.json
  • src/components/pages/index.ts
  • vite.config.ts
  • src/main.tsx
  • src/components/pages/Listings/Details/ListingExpiredSection.tsx
  • src/utils/marketplace.ts
  • src/components/pages/Listings/Details/EnglishListingPriceSection.tsx
  • src/components/pages/Listings/Details/EnglishListingBidsSection.tsx
  • src/components/pages/Listings/Details/ListingPriceSection.tsx
  • src/components/cards/NavMenuCard/NavMenuCard.tsx
  • src/components/pages/MyANTs/MyANTs.tsx
  • src/components/pages/Listings/Details/EnglishListingSettlementSection.tsx
🧰 Additional context used
🧬 Code graph analysis (14)
src/components/pages/MyANTs/PriceScheduleModal.tsx (2)
src/utils/marketplace.ts (2)
  • DecreaseInterval (11-12)
  • getMsFromInterval (55-76)
src/utils/common/common.tsx (1)
  • formatDate (12-14)
src/components/pages/Listings/Details/Details.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (4)
  • marketplaceQueryKeys (168-194)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • getCurrentListingArioPrice (127-151)
  • getStatusVariantFromListing (111-125)
src/components/pages/Listings/Details/ListingBuyerSection.tsx (2)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (1)
  • openAoLinkExplorer (153-159)
src/components/pages/MyANTs/NewListing.tsx (2)
src/utils/marketplace.ts (12)
  • Duration (6-9)
  • DecreaseInterval (11-12)
  • mergeDateAndTime (39-53)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
  • getMsFromInterval (55-76)
  • getMsFromDuration (78-109)
  • dutchDurationOptions (22-29)
  • dutchDecreaseIntervalOptions (31-37)
  • englishDurationOptions (14-20)
  • marketplaceQueryKeys (168-194)
src/components/pages/MyANTs/PriceScheduleModal.tsx (1)
  • PriceScheduleModal (24-86)
src/App.tsx (2)
src/components/pages/index.ts (3)
  • Manage (13-13)
  • Home (11-11)
  • Listings (12-12)
src/utils/routes.tsx (1)
  • Route (28-36)
src/utils/routes.tsx (1)
src/components/pages/index.ts (1)
  • Listings (12-12)
src/components/pages/Listings/Details/FixedListingPriceSection.tsx (2)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (127-151)
src/components/pages/Listings/Confirm.tsx (4)
src/utils/network.ts (1)
  • queryClient (39-45)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (3)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
  • marketplaceQueryKeys (168-194)
src/components/pages/Listings/CompletedListingsTab.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (3)
  • marketplaceQueryKeys (168-194)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • getCurrentListingArioPrice (127-151)
src/components/pages/Listings/Listings.tsx (1)
src/components/pages/index.ts (1)
  • Listings (12-12)
src/components/pages/Listings/ActiveListingsTab.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (3)
  • marketplaceQueryKeys (168-194)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • getCurrentListingArioPrice (127-151)
src/components/pages/Listings/Details/ListingMetadata.tsx (1)
src/utils/marketplace.ts (1)
  • openAoLinkExplorer (153-159)
src/components/pages/Listings/SearchListingByName.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (1)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
src/components/pages/Listings/Details/DutchListingPriceSection.tsx (2)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (127-151)
🔇 Additional comments (7)
src/components/pages/MyANTs/NewListing.tsx (2)

409-415: Fix fixed-listing date preview formatting (duplicate of prior review)

Format the merged date/time instead of using toString().

-                      value={
-                        form.date
-                          ? `${formatDate(form.date.toString(), 'dd.MM.yy')} ${form.time}`
-                          : '-'
-                      }
+                      value={
+                        form.date
+                          ? `${formatDate(mergeDateAndTime(form.date, form.time) ?? form.date)}`
+                          : '-'
+                      }

190-199: Fix endDate computation: handle custom date/time and duration properly

Current logic misuses formatDate and ignores date/time for duration.

-  const endDate =
-    form.duration === 'custom'
-      ? form.date
-        ? `${formatDate(form.date.toString(), 'yyyy-MM-dd')}T${form.time}`
-        : now.toISOString()
-      : addMilliseconds(
-          now,
-          form.duration ? getMsFromDuration(form.duration) : 0,
-        ).toISOString();
+  const endDate =
+    form.duration === 'custom'
+      ? mergeDateAndTime(form.date, form.time)?.toISOString() ?? now.toISOString()
+      : addMilliseconds(
+          now,
+          form.duration ? getMsFromDuration(form.duration, form.date, form.time) : 0,
+        ).toISOString();
src/components/pages/Listings/Listings.tsx (1)

12-35: LGTM

Composition, tabs, and search wiring look good.

src/utils/routes.tsx (1)

62-69: LGTM

Route entry for Listings is consistent with existing pattern and icon wrappers.

src/components/pages/Listings/Confirm.tsx (1)

216-221: React Query v5: safely surface error message (duplicate of prior review)

Same issue as previously noted: error is unknown; .message access is unsafe.

-                    onError: (error) => {
-                      eventEmitter.emit('error', {
-                        name: `Failed to ${operation}`,
-                        message: error.message,
-                      });
-                    },
+                    onError: (error) => {
+                      const message =
+                        error instanceof Error
+                          ? error.message
+                          : typeof error === 'string'
+                            ? error
+                            : 'Unknown error';
+                      eventEmitter.emit('error', {
+                        name: `Failed to ${operation}`,
+                        message,
+                      });
+                    },
src/components/pages/Listings/Details/ListingMetadata.tsx (1)

41-94: LGTM.

Metadata layout and explorer links look solid.

src/components/pages/MyANTs/PriceScheduleModal.tsx (1)

46-48: Prevent zero decrease step after rounding

Math.round can yield 0 for tiny ARIO deltas, so we end up passing a zero decreaseStep to getDutchListingSchedule, which violates its contract and breaks schedule generation. Add a guard right after rounding to reject/exit when the Mario step collapses to zero.

-      const decreaseStepMario = Math.round(
-        Number(arioToMario(decreaseStepArio)),
-      ).toString();
+      const decreaseStepMarioNum = Math.round(
+        Number(arioToMario(decreaseStepArio)),
+      );
+      if (decreaseStepMarioNum < 1) return [];
+      const decreaseStepMario = decreaseStepMarioNum.toString();

Comment on lines 25 to 33
const queryActiveListings = useQuery({
refetchInterval: 15 * 1000,
structuralSharing: false,
queryKey: marketplaceQueryKeys.listings.list('active', {
page: pagination.page,
pageSize: pagination.pageSize,
}),
queryFn: () => {
return fetchActiveListings({
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard the query until aoClient is ready.

Without enabled, the query may run with aoClient undefined and fail.

   const queryActiveListings = useQuery({
     refetchInterval: 15 * 1000,
     structuralSharing: false,
+    enabled: Boolean(aoClient),
     queryKey: marketplaceQueryKeys.listings.list('active', {
       page: pagination.page,
       pageSize: pagination.pageSize,
     }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const queryActiveListings = useQuery({
refetchInterval: 15 * 1000,
structuralSharing: false,
queryKey: marketplaceQueryKeys.listings.list('active', {
page: pagination.page,
pageSize: pagination.pageSize,
}),
queryFn: () => {
return fetchActiveListings({
const queryActiveListings = useQuery({
refetchInterval: 15 * 1000,
structuralSharing: false,
enabled: Boolean(aoClient),
queryKey: marketplaceQueryKeys.listings.list('active', {
page: pagination.page,
pageSize: pagination.pageSize,
}),
queryFn: () => {
return fetchActiveListings({
🤖 Prompt for AI Agents
In src/components/pages/Listings/ActiveListingsTab.tsx around lines 25 to 33,
the useQuery may run before aoClient is initialized causing failures; add an
enabled flag that checks aoClient readiness (e.g., enabled: !!aoClient) to the
useQuery options so the query will not execute until aoClient is defined, and
ensure the queryFn only references aoClient when enabled.

Comment on lines 22 to 35
const queryDetails = useQuery({
enabled: !!id,
refetchInterval: 15 * 1000,
queryKey: marketplaceQueryKeys.listings.item(id),
queryFn: () => {
if (!id) throw new Error('guard: no id provided');

return fetchListingDetails({
ao: aoClient,
activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
orderId: id,
});
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Enable the query only when both id and aoClient are available.

Prevents calling fetchListingDetails with an undefined client.

   const queryDetails = useQuery({
-    enabled: !!id,
+    enabled: Boolean(id && aoClient),
     refetchInterval: 15 * 1000,
     queryKey: marketplaceQueryKeys.listings.item(id),
     queryFn: () => {
       if (!id) throw new Error('guard: no id provided');
 
       return fetchListingDetails({
         ao: aoClient,
         activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
         orderId: id,
       });
     },
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const queryDetails = useQuery({
enabled: !!id,
refetchInterval: 15 * 1000,
queryKey: marketplaceQueryKeys.listings.item(id),
queryFn: () => {
if (!id) throw new Error('guard: no id provided');
return fetchListingDetails({
ao: aoClient,
activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
orderId: id,
});
},
});
const queryDetails = useQuery({
enabled: Boolean(id && aoClient),
refetchInterval: 15 * 1000,
queryKey: marketplaceQueryKeys.listings.item(id),
queryFn: () => {
if (!id) throw new Error('guard: no id provided');
return fetchListingDetails({
ao: aoClient,
activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
orderId: id,
});
},
});
🤖 Prompt for AI Agents
In src/components/pages/Listings/Details/Details.tsx around lines 22 to 35, the
useQuery is enabled only when id exists but may run with aoClient undefined;
change the enabled condition to require both id and aoClient (e.g. enabled: !!id
&& !!aoClient) and also add a defensive guard at the top of queryFn to throw if
aoClient or id is missing before calling fetchListingDetails so
fetchListingDetails never receives an undefined client.

Comment on lines +45 to +49
if (queryDetails.error) {
return (
<p className="text-error text-center">
Failed to load listing details: {queryDetails.error.message}
</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Safely render error messages.

error can be unknown; accessing .message directly may throw type errors. Use a safe fallback.

   if (queryDetails.error) {
     return (
       <p className="text-error text-center">
-        Failed to load listing details: {queryDetails.error.message}
+        Failed to load listing details:{' '}
+        {queryDetails.error instanceof Error
+          ? queryDetails.error.message
+          : String(queryDetails.error)}
       </p>
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (queryDetails.error) {
return (
<p className="text-error text-center">
Failed to load listing details: {queryDetails.error.message}
</p>
if (queryDetails.error) {
return (
<p className="text-error text-center">
Failed to load listing details:{' '}
{queryDetails.error instanceof Error
? queryDetails.error.message
: String(queryDetails.error)}
</p>
);
}
🤖 Prompt for AI Agents
In src/components/pages/Listings/Details/Details.tsx around lines 45 to 49, the
code directly accesses queryDetails.error.message which can throw if error is
unknown; update the render to safely read the message using optional chaining
and a fallback (e.g. queryDetails.error?.message ?? String(queryDetails.error)
?? 'Unknown error') so it never accesses .message on undefined and always
displays a safe string.

Comment on lines +27 to +32
onError: (error) => {
eventEmitter.emit('error', {
name: `Failed search for "${searchValue}"`,
message: error.message,
});
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

React Query v5: don't assume error.message

error is unknown. Accessing .message can throw. Coerce safely.

Apply:

     onError: (error) => {
-      eventEmitter.emit('error', {
-        name: `Failed search for "${searchValue}"`,
-        message: error.message,
-      });
+      const message =
+        error instanceof Error
+          ? error.message
+          : typeof error === 'string'
+            ? error
+            : 'Unknown error';
+      eventEmitter.emit('error', {
+        name: `Failed search for "${searchValue}"`,
+        message,
+      });
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onError: (error) => {
eventEmitter.emit('error', {
name: `Failed search for "${searchValue}"`,
message: error.message,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: 'Unknown error';
eventEmitter.emit('error', {
name: `Failed search for "${searchValue}"`,
message,
});
},
🤖 Prompt for AI Agents
In src/components/pages/Listings/SearchListingByName.tsx around lines 27 to 32,
the onError handler assumes error.message exists which can throw because React
Query v5 types error as unknown; change the handler to coerce the unknown error
to a safe string (e.g. use typeof error === 'object' && error !== null &&
'message' in error ? String((error as any).message) : String(error) or fallback
to JSON.stringify) and pass that safe string to eventEmitter.emit as the
message; keep the name field unchanged.

Comment on lines 80 to 83
onClick={() => {
navigate(`/register/${mutationSearch.variables}`);
}}
>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

URL-encode route param

Names can contain chars that break the URL. Encode before navigating.

-                  navigate(`/register/${mutationSearch.variables}`);
+                  navigate(`/register/${encodeURIComponent(String(mutationSearch.variables))}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick={() => {
navigate(`/register/${mutationSearch.variables}`);
}}
>
onClick={() => {
navigate(`/register/${encodeURIComponent(String(mutationSearch.variables))}`);
}}
🤖 Prompt for AI Agents
In src/components/pages/Listings/SearchListingByName.tsx around lines 80 to 83,
the code injects mutationSearch.variables directly into the route which can
break URLs for names with special characters; URL-encode the param before
calling navigate (use encodeURIComponent on the value), ensure the value is
converted to a string and guard against null/undefined (fallback or early
return) so you call
navigate(`/register/${encodeURIComponent(String(mutationSearch.variables))}`)
only with a safe encoded string.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
src/components/pages/Listings/Confirm.tsx (1)

23-34: Minor: destructure useSearchParams for clarity

Slightly clearer and avoids magic index access.

Apply:

-  const searchParams = useSearchParams();
+  const [params] = useSearchParams();
-  const name = searchParams[0].get('name') ?? '-';
-  const antProcessId = searchParams[0].get('antProcessId');
-  const price = searchParams[0].get('price');
-  const type = searchParams[0].get('type');
+  const name = params.get('name') ?? '-';
+  const antProcessId = params.get('antProcessId');
+  const price = params.get('price');
+  const type = params.get('type');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 034797e and 9ed1cc9.

📒 Files selected for processing (3)
  • src/App.tsx (2 hunks)
  • src/components/pages/Listings/Confirm.tsx (1 hunks)
  • src/components/pages/Listings/SearchListingByName.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/pages/Listings/SearchListingByName.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (1)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
src/components/pages/Listings/Confirm.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/utils/marketplace.ts (3)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
  • marketplaceQueryKeys (168-194)
src/App.tsx (2)
src/components/pages/index.ts (3)
  • Manage (13-13)
  • Home (11-11)
  • Listings (12-12)
src/utils/routes.tsx (1)
  • Route (28-36)
🔇 Additional comments (4)
src/components/pages/Listings/SearchListingByName.tsx (2)

80-84: Encode and type‑coerce route param when navigating to Register

Ensure a string is passed to encodeURIComponent.

Apply:

-                  navigate(
-                    `/register/${encodeURIComponent(mutationSearch.variables)}`,
-                  );
+                  navigate(
+                    `/register/${encodeURIComponent(String(mutationSearch.variables))}`,
+                  );

27-32: React Query v5: coerce unknown error before emitting

Accessing error.message can throw. Safely derive a string first.

Apply:

     onError: (error) => {
-      eventEmitter.emit('error', {
-        name: `Failed search for "${searchValue}"`,
-        message: error.message,
-      });
+      const message =
+        error instanceof Error
+          ? error.message
+          : typeof error === 'string'
+            ? error
+            : 'Unknown error';
+      eventEmitter.emit('error', {
+        name: `Failed search for "${searchValue}"`,
+        message,
+      });
     },

As per learnings

src/App.tsx (1)

351-410: LGTM: Listings and My ANTs routes wired correctly with Suspense

New lazy routes for listings, detail, confirm-purchase, and My ANTs look consistent with existing patterns.

src/components/pages/Listings/Confirm.tsx (1)

221-225: React Query v5: coerce unknown error before emitting

Don’t assume error.message; narrow first.

Apply:

-                    onError: (error) => {
-                      eventEmitter.emit('error', {
-                        name: `Failed to ${operation}`,
-                        message: error.message,
-                      });
-                    },
+                    onError: (error) => {
+                      const message =
+                        error instanceof Error
+                          ? error.message
+                          : typeof error === 'string'
+                            ? error
+                            : 'Unknown error';
+                      eventEmitter.emit('error', {
+                        name: `Failed to ${operation}`,
+                        message,
+                      });
+                    },

As per learnings

Copy link
Contributor

@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: 0

🧹 Nitpick comments (2)
src/utils/routes.tsx (2)

75-82: Consider consistent acronym capitalization.

The route text is 'My Ants', but ANT typically stands for Arweave Name Token and should be capitalized as 'My ANTs' for consistency with the acronym usage.

Apply this diff if you agree:

   myANTs: {
-    text: 'My Ants',
+    text: 'My ANTs',
     icon: WrappedAppWindowIcon,

83-90: Consider consistent naming convention.

The route key is listing (singular), but the text is 'Listings' (plural) and the path is '/listings' (plural). For consistency with other routes, consider renaming the key to listings.

Apply this diff if you agree:

-  listing: {
+  listings: {
     text: 'Listings',
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b709a7c and 17dec7e.

📒 Files selected for processing (2)
  • src/components/cards/NavMenuCard/NavMenuCard.tsx (1 hunks)
  • src/utils/routes.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/cards/NavMenuCard/NavMenuCard.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/utils/routes.tsx (1)
src/components/pages/index.ts (1)
  • Listings (12-12)
🔇 Additional comments (2)
src/utils/routes.tsx (2)

1-12: LGTM! Imports are correct and consistent.

The new imports for MyANTs, Listings, and the lucide-react icons (AppWindow, CalendarRange) are properly used throughout the file and follow the existing import structure.


29-39: LGTM! Icon wrappers follow the established pattern.

The new icon wrappers (WrappedCalendarRangeIcon, WrappedAppWindowIcon) are consistent with the existing wrapper implementations and properly typed. The comment on line 14 explains the mobile compatibility rationale.

Copy link
Contributor

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17dec7e and a0e4e16.

📒 Files selected for processing (1)
  • src/components/data-display/tables/DomainsTable.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/data-display/tables/DomainsTable.tsx (1)
src/utils/constants.ts (1)
  • MIN_ANT_VERSION (26-26)
🔇 Additional comments (2)
src/components/data-display/tables/DomainsTable.tsx (2)

44-44: LGTM! Import correctly includes BookCopy icon.

The import of BookCopy from lucide-react follows the recommended tree-shakable pattern and is properly used in the Sell action button below.


473-473: LGTM! Spacing adjustment improves action button layout.

The className adjustment uses standard Tailwind utilities to provide consistent spacing (gap-3) between action buttons and appropriate padding (pr-3).

Comment on lines 533 to 551
{row.getValue('role') === 'owner' && (
<Tooltip
message="Sell"
icon={
<button
disabled={row.original.version < MIN_ANT_VERSION}
onClick={() =>
navigate(
`/my-ants/new-listing/${row.original.processId.toString()}?name=${
row.original.name
}`,
)
}
>
<BookCopy className="w-[18px] text-grey" />
</button>
}
/>
)}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 1, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consider URL-encoding the name parameter in the query string.

The navigation constructs a URL with the domain name as a query parameter, but the name is not URL-encoded. If domain names contain special characters (spaces, ampersands, Unicode, etc.), this could result in malformed URLs.

Apply this diff to properly encode the name parameter:

                    <Tooltip
                      message="Sell"
                      icon={
                        <button
                          disabled={row.original.version < MIN_ANT_VERSION}
                          onClick={() =>
                            navigate(
-                             `/my-ants/new-listing/${row.original.processId.toString()}?name=${
-                               row.original.name
-                             }`,
+                             `/my-ants/new-listing/${row.original.processId}?name=${encodeURIComponent(
+                               row.original.name
+                             )}`,
                            )
                          }
                        >
                          <BookCopy className="w-[18px] text-grey" />
                        </button>
                      }
                    />

Note: The .toString() call on processId is also redundant since it's already a string, and has been removed in the suggestion above.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{row.getValue('role') === 'owner' && (
<Tooltip
message="Sell"
icon={
<button
disabled={row.original.version < MIN_ANT_VERSION}
onClick={() =>
navigate(
`/my-ants/new-listing/${row.original.processId.toString()}?name=${
row.original.name
}`,
)
}
>
<BookCopy className="w-[18px] text-grey" />
</button>
}
/>
)}
{row.getValue('role') === 'owner' && (
<Tooltip
message="Sell"
icon={
<button
disabled={row.original.version < MIN_ANT_VERSION}
onClick={() =>
navigate(
`/my-ants/new-listing/${row.original.processId}?name=${encodeURIComponent(
row.original.name
)}`,
)
}
>
<BookCopy className="w-[18px] text-grey" />
</button>
}
/>
)}
🤖 Prompt for AI Agents
In src/components/data-display/tables/DomainsTable.tsx around lines 533 to 551,
the navigation URL builds a query string with row.original.name without
URL-encoding which can break for spaces/special chars; update the navigate call
to URL-encode the name (e.g., use encodeURIComponent(row.original.name)) and
remove the redundant .toString() on processId if it is already a string so the
resulting URL is well-formed and safe.

Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of encoding URI component, we have a lowerCaseDomain utility for the names

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine the lowerCaseDomain utility implementation
ast-grep --pattern 'function lowerCaseDomain($$$) {
  $$$
}'

# Also search for export statements
ast-grep --pattern 'export $_ lowerCaseDomain = $$$'

# Fallback with rg to locate the function
rg -nP 'lowerCaseDomain\s*[=:]' --type=ts --type=tsx -A 10

Length of output: 281


🏁 Script executed:

#!/bin/bash
# Search for usage patterns of lowerCaseDomain in navigation/URL construction
rg -nP 'lowerCaseDomain.*navigate|navigate.*lowerCaseDomain' --type=ts -B 2 -A 2

# Also check for lowerCaseDomain usage in Link components or URL building
rg -nP 'lowerCaseDomain.*to=|to=.*lowerCaseDomain' --type=ts -B 2 -A 2

# Look for any query string construction with lowerCaseDomain
rg -nP 'lowerCaseDomain.*\?.*=' --type=ts -B 2 -A 2

Length of output: 1848


🏁 Script executed:

#!/bin/bash
# Search for query string construction with name or domain parameters
rg -nP '\?name=' --type=ts -B 1 -A 1

# Also search for URLSearchParams or query parameter encoding
rg -nP 'encodeURIComponent.*name|URLSearchParams' --type=ts -B 2 -A 2

Length of output: 4825


@atticusofsparta, you're absolutely right—using lowerCaseDomain maintains consistency with how domain names are handled throughout the codebase. I can see it's used for path normalization in navigation elsewhere.

For the minimal fix respecting your approach, you could apply:

navigate(
  `/my-ants/new-listing/${row.original.processId}?name=${lowerCaseDomain(
    row.original.name
  )}`,
)

However, I noticed that newer components in this PR (like FixedListingPriceSection.tsx and DutchListingPriceSection.tsx) use URLSearchParams for building query strings, which automatically handles URL encoding:

const params = new URLSearchParams({
  name: lowerCaseDomain(row.original.name),
  // ... other params
});
navigate(`/my-ants/new-listing/${row.original.processId}?${params.toString()}`);

This pattern would be more robust for edge cases while still using lowerCaseDomain for normalization. Either way works, but the URLSearchParams approach aligns with the pattern established in the Listings feature components.

onClick={() =>
navigate(
`/my-ants/new-listing/${row.original.processId.toString()}?name=${
row.original.name
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a util for parsing url safe domains, good to use here.

Suggested change
row.original.name
lowerCaseDomain(row.original.name)

const [{ walletAddress }] = useWalletState();
const queryDetails = useQuery({
enabled: !!id,
refetchInterval: 15 * 1000,
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't think we need to refetch this every 15 seconds - keep in mind, CU dryruns are slow and expensive, and theres a rate limit.

Whats the need for the refetch interval here?

Choose a reason for hiding this comment

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

For example, this is important for auctions that display bids, because users who are bidding simultaneously need to see those updates. If it’s not something that’s essential, then it can be removed.

Comment on lines +67 to +75
onSuccess: async (data) => {
console.log(`cancel success`, { data });
void Promise.allSettled([
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.listings.all],
}),
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.myANTs.all],
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Good pattern - what I would recommend adding to is, is setQueryData.

If the action returns the message result with the patched process state, it can be used to update the client query state, instead of having to wait for the refetch to update the data.

Choose a reason for hiding this comment

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

In theory, yes, but in this case the mutation returns raw data from the API, whose typing is significantly different from the data returned by the refreshed queries. Using setQueryData here would require completely remapping the data into different object structures and is not recommended.

const pagination = useCursorPagination(PAGE_SIZE);

const queryActiveListings = useQuery({
refetchInterval: 15 * 1000,
Copy link
Contributor

@atticusofsparta atticusofsparta Oct 2, 2025

Choose a reason for hiding this comment

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

Another question of if we need this refetch interval - its a costly query. Especially since this is a paginated query.

Choose a reason for hiding this comment

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

If we want to show users newly added listings in real time, then this interval is necessary. If it’s not that important for us, we can skip it here.


const queryActiveListings = useQuery({
refetchInterval: 15 * 1000,
structuralSharing: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

you are using isPending below - i don't know much about this param I'll admit but i think we do want this.

Choose a reason for hiding this comment

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

Comment on lines +28 to +30
queryKey: marketplaceQueryKeys.listings.list('active', {
page: pagination.page,
pageSize: pagination.pageSize,
Copy link
Contributor

Choose a reason for hiding this comment

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

should probably have cursor in here as well, since the page number and size may be the same with a different cursor.

Comment on lines 25 to 39
const queryCompletedListings = useQuery({
refetchInterval: 15 * 1000,
enabled: Boolean(aoClient),
queryKey: marketplaceQueryKeys.listings.list('completed', {
page: pagination.page,
pageSize: pagination.pageSize,
}),
queryFn: () => {
return fetchCompletedListings({
ao: aoClient,
activityProcessId: BLOCKYDEVS_ACTIVITY_PROCESS_ID,
limit: pagination.pageSize,
cursor: pagination.cursor,
});
},
Copy link
Contributor

Choose a reason for hiding this comment

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

refetch question on this one

  • query key should probably have cursor

const price = searchParams[0].get('price');
const type = searchParams[0].get('type');

const mutationBuyListing = useMutation({
Copy link
Contributor

@atticusofsparta atticusofsparta Oct 2, 2025

Choose a reason for hiding this comment

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

For organization and reusability, consider use[my-mutation] hooks

Comment on lines +227 to +237
onSuccess: async (data) => {
console.log(`${operation} success`, { data });
void Promise.allSettled([
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.listings.all],
}),
queryClient.refetchQueries({
queryKey: [marketplaceQueryKeys.myANTs.all],
}),
]);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

see comment regarding reusable mutations - this can be defined on the use[my-mutation] hook and then you just call mutate(params) i believe

const mutationSearch = useMutation({
mutationFn: async (name: string) => {
return searchANT({
name,
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to encode this to ASCII + puny code for searches. Dunno if the data lib does this internally (good idea to actually if it doesn't already). Likewise when rendering the names we should be encoding to unicode.

Suggested change
name,
name: lowerCaseDomain(name),

const queryMyANTs = useQuery({
enabled: !!walletAddress,
refetchInterval: 15 * 1000,
queryKey: marketplaceQueryKeys.myANTs.list(walletAddress?.toString()),
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: ario process id should be part of this key - most for dev tooling and supporting our testnet.

.... but there is no "testnet" marketplace process (yet) so not wholely necessary. I believe @dtfiedler mentioned being able to pass an ARIO process id on interactions with the process - this would aid in supporting that.

Copy link
Contributor

@atticusofsparta atticusofsparta left a comment

Choose a reason for hiding this comment

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

general comment - the font seems to have changed in some places (like the profile) and the links are no longer the right color.

I think the marketplace-ui package is leaking tailwind configurations.

import DevTools from './components/pages/Settings/devtools/DevTools';
import useSyncSettings from './hooks/useSyncSettings/useSyncSettings';
import useWanderEvents from './hooks/useWanderEvents/useWanderEvents';
import './index.css';
Copy link
Contributor

Choose a reason for hiding this comment

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

might be why some styling is off

Choose a reason for hiding this comment

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

import '@src/index.css'; it's moved to main.tsx and should override the package styles

are you able to provide examples with screenshots of things that don’t look the way they should?

Copy link
Contributor

@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: 3

♻️ Duplicate comments (1)
src/components/pages/Listings/ActiveListingsTab.tsx (1)

25-40: Critical: Add enabled flag to prevent query execution before aoClient is ready.

The past review comment about this issue is still unresolved. Without an enabled flag, the query will execute immediately even if aoClient is undefined, causing failures when fetchActiveListings attempts to use it.

Apply this diff to add the enabled flag:

   const queryActiveListings = useQuery({
     refetchInterval: 15 * 1000,
     structuralSharing: false,
+    enabled: Boolean(aoClient),
     queryKey: marketplaceQueryKeys.listings.list('active', {
       page: pagination.page,
       pageSize: pagination.pageSize,
       cursor: pagination.cursor,
     }),
🧹 Nitpick comments (2)
src/components/data-display/tables/DomainsTable.tsx (1)

533-551: Consider adding a tooltip message for the disabled state.

The Sell button correctly uses lowerCaseDomain for the name parameter and properly restricts access to owners. However, when the button is disabled (row.original.version < MIN_ANT_VERSION), the tooltip always shows "Sell" without explaining why the action is unavailable.

For better UX, consider following the pattern used by the Star button (lines 476-482), which provides a conditional tooltip message explaining the disabled state.

Apply this diff to add a conditional tooltip message:

                  {row.getValue('role') === 'owner' && (
                    <Tooltip
-                     message="Sell"
+                     message={
+                       row.original.version < MIN_ANT_VERSION
+                         ? 'Update ANT to access Marketplace listing workflow'
+                         : 'Sell'
+                     }
                      icon={
                        <button
                          disabled={row.original.version < MIN_ANT_VERSION}
                          onClick={() =>
                            navigate(
                              `/my-ants/new-listing/${
                                row.original.processId
                              }?name=${lowerCaseDomain(row.original.name)}`,
                            )
                          }
                        >
                          <BookCopy className="w-[18px] text-grey" />
                        </button>
                      }
                    />
                  )}
src/components/pages/Listings/CompletedListingsTab.tsx (1)

55-55: Consider explicit type mapping.

The implicit mapping (only 'english' → 'bid', everything else → 'buyout') may be fragile if new listing types are added. Consider explicit handling for known types like 'dutch' or add a comment documenting the assumption.

            price: {
-             type: item.type === 'english' ? 'bid' : 'buyout',
+             type: item.type === 'english' ? 'bid' : 'buyout', // dutch and fixed-price are buyout
              symbol: 'ARIO',
              value: Number(currentPrice),
            },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0e4e16 and 96b9fb6.

📒 Files selected for processing (4)
  • src/components/data-display/tables/DomainsTable.tsx (3 hunks)
  • src/components/pages/Listings/ActiveListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/CompletedListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/SearchListingByName.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/pages/Listings/SearchListingByName.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/pages/Listings/CompletedListingsTab.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (3)
  • marketplaceQueryKeys (168-194)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • getCurrentListingArioPrice (127-151)
src/components/data-display/tables/DomainsTable.tsx (2)
src/utils/constants.ts (1)
  • MIN_ANT_VERSION (26-26)
src/utils/searchUtils/searchUtils.ts (1)
  • lowerCaseDomain (132-134)
src/components/pages/Listings/ActiveListingsTab.tsx (2)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (3)
  • marketplaceQueryKeys (168-194)
  • BLOCKYDEVS_ACTIVITY_PROCESS_ID (161-162)
  • getCurrentListingArioPrice (127-151)
🔇 Additional comments (7)
src/components/data-display/tables/DomainsTable.tsx (2)

44-44: LGTM!

The BookCopy icon import is correctly added to support the new Sell button feature.


473-473: LGTM!

The spacing adjustment from 18px to 12px (gap-3) is appropriate, especially with the addition of a new action button in the same row.

src/components/pages/Listings/CompletedListingsTab.tsx (5)

1-16: LGTM!

Imports are well-organized and all dependencies appear to be used within the component.


20-23: LGTM!

Component initialization and hook setup follow React best practices.


25-40: LGTM!

Query configuration correctly addresses previous review feedback:

  • enabled flag prevents queries with undefined client
  • cursor is included in the query key for proper cache invalidation

The 15-second refetch interval provides near-real-time updates for completed listings.


74-89: LGTM!

Render logic properly handles loading and error states, and correctly uses React Query v5's isPending status. The conditional pagination rendering prevents layout shifts during loading.


57-57: Ensure safe conversion of ARIO price string to Number
Parsing currentPrice (returned as a string by getCurrentListingArioPrice) with Number(...) can lose precision for very large or highly fractional ARIO values. Confirm that ARIO prices always stay within JavaScript’s safe-integer range (±2^53−1) and required decimal precision, or switch to a BigNumber/decimal library if necessary.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (1)
src/components/pages/Listings/Confirm.tsx (1)

152-156: Unsafe access to error.message without type guard.

React Query v5 types the error parameter as unknown. Directly accessing error.message will throw a TypeScript error and can fail at runtime if the error is not an Error instance.

As flagged in past review comments, apply type narrowing:

                     onError: (error) => {
+                      const message =
+                        error instanceof Error
+                          ? error.message
+                          : typeof error === 'string'
+                            ? error
+                            : 'Unknown error';
                       eventEmitter.emit('error', {
                         name: `Failed to ${operation}`,
-                        message: error.message,
+                        message,
                       });
                     },
🧹 Nitpick comments (3)
src/hooks/listings/useBidListing.tsx (1)

18-44: Consider extracting shared validation logic.

Both useBidListing and useBuyListing duplicate the wallet/signer/ID validation checks (lines 19-33 here, lines 20-34 in useBuyListing). This creates maintenance overhead and drift risk.

Consider extracting to a shared validator:

// src/hooks/listings/validateListingMutation.ts
export function validateListingMutation(
  wallet: unknown,
  walletAddress: unknown,
  antProcessId: string | null,
  listingId: string | undefined,
) {
  if (!wallet || !walletAddress) {
    throw new Error('No wallet connected');
  }
  if (!wallet.contractSigner) {
    throw new Error('No wallet signer available');
  }
  if (!antProcessId) {
    throw new Error('antProcessId is missing');
  }
  if (!listingId) {
    throw new Error('listingId is missing');
  }
}

Then both hooks can call this validator before their mutation-specific logic.

src/hooks/listings/useBuyListing.tsx (1)

36-38: Type validation uses loose string comparison.

The type parameter is string | null, but the validation at runtime only checks for specific string literals. Consider using a discriminated union type or enum for stronger type safety.

+type ListingType = 'fixed' | 'dutch';
+
 export const useBuyListing = (
   antProcessId: string | null,
   listingId: string | undefined,
-  type: string | null,
+  type: ListingType | null,
 ) => {

This would catch type mismatches at compile time rather than runtime.

src/components/pages/Listings/Confirm.tsx (1)

158-168: Consider invalidating queries instead of refetching.

Using refetchQueries forces an immediate network request. invalidateQueries marks queries as stale and refetches them only when they're actively being observed, which is more efficient.

                     onSuccess: async (data) => {
                       console.log(`${operation} success`, { data });
                       void Promise.allSettled([
-                        queryClient.refetchQueries({
+                        queryClient.invalidateQueries({
                           queryKey: [marketplaceQueryKeys.listings.all],
                         }),
-                        queryClient.refetchQueries({
+                        queryClient.invalidateQueries({
                           queryKey: [marketplaceQueryKeys.myANTs.all],
                         }),
                       ]);
                     },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96b9fb6 and f4c918c.

📒 Files selected for processing (3)
  • src/components/pages/Listings/Confirm.tsx (1 hunks)
  • src/hooks/listings/useBidListing.tsx (1 hunks)
  • src/hooks/listings/useBuyListing.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/hooks/listings/useBidListing.tsx (3)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (2)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
src/hooks/listings/useBuyListing.tsx (3)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (2)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (163-164)
  • BLOCKYDEVS_SWAP_TOKEN_ID (165-166)
src/components/pages/Listings/Confirm.tsx (4)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/hooks/listings/useBuyListing.tsx (1)
  • useBuyListing (10-53)
src/hooks/listings/useBidListing.tsx (1)
  • useBidListing (10-47)
src/utils/marketplace.ts (1)
  • marketplaceQueryKeys (168-194)

Copy link
Contributor

@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: 9

♻️ Duplicate comments (7)
src/components/pages/Listings/SearchListingByName.tsx (2)

82-85: Defensive: coerce to string and guard before navigate

variables can be undefined before first mutate; also coerce to string for consistency.

-                  navigate(
-                    `/register/${encodeURIComponent(mutationSearch.variables)}`,
-                  );
+                  const v = mutationSearch.variables;
+                  if (!v) return;
+                  navigate(`/register/${encodeURIComponent(String(v))}`);

28-33: React Query v5: don't assume error.message

error is unknown; accessing .message can throw. Coerce safely.

-    onError: (error) => {
-      eventEmitter.emit('error', {
-        name: `Failed search for "${searchValue}"`,
-        message: error.message,
-      });
-    },
+    onError: (error) => {
+      const message =
+        error instanceof Error
+          ? error.message
+          : typeof error === 'string'
+            ? error
+            : 'Unknown error';
+      eventEmitter.emit('error', {
+        name: `Failed search for "${searchValue}"`,
+        message,
+      });
+    },
src/components/pages/Listings/Details/Details.tsx (2)

24-37: Enable only when both id and aoClient exist. Also guard in queryFn.

-  const queryDetails = useQuery({
-    enabled: !!id,
+  const queryDetails = useQuery({
+    enabled: Boolean(id && aoClient),
     refetchInterval: 15 * 1000,
     queryKey: marketplaceQueryKeys.listings.item(id),
     queryFn: () => {
-      if (!id) throw new Error('guard: no id provided');
+      if (!id) throw new Error('guard: no id provided');
+      if (!aoClient) throw new Error('guard: no aoClient provided');
 
       return fetchListingDetails({
         ao: aoClient,
         marketplaceProcessId: BLOCKYDEVS_MARKETPLACE_PROCESS_ID,
         orderId: id,
       });
     },
   });

47-51: Safely render unknown errors.

-        Failed to load listing details: {queryDetails.error.message}
+        Failed to load listing details:{' '}
+        {queryDetails.error instanceof Error
+          ? queryDetails.error.message
+          : String(queryDetails.error)}
src/utils/marketplace.ts (3)

144-150: Optional hardening: set opener = null as a fallback.

You already pass noopener,noreferrer. For belt‑and‑suspenders, also null the opener if a window is returned.

-export const openAoLinkExplorer = (address: string) => {
-  window.open(
+export const openAoLinkExplorer = (address: string) => {
+  const w = window.open(
     `${AO_LINK_EXPLORER_URL}/${address}`,
     '_blank',
     'noopener,noreferrer',
   );
+  if (w) w.opener = null;
 };

42-47: Coerce invalid time parts to 0 to prevent Invalid Date.

Parsing non‑numeric or partial times yields NaN and breaks Date.

-  const [hours, minutes, seconds = 0] = time.split(':').map(Number);
+  const [rawH, rawM, rawS = '0'] = time.split(':');
+  const hours = Number(rawH);
+  const minutes = Number(rawM);
+  const seconds = Number(rawS);
+  const safeH = Number.isFinite(hours) ? hours : 0;
+  const safeM = Number.isFinite(minutes) ? minutes : 0;
+  const safeS = Number.isFinite(seconds) ? seconds : 0;
   const merged = new Date(date);
 
-  merged.setHours(hours);
-  merged.setMinutes(minutes);
-  merged.setSeconds(seconds);
+  merged.setHours(safeH);
+  merged.setMinutes(safeM);
+  merged.setSeconds(safeS);

118-142: Settled listings must return finalPrice first; also handle nulls.

Reorder branches to prioritize settled and guard against null/undefined prices.

 export const getCurrentListingArioPrice = (listing: ListingDetails) => {
   const marioPrice = (() => {
-    if (listing.type === 'english') {
-      return listing.highestBid ?? listing.startingPrice;
-    }
-
-    if (listing.type === 'dutch' && listing.status !== 'settled') {
+    if (listing.status === 'settled') {
+      return listing.finalPrice;
+    }
+
+    if (listing.type === 'english') {
+      return listing.highestBid ?? listing.startingPrice;
+    }
+
+    if (listing.type === 'dutch') {
       return calculateCurrentDutchListingPrice({
         startingPrice: listing.startingPrice,
         minimumPrice: listing.minimumPrice,
         decreaseInterval: listing.decreaseInterval,
         decreaseStep: listing.decreaseStep,
         createdAt: new Date(listing.createdAt).getTime(),
       });
     }
-
-    if (listing.status === 'settled') {
-      return listing.finalPrice;
-    }
-
     return listing.price;
   })();
 
-  return marioToArio(marioPrice);
+  if (marioPrice == null) return 0;
+  return marioToArio(marioPrice);
 };
🧹 Nitpick comments (13)
src/hooks/listings/useBuyListing.tsx (1)

7-11: Constrain orderType and add invariants for safer calls

  • Make the param compile‑time safe ('fixed' | 'dutch'); keep runtime guard.
  • Ensure antAoClient and arioProcessId exist before calling buyListing.
  • Narrow orderType for TS.

Apply:

-export const useBuyListing = (
-  antProcessId: string | null,
-  listingId: string | undefined,
-  type: string | null,
-) => {
+export const useBuyListing = (
+  antProcessId: string | null,
+  listingId: string | undefined,
+  type: 'fixed' | 'dutch' | null,
+) => {
       if (!listingId) {
         throw new Error('listingId is missing');
       }
 
+      if (!antAoClient) {
+        throw new Error('AO client not initialized');
+      }
+
+      if (!arioProcessId) {
+        throw new Error('arioProcessId is missing');
+      }
-        orderType: type,
+        orderType: type as 'fixed' | 'dutch',

If changing the param type is disruptive, keep current signature and only apply the invariant checks and the narrowing cast on orderType.

Also applies to: 33-36, 46-46

src/hooks/listings/usePrepareListings.ts (1)

39-41: URL‑encode route param

orderId may contain chars that break the URL. Encode before navigating.

-          navigate(`/listings/${item.orderId}`);
+          navigate(`/listings/${encodeURIComponent(item.orderId)}`);
src/components/pages/Listings/Details/DutchListingPriceSection.tsx (1)

52-59: Prevent accidental form submission

Set button type explicitly to avoid submitting if nested in a form.

-        <Button
+        <Button
+          type="button"
           variant="primary"
           className="w-full"
           disabled={!walletAddress || !antMeta}
           onClick={() => {
             navigateToConfirmPurchase();
           }}
src/components/pages/Listings/SearchListingByName.tsx (1)

64-76: Display domain in Unicode (IDN) for better UX

Search uses ASCII/punycode via lowerCaseDomain, but the displayed domain should be in Unicode form. Use the existing decodeDomainToASCII() utility from searchUtils when rendering mutationSearch.variables in both places (lines 67 and 75).

src/components/pages/Listings/CompletedListingsTab.tsx (3)

48-53: Stabilize effect dependencies to avoid redundant executions.

Depending on a method reference can retrigger on each render. Key the effect to the actual values and include the pagination object once.

-  useEffect(() => {
-    if (!queryCompletedListings.data) return;
-
-    const { nextCursor, hasMore } = queryCompletedListings.data;
-    pagination.storeNextCursor(nextCursor, !!hasMore);
-  }, [queryCompletedListings.data, pagination.storeNextCursor]);
+  useEffect(() => {
+    const nextCursor = queryCompletedListings.data?.nextCursor;
+    const hasMore = queryCompletedListings.data?.hasMore;
+    if (nextCursor === undefined || hasMore === undefined) return;
+    pagination.storeNextCursor(nextCursor, !!hasMore);
+  }, [pagination, queryCompletedListings.data?.nextCursor, queryCompletedListings.data?.hasMore]);

15-16: Reduce pagination flicker by keeping previous data during cursor changes.

Leverage React Query’s keepPreviousData so the table doesn’t clear while the next page loads.

-import { useQuery } from '@tanstack/react-query';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
@@
   const queryCompletedListings = useQuery({
     refetchInterval: 15 * 1000,
     enabled: Boolean(aoClient),
+    placeholderData: keepPreviousData,
     queryKey: marketplaceQueryKeys.listings.list('completed', {
       page: pagination.page,
       pageSize: pagination.pageSize,
       cursor: pagination.cursor,
     }),

Also applies to: 24-41, 55-61


57-61: Safely stringify unknown error objects.

error may not be an Error. Provide a robust string.

-        error={queryCompletedListings.error?.message}
+        error={
+          queryCompletedListings.error
+            ? (queryCompletedListings.error instanceof Error
+                ? queryCompletedListings.error.message
+                : String(queryCompletedListings.error))
+            : undefined
+        }
src/hooks/listings/useBidListing.tsx (1)

1-6: Invalidate listing/bids queries on success to refresh UI.

Ensure details and bids reflect the new bid immediately.

-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
@@
   const [{ wallet, walletAddress }] = useWalletState();
   const [{ aoClient, arioProcessId }] = useGlobalState();
+  const queryClient = useQueryClient();
@@
-  return useMutation({
+  return useMutation({
     mutationFn: async ({ price }: { price: string }) => {
@@
       });
     },
+    onSuccess: async () => {
+      if (listingId) {
+        await queryClient.invalidateQueries({
+          queryKey: ['listings', 'details', listingId],
+        });
+      }
+      // If bids have their own query key, invalidate it too
+      await queryClient.invalidateQueries({
+        predicate: (q) =>
+          Array.isArray(q.queryKey) &&
+          q.queryKey[0] === 'listings' &&
+          q.queryKey.includes('bids') &&
+          q.queryKey.includes(listingId ?? ''),
+      });
+    },
   });

Also applies to: 14-44

src/hooks/listings/useAntsMetadata.ts (1)

20-33: Harden storage access for SSR and fix log message.

Avoid ReferenceError when localStorage is unavailable, and clarify warning text.

 const readStorage = (): FetchANTsMetadataResult | null => {
   try {
-    const stored = localStorage.getItem(
+    if (typeof window === 'undefined' || !window.localStorage) return null;
+    const stored = window.localStorage.getItem(
       BLOCKYDEVS_MARKETPLACE_METADATA_STORAGE_KEY,
     );
@@
 const updateStorage = (data: FetchANTsMetadataResult) => {
   try {
-    localStorage.setItem(
+    if (typeof window === 'undefined' || !window.localStorage) return;
+    window.localStorage.setItem(
       BLOCKYDEVS_MARKETPLACE_METADATA_STORAGE_KEY,
       JSON.stringify({ timestamp: Date.now(), data }),
     );
   } catch (error) {
-    console.warn('Failed to marketplace metadata:', error);
+    console.warn('Failed to write marketplace metadata:', error);
   }
 };

Also applies to: 35-44

src/components/pages/Listings/Details/Details.tsx (1)

24-27: Make refetch interval conditional to reduce cost and load.

Only poll when it matters (e.g., active English auctions).

-    refetchInterval: 15 * 1000,
+    refetchInterval: (data) =>
+      data && data.type === 'english' && data.status !== 'settled'
+        ? 15_000
+        : false,
src/components/pages/Listings/Details/ListingMetadata.tsx (3)

28-43: Memoize schedule to avoid recomputation.

Schedule is pure and can be heavy; memoize by listing fields.

+import { useEffect, useMemo } from 'react';
@@
-  const dutchPriceSchedule: Schedule[] =
-    listing.type === 'dutch'
-      ? getDutchListingSchedule({
+  const dutchPriceSchedule: Schedule[] = useMemo(() => {
+    if (listing.type !== 'dutch') return [];
+    return getDutchListingSchedule({
           startingPrice: listing.startingPrice,
           minimumPrice: listing.minimumPrice,
           decreaseInterval: listing.decreaseInterval,
           decreaseStep: listing.decreaseStep,
           createdAt: new Date(listing.createdAt).getTime(),
           endedAt: new Date(
-            ('endedAt' in listing && listing.endedAt)
-              ? listing.endedAt
-              : listing.expiresAt
+            ('endedAt' in listing && listing.endedAt) ? listing.endedAt : listing.expiresAt
           ).getTime(),
-        }).map((item) => ({
-          date: formatDate(item.date),
-          price: Number(marioToArio(item.price)),
-        }))
-      : [];
+        }).map((item) => ({
+          date: formatDate(item.date),
+          price: Number(marioToArio(item.price)),
+        }));
+  }, [
+    listing.type,
+    listing.startingPrice,
+    listing.minimumPrice,
+    listing.decreaseInterval,
+    listing.decreaseStep,
+    listing.createdAt,
+    // @ts-expect-error: `endedAt` present on some listing variants
+    (('endedAt' in listing) ? listing.endedAt : undefined),
+    listing.expiresAt,
+  ]);

71-99: Improve mobile layout for metadata grid.

Force‑two columns can squish content on small screens.

-        <div className="grid grid-cols-2 gap-4">
+        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">

51-53: Optional: show a skeleton instead of null.

Rendering null creates a blank section; consider a lightweight placeholder for perceived performance.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4c918c and 1baa4d6.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (18)
  • package.json (3 hunks)
  • src/App.tsx (4 hunks)
  • src/components/pages/Listings/ActiveListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/CompletedListingsTab.tsx (1 hunks)
  • src/components/pages/Listings/Details/Details.tsx (1 hunks)
  • src/components/pages/Listings/Details/DutchListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/EnglishListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/FixedListingPriceSection.tsx (1 hunks)
  • src/components/pages/Listings/Details/ListingMetadata.tsx (1 hunks)
  • src/components/pages/Listings/SearchListingByName.tsx (1 hunks)
  • src/components/pages/MyANTs/MyANTs.tsx (1 hunks)
  • src/components/pages/MyANTs/NewListing.tsx (1 hunks)
  • src/hooks/listings/useAntsMetadata.ts (1 hunks)
  • src/hooks/listings/useBidListing.tsx (1 hunks)
  • src/hooks/listings/useBuyListing.tsx (1 hunks)
  • src/hooks/listings/usePrefetchMarketplaceData.ts (1 hunks)
  • src/hooks/listings/usePrepareListings.ts (1 hunks)
  • src/utils/marketplace.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/pages/MyANTs/NewListing.tsx
  • package.json
  • src/components/pages/Listings/Details/EnglishListingPriceSection.tsx
  • src/components/pages/Listings/ActiveListingsTab.tsx
  • src/components/pages/MyANTs/MyANTs.tsx
🧰 Additional context used
🧬 Code graph analysis (12)
src/components/pages/Listings/CompletedListingsTab.tsx (4)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/hooks/listings/useAntsMetadata.ts (1)
  • useAntsMetadata (75-107)
src/utils/marketplace.ts (2)
  • marketplaceQueryKeys (157-186)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (154-155)
src/hooks/listings/usePrepareListings.ts (1)
  • usePrepareListings (11-62)
src/hooks/listings/useBidListing.tsx (3)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (1)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (154-155)
src/hooks/listings/usePrefetchMarketplaceData.ts (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/network.ts (1)
  • queryClient (39-45)
src/hooks/listings/useAntsMetadata.ts (1)
  • antsMetadataQueryOptions (46-73)
src/components/pages/Listings/Details/Details.tsx (4)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/hooks/listings/useAntsMetadata.ts (1)
  • useAntsMetadata (75-107)
src/utils/marketplace.ts (4)
  • marketplaceQueryKeys (157-186)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (154-155)
  • getCurrentListingArioPrice (118-142)
  • getStatusVariantFromListing (102-116)
src/components/pages/Listings/Details/ListingMetadata.tsx (3)
src/hooks/listings/useAntsMetadata.ts (1)
  • useAntsMetadata (75-107)
src/utils/common/common.tsx (1)
  • formatDate (12-14)
src/utils/marketplace.ts (1)
  • openAoLinkExplorer (144-150)
src/components/pages/Listings/Details/DutchListingPriceSection.tsx (3)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/hooks/listings/useAntsMetadata.ts (1)
  • useAntsMetadata (75-107)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (118-142)
src/hooks/listings/useAntsMetadata.ts (2)
src/utils/marketplace.ts (3)
  • BLOCKYDEVS_MARKETPLACE_METADATA_STORAGE_KEY (152-153)
  • marketplaceQueryKeys (157-186)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (154-155)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/components/pages/Listings/Details/FixedListingPriceSection.tsx (3)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/hooks/listings/useAntsMetadata.ts (1)
  • useAntsMetadata (75-107)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (118-142)
src/components/pages/Listings/SearchListingByName.tsx (3)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/searchUtils/searchUtils.ts (1)
  • lowerCaseDomain (132-134)
src/utils/marketplace.ts (1)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (154-155)
src/App.tsx (2)
src/components/pages/index.ts (3)
  • Manage (13-13)
  • Home (11-11)
  • Listings (12-12)
src/hooks/listings/usePrefetchMarketplaceData.ts (1)
  • usePrefetchMarketplaceData (6-26)
src/hooks/listings/useBuyListing.tsx (3)
src/state/contexts/WalletState.tsx (1)
  • useWalletState (49-50)
src/state/contexts/GlobalState.tsx (1)
  • useGlobalState (143-144)
src/utils/marketplace.ts (1)
  • BLOCKYDEVS_MARKETPLACE_PROCESS_ID (154-155)
src/hooks/listings/usePrepareListings.ts (2)
src/hooks/listings/useAntsMetadata.ts (1)
  • useAntsMetadata (75-107)
src/utils/marketplace.ts (1)
  • getCurrentListingArioPrice (118-142)
🔇 Additional comments (4)
src/hooks/listings/usePrefetchMarketplaceData.ts (1)

21-25: LGTM: prefetch gated on existing storage

Memoized queryOptions and the initialData gate avoid redundant fetches. Nice.

src/App.tsx (1)

353-412: Listings routes integration looks good

Lazy routes + Suspense + PageLoader are consistent with existing patterns.

Please confirm /listings/:id/confirm-purchase consumers expect hash routing (createHashRouter) so deep links work behind static hosting.

src/components/pages/Listings/Details/ListingMetadata.tsx (2)

41-42: Verify return type of marioToArio and precision handling.

The concern about precision loss is reasonable: inconsistent handling across the codebase suggests this is known risk (see PriceScheduleModal.tsx using .toFixed(6) vs. ListingMetadata.tsx not using it). Confirm whether marioToArio returns a string or high-precision number, and whether Number() alone at line 41 causes unacceptable rounding compared to the .toFixed(6) pattern used elsewhere.


63-66: Verify the unit of leaseEndsAt from @blockydevs/arns-marketplace-data.

The concern is valid: formatDate expects milliseconds, but leaseEndsAt is passed directly without any conversion. In the same file, other dates use .getTime() to convert to milliseconds before calling formatDate (lines 35–38). Confirm whether leaseEndsAt is already in milliseconds or requires conversion to avoid displaying incorrect dates.

Comment on lines +43 to +47
useEffect(() => {
if (antMeta) return;

queryAntsMetadata.refetch();
}, [antMeta, queryAntsMetadata]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent endless metadata refetch loops when metadata is absent.

This effect will refetch on every render until metadata appears. Dedup only prevents concurrent calls, not repeated cycles.

-  useEffect(() => {
-    if (antMeta) return;
-
-    queryAntsMetadata.refetch();
-  }, [antMeta, queryAntsMetadata]);
+  useEffect(() => {
+    // Refetch at most once per mount if metadata is missing
+    let attempted = false;
+    if (!antMeta && !attempted) {
+      attempted = true;
+      void queryAntsMetadata.refetch();
+    }
+  }, [antMeta, queryAntsMetadata.refetch]);

Alternatively, track attempts per listing ID with a ref to ensure a single retry per antProcessId.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (antMeta) return;
queryAntsMetadata.refetch();
}, [antMeta, queryAntsMetadata]);
const attemptedRef = useRef(false);
useEffect(() => {
if (antMeta || attemptedRef.current) return;
attemptedRef.current = true;
void queryAntsMetadata.refetch();
}, [antMeta, queryAntsMetadata]);

Comment on lines +35 to +38
createdAt: new Date(listing.createdAt).getTime(),
endedAt: new Date(
'endedAt' in listing ? listing.endedAt : listing.expiresAt,
).getTime(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard endedAt fallback to avoid NaN timestamps.

'endedAt' in listing is true even when endedAt is undefined/null. Prefer nullish coalescing with a presence check.

-          endedAt: new Date(
-            'endedAt' in listing ? listing.endedAt : listing.expiresAt,
-          ).getTime(),
+          endedAt: new Date(
+            ('endedAt' in listing && listing.endedAt)
+              ? listing.endedAt
+              : listing.expiresAt
+          ).getTime(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
createdAt: new Date(listing.createdAt).getTime(),
endedAt: new Date(
'endedAt' in listing ? listing.endedAt : listing.expiresAt,
).getTime(),
createdAt: new Date(listing.createdAt).getTime(),
endedAt: new Date(
('endedAt' in listing && listing.endedAt)
? listing.endedAt
: listing.expiresAt
).getTime(),
🤖 Prompt for AI Agents
In src/components/pages/Listings/Details/ListingMetadata.tsx around lines 35–38,
replace the "'endedAt' in listing ? listing.endedAt : listing.expiresAt" check
with a nullish-coalescing presence check and a defensive fallback to avoid NaN:
compute a value like const ended = listing.endedAt ?? listing.expiresAt ??
listing.createdAt and pass ended into new Date(ended).getTime(); this ensures
undefined/null endedAt won’t produce NaN and always falls back to a valid
timestamp.

Comment on lines +45 to +49
useEffect(() => {
if (!antMeta) {
queryAntsMetadata.refetch();
}
}, [antMeta, queryAntsMetadata]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid refetch loops: depend on stable refs only.

queryAntsMetadata is a fresh object each render; the effect can refire and spam refetch. Destructure refetch and depend on [antMeta, refetch].

Apply:

-  const queryAntsMetadata = useAntsMetadata();
-
-  const antMeta = queryAntsMetadata.data?.[listing.antProcessId];
+  const { data: antsMeta, refetch } = useAntsMetadata();
+
+  const antMeta = antsMeta?.[listing.antProcessId];
@@
-  useEffect(() => {
-    if (!antMeta) {
-      queryAntsMetadata.refetch();
-    }
-  }, [antMeta, queryAntsMetadata]);
+  useEffect(() => {
+    if (!antMeta) {
+      void refetch();
+    }
+  }, [antMeta, refetch]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/pages/Listings/Details/ListingMetadata.tsx around lines 45 to
49, the useEffect depends on the whole queryAntsMetadata object which is
re-created each render causing refetch loops; destructure the refetch function
(e.g. const { refetch } = queryAntsMetadata) and change the effect to depend on
[antMeta, refetch] so only the stable refetch reference is watched and refetch
is only called when antMeta is missing and the refetch function changes.

Comment on lines +53 to +72
return queryOptions({
queryKey: [marketplaceQueryKeys.metadata.all],
initialData: () => readStorage() ?? undefined,
queryFn: async () => {
const antIds = await fetchAllAntsFromActivity({
ao: aoClient,
marketplaceProcessId: BLOCKYDEVS_MARKETPLACE_PROCESS_ID,
});

const result = await fetchANTsMetadata({
ao: aoClient,
arioProcessId,
antIds,
});

updateStorage(result);

return result;
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Scope metadata query key to environment to avoid cache collisions.

Include arioProcessId (and optionally marketplace process id) in the key.

-  return queryOptions({
-    queryKey: [marketplaceQueryKeys.metadata.all],
+  return queryOptions({
+    queryKey: [
+      marketplaceQueryKeys.metadata.all,
+      { arioProcessId, marketplaceProcessId: BLOCKYDEVS_MARKETPLACE_PROCESS_ID },
+    ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return queryOptions({
queryKey: [marketplaceQueryKeys.metadata.all],
initialData: () => readStorage() ?? undefined,
queryFn: async () => {
const antIds = await fetchAllAntsFromActivity({
ao: aoClient,
marketplaceProcessId: BLOCKYDEVS_MARKETPLACE_PROCESS_ID,
});
const result = await fetchANTsMetadata({
ao: aoClient,
arioProcessId,
antIds,
});
updateStorage(result);
return result;
},
});
return queryOptions({
queryKey: [
marketplaceQueryKeys.metadata.all,
{ arioProcessId, marketplaceProcessId: BLOCKYDEVS_MARKETPLACE_PROCESS_ID },
],
initialData: () => readStorage() ?? undefined,
queryFn: async () => {
const antIds = await fetchAllAntsFromActivity({
ao: aoClient,
marketplaceProcessId: BLOCKYDEVS_MARKETPLACE_PROCESS_ID,
});
const result = await fetchANTsMetadata({
ao: aoClient,
arioProcessId,
antIds,
});
updateStorage(result);
return result;
},
});
🤖 Prompt for AI Agents
In src/hooks/listings/useAntsMetadata.ts around lines 53 to 72, the queryKey
currently uses a global metadata key causing cache collisions across
environments; include arioProcessId (and optionally the marketplace process id)
in the queryKey so the cache is scoped per environment. Update the queryKey
array to include arioProcessId and BLOCKYDEVS_MARKETPLACE_PROCESS_ID (or the
local marketplaceProcessId variable) and ensure any readStorage/updateStorage
usage is consistent with the new keyed identity so initialData and cache
reads/writes remain correct.

Comment on lines +75 to +91
export const useAntsMetadata = () => {
const [{ aoClient, arioProcessId }] = useGlobalState();
const inFlightRef = useRef(false);

const initialData = readStorage();

// refetching should happen only when there's no initial data or user has encountered ANT without metadata
const query = useQuery({
...antsMetadataQueryOptions({ aoClient, arioProcessId }),
refetchInterval: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: initialData ? Infinity : 0,
placeholderData: keepPreviousData,
});

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard the query until aoClient and arioProcessId are available.

Prevents calling SDK with undefined client/ids.

-  const query = useQuery({
+  const query = useQuery({
     ...antsMetadataQueryOptions({ aoClient, arioProcessId }),
+    enabled: Boolean(aoClient && arioProcessId),
     refetchInterval: false,
     refetchOnWindowFocus: false,
     refetchOnMount: false,
     refetchOnReconnect: false,
     staleTime: initialData ? Infinity : 0,
     placeholderData: keepPreviousData,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useAntsMetadata = () => {
const [{ aoClient, arioProcessId }] = useGlobalState();
const inFlightRef = useRef(false);
const initialData = readStorage();
// refetching should happen only when there's no initial data or user has encountered ANT without metadata
const query = useQuery({
...antsMetadataQueryOptions({ aoClient, arioProcessId }),
refetchInterval: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: initialData ? Infinity : 0,
placeholderData: keepPreviousData,
});
export const useAntsMetadata = () => {
const [{ aoClient, arioProcessId }] = useGlobalState();
const inFlightRef = useRef(false);
const initialData = readStorage();
// refetching should happen only when there's no initial data or user has encountered ANT without metadata
const query = useQuery({
...antsMetadataQueryOptions({ aoClient, arioProcessId }),
enabled: Boolean(aoClient && arioProcessId),
refetchInterval: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: initialData ? Infinity : 0,
placeholderData: keepPreviousData,
});
🤖 Prompt for AI Agents
In src/hooks/listings/useAntsMetadata.ts around lines 75 to 91, the query runs
even when aoClient or arioProcessId may be undefined; add a guard by enabling
the query only when both aoClient and arioProcessId are present (set the query's
enabled option to a boolean like !!aoClient && !!arioProcessId) so the SDK is
never called with undefined values, and keep the existing
placeholderData/staleTime logic intact so behavior with initialData remains
unchanged.

Comment on lines +12 to +13
const [{ antAoClient, arioProcessId }] = useGlobalState();

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: wrong client variable used (antAoClient). Should be aoClient.

Other modules use aoClient; antAoClient likely undefined, causing runtime failure when calling bidListing.

-  const [{ antAoClient, arioProcessId }] = useGlobalState();
+  const [{ aoClient, arioProcessId }] = useGlobalState();
@@
-      return await bidListing({
-        ao: antAoClient,
+      return await bidListing({
+        ao: aoClient,

Also applies to: 32-41

🤖 Prompt for AI Agents
In src/hooks/listings/useBidListing.tsx around lines 12-13 and also covering
lines 32-41, the code destructures and references antAoClient which is the wrong
variable name and likely undefined; change the destructuring from
useGlobalState() to extract aoClient (not antAoClient) and update all subsequent
references in this block (lines 32-41) to use aoClient so the correct client is
used when calling bidListing and related methods.

Comment on lines +14 to +31
return useMutation({
mutationFn: async ({ price }: { price: string }) => {
if (!wallet || !walletAddress) {
throw new Error('No wallet connected');
}

if (!wallet.contractSigner) {
throw new Error('No wallet signer available');
}

if (!antProcessId) {
throw new Error('antProcessId is missing');
}

if (!listingId) {
throw new Error('listingId is missing');
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate bid price before submitting.

Reject empty, non-numeric, NaN, or non-positive values early.

   return useMutation({
     mutationFn: async ({ price }: { price: string }) => {
+      const parsed = Number(price);
+      if (!Number.isFinite(parsed) || parsed <= 0) {
+        throw new Error('Invalid bid price');
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return useMutation({
mutationFn: async ({ price }: { price: string }) => {
if (!wallet || !walletAddress) {
throw new Error('No wallet connected');
}
if (!wallet.contractSigner) {
throw new Error('No wallet signer available');
}
if (!antProcessId) {
throw new Error('antProcessId is missing');
}
if (!listingId) {
throw new Error('listingId is missing');
}
return useMutation({
mutationFn: async ({ price }: { price: string }) => {
const parsed = Number(price);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error('Invalid bid price');
}
if (!wallet || !walletAddress) {
throw new Error('No wallet connected');
}
if (!wallet.contractSigner) {
throw new Error('No wallet signer available');
}
if (!antProcessId) {
throw new Error('antProcessId is missing');
}
if (!listingId) {
throw new Error('listingId is missing');
}
🤖 Prompt for AI Agents
In src/hooks/listings/useBidListing.tsx around lines 14 to 31, the mutation
currently accepts any price string; add explicit validation at the start of
mutationFn to reject empty, whitespace-only, non-numeric, NaN, or non-positive
values: trim the input, parse it to a number (Number or parseFloat), check for
empty string, isNaN(parsed), or parsed <= 0 and throw a clear Error (e.g.
"Invalid bid price") before proceeding with wallet/ID checks so invalid bids are
rejected early.

Comment on lines +90 to +95
case 'custom': {
if (!date || !time) return 0;
const customDate = mergeDateAndTime(date, time);
if (!customDate) return 0;
return customDate.getTime() - Date.now();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Clamp custom duration to non‑negative.

If the chosen datetime is in the past, return 0 instead of a negative duration.

-      return customDate.getTime() - Date.now();
+      return Math.max(0, customDate.getTime() - Date.now());
🤖 Prompt for AI Agents
In src/utils/marketplace.ts around lines 90 to 95, the 'custom' case can return
a negative duration when the combined date/time is in the past; change the
return to clamp to non‑negative by computing the difference between
customDate.getTime() and Date.now() and returning Math.max(0, difference) so it
returns 0 for past datetimes instead of a negative value.

Comment on lines +175 to +179
list: (type: 'active' | 'completed', options?: Record<string, unknown>) => [
marketplaceQueryKeys.listings.all,
type,
options,
],
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Stabilize query keys: avoid raw options objects.

Using objects in React Query keys causes cache misses if identity changes each render. Hash or stringify with stable key order.

-    list: (type: 'active' | 'completed', options?: Record<string, unknown>) => [
-      marketplaceQueryKeys.listings.all,
-      type,
-      options,
-    ],
+    list: (type: 'active' | 'completed', options?: Record<string, unknown>) => {
+      const stableOptions = options ? stableStringify(options) : undefined;
+      return [marketplaceQueryKeys.listings.all, type, stableOptions];
+    },

Add helper (top‑level in this module):

const stableStringify = (value: unknown): string => {
  const sortKeys = (v: any): any => {
    if (Array.isArray(v)) return v.map(sortKeys);
    if (v && typeof v === 'object' && v.constructor === Object) {
      return Object.keys(v).sort().reduce((acc, k) => {
        acc[k] = sortKeys(v[k]);
        return acc;
      }, {} as Record<string, unknown>);
    }
    return v;
  };
  return JSON.stringify(sortKeys(value));
};
🤖 Prompt for AI Agents
In src/utils/marketplace.ts around lines 175 to 179, the React Query key
currently includes the raw options object causing cache misses when object
identity changes; add the suggested top-level stableStringify helper that
recursively sorts object keys and JSON.stringifys the result, then replace the
raw options in the list key with stableStringify(options) (or omit/undefined
when options is falsy) so keys are stable across renders and preserve clear,
deterministic key ordering.

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.

6 participants