-
Notifications
You must be signed in to change notification settings - Fork 3
chore: ArNS Marketplace UI #880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Potential review hotspots:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
| </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> |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this 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.tsRepo 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 buttonExpose 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-whiteis overridden bytext-[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‑nabbingUpdate 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/invalidCurrent 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 navigationNames/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 errorsPrevent 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.logInvalidation 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/dedupeHelps 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‑linkingConsider 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.crumbsfor/listings,/listings/:id, and/my-ants/*to match the Manage flows.
2-2: Remove duplicate global CSS import from src/App.tsxstyle.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
createSearchParamsto safely passname.-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
getCurrentListingArioPricein@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
endedAtsupport togetCurrentListingArioPriceso both places share identical logic?src/components/pages/Listings/Details/EnglishListingSettlementSection.tsx (2)
60-65: Harden error handling.
error.messagemay be undefined iferrorisn’t anError. 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
useMemoand 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
.messageexists onerror.- 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.
getStatusVariantFromListingcan returnundefined; pass a fallback to avoid UI regressions.- const status = getStatusVariantFromListing(listing); + const status = getStatusVariantFromListing(listing) ?? 'processing';Please confirm
DetailsCardaccepts this fallback.src/components/pages/Listings/SearchListingByName.tsx (2)
27-33: Defensive error handling.
errormay not be anError. 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 insideselect; move cursor storage toonSuccess.
selectshould be pure. Persist cursors inonSuccess.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.
mutationBuyListingandmutationBidListingrepeat the same guards. Extract a smallensureWalletReady()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
⛔ Files ignored due to path filters (1)
yarn.lockis 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 buyerAddressIf upstream ever passes an empty string,
shortenAddressmay 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 placeholderImproves 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 ruleAdding 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 orderUI styles before app overrides is correct.
src/components/pages/index.ts (1)
2-2: Expose Listings pageExport wiring looks good.
Also applies to: 12-12
package.json (1)
32-33: Deps look consistent with UI additions — quick verificationpackage.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 protectedRepo 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
pricein 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.typeand 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: Validatetypevs 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.
| 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(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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
⛔ Files ignored due to path filters (1)
yarn.lockis 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.
There was a problem hiding this 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 defensivenewWin.opener = nullafter 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 computedendDateinstead of reparsing.We already compute
endDateabove (and after the fixes it will be aDate). Converting back and forth vianew Date(endDate)risksInvalid Datewhen the preview string drifts. Pass theDatedirectly 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
📒 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(), …), sonew Date(endDate)feeds"Invalid DateT12:00"into the modal and schedule math. Please reusemergeDateAndTimefor the custom branch and pass the Date directly; for preset durations, forwardform.date/form.timeintogetMsFromDurationso 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 useendDatedirectly.)
| console.log(`${operation} success`, { data }); | ||
| void Promise.allSettled([ | ||
| queryClient.refetchQueries({ | ||
| queryKey: [marketplaceQueryKeys.listings.all], | ||
| }), | ||
| queryClient.refetchQueries({ | ||
| queryKey: [marketplaceQueryKeys.myANTs.all], | ||
| }), | ||
| ]); | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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
📒 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)
There was a problem hiding this 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
optionsobject can break caching if it contains non‑serializable values or unstable identity.
- Confirm
optionsonly 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/marketplacehas no side effects; the typed imports below suffice.Apply this diff:
-import '@src/utils/marketplace';
309-315: Pass a Date to PriceScheduleModal.After fixing
endDateto 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: avoidnew 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
0duration 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 withvalueoverdefaultValue.Ensures the UI reflects state updates reliably.
If
Selectsupports it, switch tovalue={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
errorisn’t anErrorinstance.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
📒 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_IDor add an allowlist entry in.gitleaks.tomlto 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
formatDateand 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, + );
| form.date | ||
| ? `${formatDate(form.date.toString(), 'dd.MM.yy')} ${ | ||
| form.time | ||
| }` | ||
| : '-' | ||
| } | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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
optionsin 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
📒 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
mergeDateAndTimeand 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();
| 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 }), | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| const decreaseStepMario = Math.round( | ||
| Number(arioToMario(decreaseStepArio)), | ||
| ).toString(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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
calculateCurrentDutchListingPricein 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.
errorisunknownin 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
📒 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,noreferrerand also nullifyopenerwhen 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
settledfirst and guard null before converting. Prevents wrong price for settled English and avoids passing undefined tomarioToArio.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'); + }
f876dd6 to
5c7693e
Compare
5c7693e to
034797e
Compare
There was a problem hiding this 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 submittingAvoid 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 foruseSearchParamsImproves 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 importThis pulls the whole module for no reason.
-import '@src/utils/marketplace';
310-315: Avoid reparsing endDateYou 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 controlledUse
valueinstead ofdefaultValue.- <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 listingsIf 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
⛔ Files ignored due to path filters (1)
yarn.lockis 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 properlyCurrent 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: LGTMComposition, tabs, and search wiring look good.
src/utils/routes.tsx (1)
62-69: LGTMRoute 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:
erroris unknown;.messageaccess 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.roundcan yield0for tiny ARIO deltas, so we end up passing a zerodecreaseSteptogetDutchListingSchedule, 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();
| const queryActiveListings = useQuery({ | ||
| refetchInterval: 15 * 1000, | ||
| structuralSharing: false, | ||
| queryKey: marketplaceQueryKeys.listings.list('active', { | ||
| page: pagination.page, | ||
| pageSize: pagination.pageSize, | ||
| }), | ||
| queryFn: () => { | ||
| return fetchActiveListings({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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, | ||
| }); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| if (queryDetails.error) { | ||
| return ( | ||
| <p className="text-error text-center"> | ||
| Failed to load listing details: {queryDetails.error.message} | ||
| </p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
src/components/pages/Listings/Details/DutchListingPriceSection.tsx
Outdated
Show resolved
Hide resolved
src/components/pages/Listings/Details/FixedListingPriceSection.tsx
Outdated
Show resolved
Hide resolved
| onError: (error) => { | ||
| eventEmitter.emit('error', { | ||
| name: `Failed search for "${searchValue}"`, | ||
| message: error.message, | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| onClick={() => { | ||
| navigate(`/register/${mutationSearch.variables}`); | ||
| }} | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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 claritySlightly 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
📒 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 RegisterEnsure 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 emittingAccessing 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 SuspenseNew 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 emittingDon’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
There was a problem hiding this 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 tolistings.Apply this diff if you agree:
- listing: { + listings: { text: 'Listings',
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.
There was a problem hiding this 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
📒 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
BookCopyfrom 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).
| {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> | ||
| } | ||
| /> | ||
| )} |
There was a problem hiding this comment.
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.
| {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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| row.original.name | |
| lowerCaseDomain(row.original.name) |
| const [{ walletAddress }] = useWalletState(); | ||
| const queryDetails = useQuery({ | ||
| enabled: !!id, | ||
| refetchInterval: 15 * 1000, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| onSuccess: async (data) => { | ||
| console.log(`cancel success`, { data }); | ||
| void Promise.allSettled([ | ||
| queryClient.refetchQueries({ | ||
| queryKey: [marketplaceQueryKeys.listings.all], | ||
| }), | ||
| queryClient.refetchQueries({ | ||
| queryKey: [marketplaceQueryKeys.myANTs.all], | ||
| }), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| queryKey: marketplaceQueryKeys.listings.list('active', { | ||
| page: pagination.page, | ||
| pageSize: pagination.pageSize, |
There was a problem hiding this comment.
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.
| 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, | ||
| }); | ||
| }, |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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
| onSuccess: async (data) => { | ||
| console.log(`${operation} success`, { data }); | ||
| void Promise.allSettled([ | ||
| queryClient.refetchQueries({ | ||
| queryKey: [marketplaceQueryKeys.listings.all], | ||
| }), | ||
| queryClient.refetchQueries({ | ||
| queryKey: [marketplaceQueryKeys.myANTs.all], | ||
| }), | ||
| ]); | ||
| }, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| name, | |
| name: lowerCaseDomain(name), |
| const queryMyANTs = useQuery({ | ||
| enabled: !!walletAddress, | ||
| refetchInterval: 15 * 1000, | ||
| queryKey: marketplaceQueryKeys.myANTs.list(walletAddress?.toString()), |
There was a problem hiding this comment.
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.
There was a problem hiding this 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'; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this 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: Addenabledflag to prevent query execution before aoClient is ready.The past review comment about this issue is still unresolved. Without an
enabledflag, the query will execute immediately even ifaoClientisundefined, causing failures whenfetchActiveListingsattempts 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
lowerCaseDomainfor 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
📒 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:
enabledflag prevents queries with undefined clientcursoris included in the query key for proper cache invalidationThe 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
isPendingstatus. The conditional pagination rendering prevents layout shifts during loading.
57-57: Ensure safe conversion of ARIO price string to Number
ParsingcurrentPrice(returned as a string bygetCurrentListingArioPrice) withNumber(...)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.
There was a problem hiding this 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 toerror.messagewithout type guard.React Query v5 types the
errorparameter asunknown. Directly accessingerror.messagewill throw a TypeScript error and can fail at runtime if the error is not anErrorinstance.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
useBidListinganduseBuyListingduplicate 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
typeparameter isstring | 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
refetchQueriesforces an immediate network request.invalidateQueriesmarks 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
📒 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)
…e-marketplace-changes feat: support last marketplace changes
There was a problem hiding this 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
variablescan 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 assumeerror.message
erroris unknown; accessing.messagecan 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: setopener = nullas 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 preventInvalid Date.Parsing non‑numeric or partial times yields
NaNand breaksDate.- 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 returnfinalPricefirst; also handle nulls.Reorder branches to prioritize
settledand guard againstnull/undefinedprices.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
antAoClientandarioProcessIdexist before callingbuyListing.- Narrow
orderTypefor 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
orderIdmay 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 submissionSet 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 UXSearch uses ASCII/punycode via
lowerCaseDomain, but the displayed domain should be in Unicode form. Use the existingdecodeDomainToASCII()utility fromsearchUtilswhen renderingmutationSearch.variablesin 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
nullcreates a blank section; consider a lightweight placeholder for perceived performance.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
yarn.lockis 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 storageMemoized queryOptions and the initialData gate avoid redundant fetches. Nice.
src/App.tsx (1)
353-412: Listings routes integration looks goodLazy routes + Suspense + PageLoader are consistent with existing patterns.
Please confirm
/listings/:id/confirm-purchaseconsumers expect hash routing (createHashRouter) so deep links work behind static hosting.src/components/pages/Listings/Details/ListingMetadata.tsx (2)
41-42: Verify return type ofmarioToArioand precision handling.The concern about precision loss is reasonable: inconsistent handling across the codebase suggests this is known risk (see
PriceScheduleModal.tsxusing.toFixed(6)vs.ListingMetadata.tsxnot using it). Confirm whethermarioToArioreturns a string or high-precision number, and whetherNumber()alone at line 41 causes unacceptable rounding compared to the.toFixed(6)pattern used elsewhere.
63-66: Verify the unit ofleaseEndsAtfrom@blockydevs/arns-marketplace-data.The concern is valid:
formatDateexpects milliseconds, butleaseEndsAtis passed directly without any conversion. In the same file, other dates use.getTime()to convert to milliseconds before callingformatDate(lines 35–38). Confirm whetherleaseEndsAtis already in milliseconds or requires conversion to avoid displaying incorrect dates.
| useEffect(() => { | ||
| if (antMeta) return; | ||
|
|
||
| queryAntsMetadata.refetch(); | ||
| }, [antMeta, queryAntsMetadata]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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]); |
| createdAt: new Date(listing.createdAt).getTime(), | ||
| endedAt: new Date( | ||
| 'endedAt' in listing ? listing.endedAt : listing.expiresAt, | ||
| ).getTime(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| useEffect(() => { | ||
| if (!antMeta) { | ||
| queryAntsMetadata.refetch(); | ||
| } | ||
| }, [antMeta, queryAntsMetadata]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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; | ||
| }, | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| 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, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| const [{ antAoClient, arioProcessId }] = useGlobalState(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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'); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| case 'custom': { | ||
| if (!date || !time) return 0; | ||
| const customDate = mergeDateAndTime(date, time); | ||
| if (!customDate) return 0; | ||
| return customDate.getTime() - Date.now(); | ||
| } |
There was a problem hiding this comment.
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.
| list: (type: 'active' | 'completed', options?: Record<string, unknown>) => [ | ||
| marketplaceQueryKeys.listings.all, | ||
| type, | ||
| options, | ||
| ], |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
New Features
Style
Chores