-
Notifications
You must be signed in to change notification settings - Fork 4
fix(PE-8875): marketplace feature feedback #916
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
fix(PE-8875): marketplace feature feedback #916
Conversation
…sage and display of price
…ddresses and safe ario formatting
fix(marketplace): bug fixes
📝 WalkthroughWalkthroughThis PR introduces a comprehensive ArNS Marketplace feature, removing legacy test infrastructure (Jest, Playwright, Babel configs) and switching to pnpm. It adds marketplace pages, modals, hooks, and state management for listing/buying domains, while updating critical paths like global state and domain asset fetching to support marketplace contracts and process IDs. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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 |
…e-feature-feedback
Permaweb Preview DeployedYour preview is available at: https://arns-react-pr-916_arns.ar.io
|
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: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/services/wallets/WanderWalletConnector.ts (1)
103-103: Inconsistent use ofsafeWanderApiExecutor.Unlike
connect()which wrapsgetPermissions()withsafeWanderApiExecutor(lines 58-60), this call is made directly. Per the comment on lines 37-41, the Wander API can hang, so this should likely be wrapped for consistency:- const permissions = await this._wallet.getPermissions(); + const permissions = await this.safeWanderApiExecutor( + this._wallet?.getPermissions, + );src/components/cards/NavMenuCard/NavMenuCard.tsx (1)
71-85: Split balance refresh from click‑outside listener to avoid duplicate fetches.Right now, the
showMenudependency is needed to attach the click‑outside listener when the menu opens, but it also re‑runs the balance reset/fetch on every open/close, which can cause unnecessary requests and loader flicker. Consider separating these concerns into two effects: one for wallet balance refresh (wallet changes), and one for click‑outside binding (menu open).💡 Suggested refactor
- useEffect(() => { - if (walletAddress) { - resetWalletDetails(); - fetchWalletDetails(walletAddress); - } - - if (!menuRef.current) { - return; - } - document.addEventListener('mousedown', handleClickOutside); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [menuRef, showMenu, wallet, walletAddress]); + useEffect(() => { + if (walletAddress) { + resetWalletDetails(); + fetchWalletDetails(walletAddress); + } + }, [wallet, walletAddress]); + + useEffect(() => { + if (!showMenu || !menuRef.current) return; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showMenu]);Also applies to: 97-102
src/state/contexts/ArNSState.tsx (1)
71-82: Add missing dependenciesmarketplaceProcessIdandantRegistryProcessIdto useEffect dependency array.Both
marketplaceProcessIdandantRegistryProcessIdare extracted from global state, passed todispatchArNSUpdate, and can change at runtime via reducer actions. However, the dependency array is missing both. This causes the effect to not re-run when these values change, leading to stale process IDs being used in contract initialization and critical ownership checks.Proposed fix
}, [walletAddress, aoNetwork, arioProcessId, hyperbeamUrl]); + }, [walletAddress, aoNetwork, arioProcessId, hyperbeamUrl, marketplaceProcessId, antRegistryProcessId]);src/components/pages/Settings/NetworkSettings.tsx (1)
511-590: The "Set" button cannot be clicked when the field is empty, blocking users from clearing/disabling these URLs.
isValidNetworkURL('')returnsfalse, which setsisValid=falseon theSettingInputcomponent. This disables the "Set" button (line 77 ofSettingInput.tsx). TheonPressEnterhandler also blocks empty input with its own validation guard. While theonSetpayload logic supports clearing viaundefined, it's unreachable when the field is empty. Users can only restore defaults via the Reset button, not clear the URL entirely.src/components/layout/Navbar/NotificationMenu/NotificationMenu.tsx (1)
24-28: Interrupted-workflow detection relies onReactNode.toString()and will never match.
notification.messageis a React element, so.toString()returns"[object Object]", meaning the "Interrupted" checks never fire. Add an explicitkind(or similar) field and branch on that.✅ Suggested fix with explicit kind
export type Notification = { type: 'success' | 'warning' | 'error'; message: ReactNode; link: string; + kind: 'interrupted' | 'expiry' | 'undername' | 'update'; }; // Interrupted workflows notification if (interruptedWorkflowsCount > 0) { const firstDomain = interruptedWorkflows[0]?.domainName; notificationList.push({ type: 'error', message: ( <span className="text-sm"> {interruptedWorkflowsCount === 1 ? `Interrupted marketplace workflow for ${firstDomain}` : `${interruptedWorkflowsCount} interrupted marketplace workflows`} </span> ), link: '/manage/names', + kind: 'interrupted', }); } // ...set kind for other notifications (expiry/undername/update) - } : notification.type === 'error' && - notification.message?.toString().includes('Interrupted') ? ( + } : notification.kind === 'interrupted' ? ( <AlertTriangle className="text-error animate-pulse" width={'16px'} height={'16px'} /> ) : ( <CircleAlert className="text-error" width={'16px'} height={'16px'} /> )} - {notification.message?.toString().includes('Interrupted') ? ( + {notification.kind === 'interrupted' ? ( <button ...> <AlertTriangle className="size-4" /> </button> ) : ( <Link ...> <Settings className="size-4" /> </Link> )}Also applies to: 215-369
src/components/data-display/tables/DomainsTable.tsx (1)
589-595: Missing dependencies in useEffect.The effect uses
walletAddress,latestAntVersion, andmarketplaceProcessIdbut they are not included in the dependency array. This could cause stale table data when these values change.🔧 Suggested fix
}, [ domainData, loading, loadingArnsState, primaryNameData, dispatchArNSState, + walletAddress, + latestAntVersion, + marketplaceProcessId, ]);src/state/contexts/GlobalState.tsx (1)
118-126: AddMU_URLto marketplace contract initialization for consistency.The
defaultMarketplaceContractis missingMU_URLin itsconnect()configuration, whiledefaultArIOand other AO contracts throughout the codebase include bothCU_URLandMU_URL. The Messenger Unit endpoint is a standard configuration parameter for AO Connect.Suggested fix
export const defaultMarketplaceContract = new ArNSMarketplaceRead({ process: new AOProcess({ processId: initialMarketplaceProcessId, ao: connect({ CU_URL: initialAoNetwork.ARIO.CU_URL, + MU_URL: initialAoNetwork.ARIO.MU_URL, MODE: 'legacy', }), }), });
🤖 Fix all issues with AI agents
In `@package.json`:
- Line 27: The dependency "@ar.io/sdk" is pinned to a pre-release alpha version;
change the version string in package.json from "^3.23.0-alpha.3" to the stable
"3.19.0" and run your package manager to update lockfiles (npm install / yarn
install) and CI artifacts, or if the alpha is required, add a short
justification comment in the repo (e.g., in README or DEPENDENCIES.md) and
include a package.json comment (or a maintainer note) explaining why
"@ar.io/sdk" must remain at "^3.23.0-alpha.3" and any risks/required follow-ups
so reviewers understand the rationale.
In `@src/components/layout/Navbar/NotificationMenu/NotificationMenu.tsx`:
- Around line 182-195: The namesExceedingLimit calculation in
namesExceedingLimit (inside the useMemo) incorrectly uses the hard-coded
threshold 10000; update the reduce callback to compare
Object.keys(ant.state.Records).length against the domain-specific limit
record.undernameLimit (falling back to a sensible default if undernameLimit is
undefined) instead of 10000 so each AoArNSNameData record's per-domain limit is
honored when computing violations.
In `@src/components/modals/ContinueWorkflowModal/ContinueWorkflowModal.tsx`:
- Around line 184-195: In ContinueWorkflowModal, the polling loop around
marketplaceContract.getUserAssets uses a const retries and never increments,
causing an infinite loop; change retries to a let, increment it inside the while
loop, break out of the loop on successful fetch (when _res is received), and on
failure log the error and optionally await a short delay/backoff before
retrying; ensure you still respect maxRetries and handle the final failure case
(e.g., surface an error state) using symbols walletAddress, arioProcessId,
marketplaceContract.getUserAssets, retries, and maxRetries to locate the code.
In `@src/components/modals/ListNameForSaleModal/ListNameForSaleModal.tsx`:
- Around line 380-416: The loop in ListNameForSaleModal.tsx incorrectly treats
an equal ARIO balance as insufficient: inside the while loop (in the block that
checks tries and balance) change the condition that currently throws on
newARIOBalance <= feeDetails.listingFee to only throw when newARIOBalance <
feeDetails.listingFee (so equality is accepted); specifically update the
conditional in the try block used with
writeMarketplaceContract.getMarketplaceBalance and mARIOToken conversion so it
only errors when tries >= maxTries OR newARIOBalance < feeDetails.listingFee,
keeping the outer loop while (newARIOBalance < feeDetails.listingFee) logic
intact.
In `@src/components/modals/ListNameForSaleModal/panels/ConfirmListingPanel.tsx`:
- Around line 74-90: The 'Current Owner' entry in orderSummary in
ConfirmListingPanel uses walletAddress?.toString().slice(...) which can call
.slice on undefined and crash rendering; update the code that builds
orderSummary (or create a small helper like ownerDisplay) to check for a present
walletAddress/toString() result before slicing and provide a safe fallback
string (e.g., 'Not connected' or '—') when missing, ensuring the 'Current Owner'
value never calls .slice on undefined.
In `@src/components/modals/ListNameForSaleModal/panels/FixedPricePanel.tsx`:
- Around line 149-161: The preset button highlighting is inconsistent because
buttons set buttonSelected to the index while the sync effect in FixedPricePanel
expects buttonSelected to be the matching value; update the button click handler
for the PRICE_BUTTON_VALUES map so it calls setButtonSelected(value) (not index)
and compare the class condition to buttonSelected === value (or cast types
consistently) so the stored selection matches the sync effect; ensure
setCustomValue(value.toString()), setListingPrice(value) and
setCustomValueError('') remain but use value for setButtonSelected to align with
the existing effect.
In `@src/hooks/useInterruptedWorkflows.ts`:
- Around line 68-71: The current lookup for antIntent uses intents.find by
antProcessId but doesn't ensure the intent is still pending, causing completed
intents to be treated as interrupted; update the find predicate in
useInterruptedWorkflows (where antIntent is computed) to also check the intent's
pending status (e.g., intent.status === 'PENDING' or the project's equivalent
flag on MarketplaceIntent) so only active/pending intents are matched by
antProcessId.
In `@src/hooks/useMarketplaceUserAssets.ts`:
- Around line 20-39: The query uses arioProcessId in
marketplaceContract.getUserAssets(...) but omits it from the React Query cache
key and enabled guard; update the queryKey array (the 'marketplace-user-assets'
key) to include arioProcessId and add arioProcessId to the enabled condition so
the hook only runs when marketplaceContract, marketplaceProcessId, address,
aoNetwork.ARIO, and arioProcessId are all truthy, ensuring cache invalidation
and preventing calls with an undefined arioProcessId.
In `@src/hooks/usePendingWorkflows.ts`:
- Around line 54-63: The code currently only excludes marketplace ownership;
update usePendingWorkflows to require the ANT be owned by the current user by
replacing the marketplace-only check with an explicit equality check against the
current process id (e.g., if (ant.state.Owner !== processId) return;), so the
hook returns early unless ant.state.Owner equals the user's process id; keep the
existing marketplaceProcessId symbol in mind when locating the code and ensure
intents.find still filters by intent.antProcessId === antId && intent.status ===
'pending'.
In `@src/utils/constants.ts`:
- Around line 103-105: The MARKETPLACE_PROCESS_ID constant currently falls back
to a hard-coded mainnet ID and is not configurable per environment; update
MARKETPLACE_PROCESS_ID (in src/utils/constants.ts) to follow the same
environment-based pattern as ARIO_PROCESS_ID and ANT_REGISTRY_PROCESS_ID by
allowing environment overrides and adding explicit variants (e.g.,
testnet/staging and mainnet) or ensure the fallback value is the correct mainnet
ID used in production; modify the code to read an env-specific variable
(VITE_MARKETPLACE_PROCESS_ID) and, if needed, add additional env keys or
conditional selection logic so CI/CD can supply the appropriate ID for
staging/testnet and production.
In `@src/utils/searchUtils/searchUtils.ts`:
- Around line 204-241: The isValidNetworkURL function allows scheme-less inputs
because the catch fallback uses URL_REGEX that permits optional schemes; add an
explicit pre-check at the top of isValidNetworkURL to reject any string that
does not start with "http://" or "https://" (e.g., using
/^https?:\/\//i.test(url)) before attempting new URL(url), so scheme-less inputs
return false and the catch fallback cannot validate them; update logic in
isValidNetworkURL and keep the existing URL_REGEX only for validating
syntactically correct URLs that already include the http(s) scheme.
🟡 Minor comments (13)
package.json-40-41 (1)
40-41: Remove@sentry/browseras it is redundant with@sentry/react.
@sentry/reactis a React-specific wrapper that re-exports the@sentry/browserAPI plus React-specific helpers (ErrorBoundary, Profiler, etc.). For a React application, only@sentry/reactis needed. Having both packages contributes to unnecessary bundle bloat.src/components/cards/NavMenuCard/NavMenuCard.tsx-111-139 (1)
111-139: Avoid infinite loader on fetch failure.On error, the balance stays
undefined, so the UI keeps showing a loader indefinitely. Consider setting a fallback display value (e.g.,'—') on error so the UI can settle gracefully.🐛 Suggested fix
} catch (error) { console.error('Error fetching wallet details:', error); + setWalletDetails((prev) => ({ + ...prev, + [arioTicker]: '—', + })); }src/components/data-display/VerticalTimelineStepper.tsx-16-37 (1)
16-37: Use<li>as the direct<ol>child for correct semantics.Right now each
<ol>child is a<div>wrapping a<li>, which breaks list semantics and can hurt accessibility. You can also avoid repeatedObject.entriescalls by caching the entries.✅ Suggested fix
- return ( - <ol className="relative gap-2"> - {Object.entries(steps).map(([, { title, description, icon }], i) => ( - <div key={i}> - <li className="ms-7"> + const entries = Object.entries(steps); + return ( + <ol className="relative gap-2"> + {entries.map(([, { title, description, icon }], i) => ( + <li key={i} className="ms-7 relative"> <span className="absolute flex items-center justify-center w-8 h-8 rounded-full -start-4 ring-1 ring-dark-grey"> {icon} </span> <div className="flex flex-col max-h-8 overflow-visible"> {' '} <h3 className="text-md leading-tight">{title}</h3> <p className="text-xs text-grey">{description}</p> </div> - </li> - <div - className={ - i === Object.entries(steps).length - 1 - ? '' - : 'border-l border-dark-grey h-8' - } - key={`${i}-divider`} - /> - </div> + {i < entries.length - 1 && ( + <div className="border-l border-dark-grey h-8" /> + )} + </li> ))} </ol> );src/components/pages/ViewListing/ViewListing.tsx-857-864 (1)
857-864: Incorrect message for seller cancellation flow.When a seller successfully cancels a listing, the message still says
${name} is now yours!which is incorrect. The message should differentiate between buy and cancel outcomes.🐛 Proposed fix
<p className="text-sm text-grey"> {workflowComplete ? workflowError ? 'There was an error completing your purchase.' - : `${name} is now yours!` + : isUserSeller + ? 'Your listing has been cancelled.' + : `${name} is now yours!` : 'Please wait while we process your purchase. Do not close this page.'} </p>src/components/inputs/DateTimePicker/DateTimePicker.tsx-39-46 (1)
39-46: State initialization doesn't sync whenvalueprop changes externally.The initial state for
hour12,minutes, andperiodis only set once during component mount. Ifvalueprop changes externally (e.g., parent resets it), the internal state won't update to reflect the new value.🔧 Suggested fix using useEffect
+// Sync internal state when value prop changes +useEffect(() => { + if (value) { + const hour24 = value.getHours(); + const converted = to12Hour(hour24); + setHour12(converted.hour12); + setMinutes(value.getMinutes()); + setPeriod(converted.period); + } +}, [value]);src/components/modals/ManageMarketplaceARIOModal/ManageMarketplaceARIOModal.tsx-27-31 (1)
27-31: Add an accessible label to the icon-only close button.Screen readers won’t have a name for the control without an aria-label.
🔧 Proposed fix
- <button onClick={onClose}> + <button onClick={onClose} aria-label="Close"> <XIcon className="size-5" /> </button>src/components/modals/ManageMarketplaceARIOModal/panels/WithdrawPanel.tsx-273-285 (1)
273-285: Allow clearing the custom amount input.When the input is emptied, it’s immediately coerced to
"0", which makes it impossible to clear.🔧 Proposed fix
onChange={(e) => { const val = e.target.value; + if (val === '') { + setCustomValue(''); + setPercentageSelected(undefined); + setCustomValueError(''); + setError(''); + return; + } if (isValidCustomFormat(val)) { const numVal = Number(val); const clampedVal = Math.min(numVal, currentBalance);src/components/modals/ContinueWorkflowModal/ContinueWorkflowModal.tsx-336-339 (1)
336-339: Align the success copy with the actual workflow type.The success message always mentions a transfer, which is incorrect for PUSH_INTENT workflows.
✏️ Suggested copy fix
- <p className="text-sm text-grey"> - The interrupted workflow has been successfully continued. The - ANT has been transferred to the marketplace with the correct - intent ID. - </p> + <p className="text-sm text-grey"> + {workflowType === InterruptedWorkflowType.TRANSFER + ? 'The interrupted workflow has been successfully continued. The ANT has been transferred to the marketplace with the correct intent ID.' + : 'The interrupted workflow has been successfully continued. The intent resolution was pushed to the marketplace.'} + </p>src/components/cards/MarketplaceOrderInfoCard/MarketplaceOrderInfoCard.tsx-77-82 (1)
77-82: Remove stray console logging.Line 81 leaks internal config to the console in production builds.
🧹 Proposed fix
- console.log(config);src/components/pages/Settings/devtools/ANTTools.tsx-225-228 (1)
225-228: Improve status text contrast.Line 226 uses
text-blackon a dark background, which is hard to read.🎨 Proposed fix
- {!walletAddress && ( - <span className="text-black text-xs"> + {!walletAddress && ( + <span className="text-grey text-xs"> Connect wallet to spawn ANT </span> )}src/components/modals/ManageMarketplaceARIOModal/panels/DepositPanel.tsx-298-310 (1)
298-310: Allow clearing the custom amount input.Lines 298‑305 convert an empty string to
0, which prevents clearing the field and immediately triggers the minimum‑deposit error. Handle the empty case explicitly.🧩 Proposed fix
onChange={(e) => { const val = e.target.value; + if (val === '') { + setCustomValue(''); + setButtonSelected(undefined); + setCustomValueError(''); + setError(''); + return; + } + if (isValidCustomFormat(val)) { const numVal = Number(val); const clampedVal = Math.min(numVal, MAX_ARIO_DEPOSIT); setCustomValue(clampedVal.toString());src/components/modals/ant-management/UpgradeDomainForMarketplaceModal/UpgradeDomainForMarketplaceModal.tsx-213-223 (1)
213-223: Avoid rendering an invalid source link.When
luaSourceIdis falsy, the anchor still renders with an invalid target. Render plain text instead.🔗 Proposed fix
- <span className="pt-2 text-primary"> - View the code:{' '} - <a - className="text-link" - href={`https://arscan.io/tx/${luaSourceId}`} - target="_blank" - rel="noreferrer" - > - {luaSourceId - ? formatForMaxCharCount(luaSourceId, 8) - : 'No source ID found'} - </a> - </span> + <span className="pt-2 text-primary"> + View the code:{' '} + {luaSourceId ? ( + <a + className="text-link" + href={`https://arscan.io/tx/${luaSourceId}`} + target="_blank" + rel="noreferrer" + > + {formatForMaxCharCount(luaSourceId, 8)} + </a> + ) : ( + <span>No source ID found</span> + )} + </span>src/components/modals/ant-management/UpgradeDomainForMarketplaceModal/UpgradeDomainForMarketplaceModal.tsx-59-63 (1)
59-63: Reset signing text on close.Line 59 closes the modal but leaves
signingMessageintact; reopening can show stale text.🧹 Proposed fix
function handleClose() { setVisible(false); setAccepted(false); setUpgrading(false); + setSigningMessage(''); }
🧹 Nitpick comments (31)
src/services/wallets/WanderWalletConnector.ts (1)
64-64: Consider consolidating type casts to improve type safety.Multiple
as anycasts throughout the file work around a type mismatch betweengetPermissions()return type andPermissionType. A cleaner approach is to castpermissionsonce at the source:const permissions = await this.safeWanderApiExecutor( this._wallet?.getPermissions, ) as PermissionType[] | undefined;This allows removing the individual
as anycasts in theincludes()calls while maintaining the workaround in a single location.Also applies to: 75-75
src/utils/common/common.tsx (3)
165-169: JSDoc incorrectly documents timestamp unit as seconds.The comment states
endTimestampis in seconds, but the implementation treats it as milliseconds—it's compared directly againstDate.now()(milliseconds) and passed toformatDate(epochMs). Update the JSDoc to reflect the actual unit.📝 Suggested fix
/** * Formats a unix timestamp into a human-readable date string. - * `@param` endTimestamp unix timestamp in seconds + * `@param` endTimestamp unix timestamp in milliseconds * `@returns` {`@type` JSX.Element} formatted date string */
170-208: Inconsistent return type: function returnsstring | JSX.Element.The function returns a plain string in two cases (lines 173, 207) but a
<Tooltip>JSX element otherwise. This inconsistency can cause unexpected rendering behavior at call sites and makes TypeScript inference less precise.Consider wrapping the string returns in a consistent element or extracting the permanent/error cases to return a similar structure.
♻️ Suggested approach
export function formatExpiryDate(endTimestamp?: number) { try { if (!endTimestamp) { - return PERMANENT_DOMAIN_MESSAGE; + return <span>{PERMANENT_DOMAIN_MESSAGE}</span>; } // ... existing Tooltip logic ... } catch { - return 'Error formatting expiry date'; + return <span>Error formatting expiry date</span>; } }
175-178: Nitpick:isExpiredis true during grace period, which is counterintuitive.The variable
isExpiredevaluates totruefor any domain past itsendTimestamp, including those still in grace period. While the logic works becauseisGracePeriodis checked first in conditionals, renaming or refining the condition would improve readability.♻️ Suggested clarification
const isGracePeriod = Date.now() > endTimestamp && Date.now() < endTimestamp + MILLISECONDS_IN_GRACE_PERIOD; - const isExpired = endTimestamp < Date.now(); + const isExpired = + Date.now() >= endTimestamp + MILLISECONDS_IN_GRACE_PERIOD; + const isActive = endTimestamp > Date.now();Then use
isActivefor the green color check instead ofendTimestamp > Date.now()at line 194..github/workflows/production.yml (1)
89-100: Inconsistent Node.js version specification.The
publishjob uses a hardcodednode-version: 20.xwhile other jobs (lint_and_build_check,build_for_production) usenode-version-file: '.nvmrc'. This could lead to version drift if.nvmrcis updated.Suggested fix
- uses: actions/setup-node@v4 with: - node-version: 20.x + node-version-file: '.nvmrc' + cache: 'yarn'.github/workflows/build_and_test.yml (1)
1-6: Consider renaming the workflow file.The workflow is now named "Build and Lint" but the file is still
build_and_test.yml. Consider renaming tobuild_and_lint.ymlfor consistency.package.json (1)
64-65: Consider movingpermaweb-deployto devDependencies.
permaweb-deployis only used in npm scripts (publish:arweave,deploy:arweave) and is not a runtime dependency. It should be indevDependenciesto reduce production bundle concerns.Suggested change
Move
permaweb-deployfromdependenciestodevDependencies:"dependencies": { ... - "permaweb-deploy": "^2.5.1", ... }, "devDependencies": { ... + "permaweb-deploy": "^2.5.1", ... }src/components/indicators/ARIOLoadingSpinner.tsx (1)
4-15: Add accessible loading semantics for screen readers.Right now the spinner is purely visual. Consider adding
role="status"and anaria-label(oraria-hiddenif another element already announces loading) so assistive tech can detect the loading state.♿ Proposed tweak
- return ( - <div className={className}> + return ( + <div className={className} role="status" aria-live="polite" aria-label="Loading"> <Lottie animationData={arioLoading} loop={true} style={{ width: size, height: size }} /> </div> );src/components/indicators/InterruptedWorkflowIndicator/InterruptedWorkflowIndicator.tsx (1)
23-47: Make the tooltip trigger keyboard-focusable.The tooltip trigger is a plain
<div>; keyboard users may not be able to focus it and see the tooltip. Consider addingtabIndex=0and anaria-labelto the indicator wrapper.♿ Proposed tweak
- icon={ - <div className={`interrupted-workflow-indicator ${className}`}> + icon={ + <div + className={`interrupted-workflow-indicator ${className}`} + tabIndex={0} + aria-label={`Interrupted workflow for ${domainName}`} + > <AlertTriangle className={`${sizeClasses[size]} text-error animate-pulse`} /> <div className="red-ping-dot" /> </div> }src/components/indicators/InterruptedWorkflowIndicator/InterruptedWorkflowIndicator.css (1)
3-14: Move transition to the base state for smooth hover-out.🎨 Proposed tweak
.interrupted-workflow-indicator { position: relative; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; + transition: transform 0.2s ease-in-out; } .interrupted-workflow-indicator:hover { transform: scale(1.1); - transition: transform 0.2s ease-in-out; }src/hooks/useMarketplaceOrder.tsx (1)
6-42: Type safety concern:marketplaceContractmay be undefined from global state.The
buildMarketplaceOrderQueryfunction signature requiresmarketplaceContractto beArNSMarketplaceRead | ArNSMarketplaceWrite, but when called fromuseMarketplaceOrder, the value comes from global state which may beundefined. While theenabledflag prevents the query from running whenmarketplaceContractis falsy, TypeScript won't catch this mismatch.🔧 Suggested fix
export function buildMarketplaceOrderQuery({ antId, marketplaceContract, marketplaceProcessId, aoNetwork, }: { antId?: string; - marketplaceContract: ArNSMarketplaceRead | ArNSMarketplaceWrite; + marketplaceContract?: ArNSMarketplaceRead | ArNSMarketplaceWrite; marketplaceProcessId: string; aoNetwork: typeof NETWORK_DEFAULTS.AO; }) {src/components/pages/ViewListing/ViewListing.tsx (3)
288-311: Polling loop has a finite retry limit but lacks exponential backoff.The deposit confirmation polling uses a fixed 5-second interval with 10 retries (50 seconds total). Consider:
- Adding exponential backoff to reduce load on the marketplace contract
- The error inside the loop is caught but only logged, which is appropriate for retries
The current implementation is functional but could benefit from backoff for production resilience.
443-446: Hardcoded sleep for cancellation processing.The 10-second sleep to wait for cancellation processing is a magic number. Consider extracting this to a named constant for clarity and easier adjustment.
🔧 Suggested improvement
+const CANCEL_PROCESSING_DELAY_MS = 10_000; + // Wait for the cancellation to be processed/cranked -await sleep(10_000); +await sleep(CANCEL_PROCESSING_DELAY_MS);
693-702: Unsafe type assertion withas any.The
endTimestampaccess usesas anyto bypass TypeScript. This could mask potential runtime errors if the type structure changes.🔧 Suggested improvement
Consider defining a proper type guard or interface for lease records:
<LeaseDurationFromEndTimestamp endTimestamp={ domainInfo?.arnsRecord?.type === 'lease' - ? (domainInfo.arnsRecord as any)?.endTimestamp + ? 'endTimestamp' in domainInfo.arnsRecord + ? domainInfo.arnsRecord.endTimestamp + : undefined : undefined } />src/hooks/useMarketplaceOrders.ts (3)
35-44: Use ofanytype reduces type safety.Both
allOrdersandresultuseanytype, which bypasses TypeScript's type checking. Consider using proper types from the SDK.🔧 Suggested improvement
+import { Order } from '@ar.io/sdk/web'; + // Inside queryFn: -const allOrders: any[] = []; +const allOrders: Order[] = []; // ... -const result: any = await marketplaceContract.getPaginatedOrders({ +const result = await marketplaceContract.getPaginatedOrders({Please verify the return type of
getPaginatedOrdersfrom the SDK to ensure proper typing.
38-57: Pagination fetches all pages in a single query execution.The current implementation fetches all pages up to the limit within one
queryFncall. For large datasets, this could:
- Cause long loading times
- Make many sequential network requests
Consider whether true cursor-based pagination at the component level would be more appropriate for large order lists.
59-64:totalCountreflects fetched count, not actual total.The returned
totalCountisallOrders.length, which is the number of items fetched, not necessarily the total available. Consider renaming tofetchedCountor documenting this behavior if the marketplace doesn't provide a true total count.src/components/inputs/DateTimePicker/DateTimePicker.tsx (1)
10-10: UnusedminDateprop.The
minDateprop is declared in the interface but never used in the component. The date validation (Line 96-105) uses hardcoded logic for "today to 30 days from now" instead.🔧 Either use minDate or remove it
If you want to use
minDate:-const isDateDisabled = (date: Date) => { - const today = new Date(); - today.setHours(0, 0, 0, 0); +const isDateDisabled = (date: Date) => { + const minDateTime = minDate || new Date(); + minDateTime.setHours(0, 0, 0, 0); // ... - return date < today || date > thirtyDaysFromNow; + return date < minDateTime || date > thirtyDaysFromNow; };Or remove from interface if intentionally unused:
interface DateTimePickerProps { value?: Date; onChange: (date: Date | undefined) => void; placeholder?: string; - minDate?: Date; className?: string; }src/components/modals/ListNameForSaleModal/panels/FixedPricePanel.tsx (3)
15-15: UnusedantIdprop.The
antIdprop is declared inFixedPricePanelPropsbut never used in the component.🔧 Remove unused prop
interface FixedPricePanelProps { domainName: string; - antId?: string; listingPrice: number; // ... }
100-102: Duplicate mARIO-to-ARIO conversion logic.The conversion
domainPrices.buy / 1000000appears multiple times. The initial effect (Line 50) usesmARIOToken.toARIO()but the display logic uses direct division. Consider using the SDK's conversion consistently.🔧 Extract to a computed value
+const marketPriceInARIO = domainPrices?.buy + ? new mARIOToken(domainPrices.buy).toARIO().valueOf() + : 0; + const marketPriceDisplay = domainPrices?.buy - ? `Market Value: ${formatARIOWithCommas(Math.ceil(domainPrices.buy / 1000000))} ${arioTicker}` + ? `Market Value: ${formatARIOWithCommas(Math.ceil(marketPriceInARIO))} ${arioTicker}` : '';Then use
marketPriceInARIOin the display section as well.Also applies to: 129-133
196-208: Input clamping on every keystroke may frustrate users.When typing a value like "2000000", the input is clamped to 1000000 immediately as the user types. This could prevent users from easily correcting mistakes or entering values digit by digit.
Consider validating on blur or submit instead of clamping on every change.
🔧 Alternative: validate on blur
<input type="text" // ... onChange={(e) => { const val = e.target.value; if (isValidCustomFormat(val)) { - const numVal = Number(val); - const clampedVal = Math.min(numVal, MAX_LISTING_PRICE); - setCustomValue(clampedVal.toString()); - setListingPrice(clampedVal); + setCustomValue(val); + setListingPrice(Number(val)); setButtonSelected(undefined); - setCustomValueError( - isValidCustomAmount(clampedVal.toString()), - ); + setCustomValueError(isValidCustomAmount(val)); } }} + onBlur={(e) => { + const numVal = Number(e.target.value); + if (numVal > MAX_LISTING_PRICE) { + setCustomValue(MAX_LISTING_PRICE.toString()); + setListingPrice(MAX_LISTING_PRICE); + } + }} />src/state/actions/dispatchArIOInteraction.ts (1)
23-27: Remove unused import:NETWORK_DEFAULTS
NETWORK_DEFAULTSis imported but never used in this file. Remove it to keep imports clean.Proposed fix
-import { - APP_NAME, - NETWORK_DEFAULTS, - WRITE_OPTIONS, -} from '@src/utils/constants'; +import { APP_NAME, WRITE_OPTIONS } from '@src/utils/constants';src/hooks/useMarketplaceInfo.ts (1)
1-23: Confirm whethergetInfo()should be wallet-agnostic.The query is disabled unless
walletAddressis present and includes it in the key. IfgetInfo()is global, this blocks unauthenticated browsing and refetches on wallet changes. Consider removing walletAddress fromenabled/queryKeyif it isn’t required.♻️ Possible refactor if walletAddress isn’t needed
-import { useGlobalState, useWalletState } from '@src/state'; +import { useGlobalState } from '@src/state'; import { useQuery } from '@tanstack/react-query'; export function useMarketplaceInfo() { const [{ marketplaceContract, marketplaceProcessId, aoNetwork }] = useGlobalState(); - const [{ walletAddress }] = useWalletState(); return useQuery({ queryKey: [ 'marketplace-intents', marketplaceProcessId, - walletAddress, aoNetwork.ARIO, ], queryFn: () => { return marketplaceContract.getInfo(); }, enabled: !!marketplaceContract && !!marketplaceProcessId && - !!walletAddress && !!aoNetwork.ARIO, staleTime: 24 * 60 * 60 * 1000, // 24 hours }); }src/components/pages/Settings/devtools/MarketplaceTools.tsx (1)
23-70: Avoid silent no-op when version input is invalid.
Consider disabling the Update button (or showing inline validation) so users get immediate feedback.Suggested tweak
function MarketplaceTools() { const { data: marketplaceInfo } = useMarketplaceInfo(); const [{ minimumANTVersionForMarketplace }, dispatchGlobalState] = useGlobalState(); const [expanded, setExpanded] = React.useState(false); const [newVersion, setNewVersion] = React.useState( minimumANTVersionForMarketplace.toString(), ); + const parsedVersion = Number(newVersion); + const isValidVersion = + Number.isInteger(parsedVersion) && parsedVersion > 0; const handleVersionUpdate = () => { - const version = parseInt(newVersion, 10); - if (!isNaN(version) && version > 0) { + if (isValidVersion) { dispatchGlobalState({ type: 'setMinimumANTVersionForMarketplace', - payload: version, + payload: parsedVersion, }); } }; @@ <button onClick={handleVersionUpdate} + disabled={!isValidVersion} className="text-white border border-primary-thin rounded px-2 py-1 text-xs hover:bg-primary-thin transition-all" > Update </button>src/components/data-display/tables/MarketplaceListingsTable.tsx (1)
81-118: Consider scoping ArNS record fetches to listed ANTs.
useArNSRecords()without filters will pull the full registry and can be heavy on the marketplace page. Consider deriving ANT IDs fromordersDataand passing filters (or a dedicated query) to fetch only the needed records.src/hooks/useANTIntent.ts (1)
41-46: Defensively handle missing intents.If
userAssets.intentsis ever undefined, Line 44 will throw. Defaulting to an empty array keeps the hook resilient.🛡️ Proposed fix
- const intents = userAssets.intents; + const intents = userAssets.intents ?? [];src/state/actions/dispatchArNSUpdate.ts (1)
124-124: Remove debug console.log statement.This debug logging statement should be removed before merging to production.
🔧 Suggested fix
- console.log('marketplaceUserDomains', userDomains);src/components/modals/ListNameForSaleModal/ListNameForSaleModal.tsx (4)
593-596: Inconsistent query invalidation pattern.The invalidation predicate only checks the first element of
queryKey, while the reset patterns (lines 510-524) use.some()to check all elements. This could miss queries where 'marketplace' appears in later key positions.🔧 Suggested fix
queryClient.invalidateQueries({ predicate: (query) => - query.queryKey?.[0]?.toString().includes('marketplace') ?? false, + query.queryKey.some( + (key: unknown) => + typeof key === 'string' && key.includes('marketplace'), + ), });
779-796: Dead code: Success panel is never displayed.The
panelStateis never set to'success'in the component. The workflow completion is handled by theProcessTransactionPanelwithisCompleteprop instead. Consider removing this unused panel.
123-125: Clarify default expiration comment and consider UX.The comment says "current day" but the default is 1 hour. Also, 1 hour may be unexpectedly short for users listing names for sale. Consider a longer default (e.g., 24 hours or 7 days) for better UX.
🔧 Suggested fix
const [expirationDate, setExpirationDate] = useState<Date | undefined>( - new Date(Date.now() + 60 * 60 * 1000), // Default to 1 hour from now (current day) + new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Default to 7 days from now );
300-308: Consider adding close confirmation for processing state.The confirmation dialog only appears on the 'confirm' panel. Users clicking close during 'processing' might not realize they're interrupting an in-progress transaction. Consider extending the confirmation to the processing state or disabling close during processing.
| "dependencies": { | ||
| "@ant-design/icons": "5.4.0", | ||
| "@ar.io/sdk": "^3.22.1", | ||
| "@ar.io/sdk": "^3.23.0-alpha.3", |
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
🌐 Web query:
@ar.io/sdk npm package latest version stable
💡 Result:
Latest stable version: 3.19.0 (checked Jan 20, 2026). [1][2]
Sources:
[1] npm package page for @ar.io/sdk.
[2] ar-io GitHub repository.
Replace alpha version with stable release or document justification.
@ar.io/sdk is pinned to ^3.23.0-alpha.3, a pre-release version. The latest stable release is 3.19.0. Alpha versions may introduce breaking changes, incomplete features, and instability in production. Either upgrade to 3.19.0 or document why the alpha version is required.
🤖 Prompt for AI Agents
In `@package.json` at line 27, The dependency "@ar.io/sdk" is pinned to a
pre-release alpha version; change the version string in package.json from
"^3.23.0-alpha.3" to the stable "3.19.0" and run your package manager to update
lockfiles (npm install / yarn install) and CI artifacts, or if the alpha is
required, add a short justification comment in the repo (e.g., in README or
DEPENDENCIES.md) and include a package.json comment (or a maintainer note)
explaining why "@ar.io/sdk" must remain at "^3.23.0-alpha.3" and any
risks/required follow-ups so reviewers understand the rationale.
| const namesExceedingLimit = useMemo(() => { | ||
| if (!domains || !ants) return 0; | ||
| return Object.values(domains).reduce( | ||
| (acc: number, record: AoArNSNameData) => { | ||
| const ant = ants[record.processId]; | ||
| if ( | ||
| ant?.state?.Records && | ||
| Object.keys(ant.state.Records).length > 10000 | ||
| ) { | ||
| acc++; | ||
| } | ||
| return acc; | ||
| }, | ||
| 0, |
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.
Undername-limit notification uses a hard-coded 10,000 instead of the per-domain limit.
This bypasses record.undernameLimit and will miss valid violations.
🔧 Suggested fix
return Object.values(domains).reduce(
(acc: number, record: AoArNSNameData) => {
const ant = ants[record.processId];
- if (
- ant?.state?.Records &&
- Object.keys(ant.state.Records).length > 10000
- ) {
+ const undernameCount = Object.keys(
+ ant?.state?.Records ?? {},
+ ).filter((key) => key !== '@').length;
+ if (undernameCount > record.undernameLimit) {
acc++;
}
return acc;
},
0,
);🤖 Prompt for AI Agents
In `@src/components/layout/Navbar/NotificationMenu/NotificationMenu.tsx` around
lines 182 - 195, The namesExceedingLimit calculation in namesExceedingLimit
(inside the useMemo) incorrectly uses the hard-coded threshold 10000; update the
reduce callback to compare Object.keys(ant.state.Records).length against the
domain-specific limit record.undernameLimit (falling back to a sensible default
if undernameLimit is undefined) instead of 10000 so each AoArNSNameData record's
per-domain limit is honored when computing violations.
| const retries = 0; | ||
| const maxRetries = 10; | ||
| while (retries < maxRetries) { | ||
| try { | ||
| const _res = await marketplaceContract.getUserAssets({ | ||
| address: walletAddress.toString(), | ||
| arioProcessId, | ||
| }); | ||
| } catch { | ||
| console.error('Issue retrieving marketplace assets'); | ||
| } | ||
| } |
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 the infinite retry loop when polling marketplace assets.
retries is const and never increments, so this loop never terminates and can hammer the network/UI indefinitely.
🐛 Proposed fix
- const retries = 0;
+ let retries = 0;
const maxRetries = 10;
while (retries < maxRetries) {
try {
const _res = await marketplaceContract.getUserAssets({
address: walletAddress.toString(),
arioProcessId,
});
} catch {
console.error('Issue retrieving marketplace assets');
}
+ retries += 1;
+ await new Promise((resolve) => setTimeout(resolve, 1000));
}📝 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 retries = 0; | |
| const maxRetries = 10; | |
| while (retries < maxRetries) { | |
| try { | |
| const _res = await marketplaceContract.getUserAssets({ | |
| address: walletAddress.toString(), | |
| arioProcessId, | |
| }); | |
| } catch { | |
| console.error('Issue retrieving marketplace assets'); | |
| } | |
| } | |
| let retries = 0; | |
| const maxRetries = 10; | |
| while (retries < maxRetries) { | |
| try { | |
| const _res = await marketplaceContract.getUserAssets({ | |
| address: walletAddress.toString(), | |
| arioProcessId, | |
| }); | |
| } catch { | |
| console.error('Issue retrieving marketplace assets'); | |
| } | |
| retries += 1; | |
| await new Promise((resolve) => setTimeout(resolve, 1000)); | |
| } |
🤖 Prompt for AI Agents
In `@src/components/modals/ContinueWorkflowModal/ContinueWorkflowModal.tsx` around
lines 184 - 195, In ContinueWorkflowModal, the polling loop around
marketplaceContract.getUserAssets uses a const retries and never increments,
causing an infinite loop; change retries to a let, increment it inside the while
loop, break out of the loop on successful fetch (when _res is received), and on
failure log the error and optionally await a short delay/backoff before
retrying; ensure you still respect maxRetries and handle the final failure case
(e.g., surface an error state) using symbols walletAddress, arioProcessId,
marketplaceContract.getUserAssets, retries, and maxRetries to locate the code.
| let newARIOBalance = 0; | ||
| let tries = 0; | ||
| const maxTries = 10; | ||
| while (newARIOBalance < feeDetails.listingFee) { | ||
| try { | ||
| const userBalanceResult = | ||
| await writeMarketplaceContract.getMarketplaceBalance({ | ||
| address: walletAddress.toString(), | ||
| }); | ||
|
|
||
| newARIOBalance = new mARIOToken( | ||
| Number(userBalanceResult?.balance ?? 0), | ||
| ) | ||
| .toARIO() | ||
| .valueOf(); | ||
| console.log({ | ||
| newARIOBalance, | ||
| listingFee: feeDetails.listingFee, | ||
| tries, | ||
| maxTries, | ||
| }); | ||
| if ( | ||
| tries >= maxTries || | ||
| newARIOBalance <= feeDetails.listingFee | ||
| ) { | ||
| throw new Error('Failed to deposit enough ARIO'); | ||
| } | ||
| } catch (error) { | ||
| tries++; | ||
| console.error(error); | ||
| if (tries >= maxTries) { | ||
| throw error; | ||
| } | ||
| // allow for cranking time for message passing | ||
| await sleep(7000); | ||
| } | ||
| } |
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.
Logic error in deposit verification loop.
Line 403 throws an error when newARIOBalance <= feeDetails.listingFee, but an equal balance should be sufficient. This causes the deposit to fail when the balance exactly matches the required fee.
🐛 Proposed fix
if (
tries >= maxTries ||
- newARIOBalance <= feeDetails.listingFee
+ newARIOBalance < feeDetails.listingFee
) {
+ if (tries >= maxTries && newARIOBalance < feeDetails.listingFee) {
throw new Error('Failed to deposit enough ARIO');
+ }
}Or simplify:
- if (
- tries >= maxTries ||
- newARIOBalance <= feeDetails.listingFee
- ) {
- throw new Error('Failed to deposit enough ARIO');
- }
+ if (tries >= maxTries && newARIOBalance < feeDetails.listingFee) {
+ throw new Error('Failed to deposit enough ARIO');
+ }📝 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.
| let newARIOBalance = 0; | |
| let tries = 0; | |
| const maxTries = 10; | |
| while (newARIOBalance < feeDetails.listingFee) { | |
| try { | |
| const userBalanceResult = | |
| await writeMarketplaceContract.getMarketplaceBalance({ | |
| address: walletAddress.toString(), | |
| }); | |
| newARIOBalance = new mARIOToken( | |
| Number(userBalanceResult?.balance ?? 0), | |
| ) | |
| .toARIO() | |
| .valueOf(); | |
| console.log({ | |
| newARIOBalance, | |
| listingFee: feeDetails.listingFee, | |
| tries, | |
| maxTries, | |
| }); | |
| if ( | |
| tries >= maxTries || | |
| newARIOBalance <= feeDetails.listingFee | |
| ) { | |
| throw new Error('Failed to deposit enough ARIO'); | |
| } | |
| } catch (error) { | |
| tries++; | |
| console.error(error); | |
| if (tries >= maxTries) { | |
| throw error; | |
| } | |
| // allow for cranking time for message passing | |
| await sleep(7000); | |
| } | |
| } | |
| let newARIOBalance = 0; | |
| let tries = 0; | |
| const maxTries = 10; | |
| while (newARIOBalance < feeDetails.listingFee) { | |
| try { | |
| const userBalanceResult = | |
| await writeMarketplaceContract.getMarketplaceBalance({ | |
| address: walletAddress.toString(), | |
| }); | |
| newARIOBalance = new mARIOToken( | |
| Number(userBalanceResult?.balance ?? 0), | |
| ) | |
| .toARIO() | |
| .valueOf(); | |
| console.log({ | |
| newARIOBalance, | |
| listingFee: feeDetails.listingFee, | |
| tries, | |
| maxTries, | |
| }); | |
| if (tries >= maxTries && newARIOBalance < feeDetails.listingFee) { | |
| throw new Error('Failed to deposit enough ARIO'); | |
| } | |
| } catch (error) { | |
| tries++; | |
| console.error(error); | |
| if (tries >= maxTries) { | |
| throw error; | |
| } | |
| // allow for cranking time for message passing | |
| await sleep(7000); | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@src/components/modals/ListNameForSaleModal/ListNameForSaleModal.tsx` around
lines 380 - 416, The loop in ListNameForSaleModal.tsx incorrectly treats an
equal ARIO balance as insufficient: inside the while loop (in the block that
checks tries and balance) change the condition that currently throws on
newARIOBalance <= feeDetails.listingFee to only throw when newARIOBalance <
feeDetails.listingFee (so equality is accepted); specifically update the
conditional in the try block used with
writeMarketplaceContract.getMarketplaceBalance and mARIOToken conversion so it
only errors when tries >= maxTries OR newARIOBalance < feeDetails.listingFee,
keeping the outer loop while (newARIOBalance < feeDetails.listingFee) logic
intact.
| const orderSummary: Record<string, ReactNode> = { | ||
| 'Listing Type': getListingTypeLabel(), | ||
| 'Domain Name': domainName, | ||
| 'Current Owner': | ||
| walletAddress?.toString().slice(0, 8) + | ||
| '...' + | ||
| walletAddress?.toString().slice(-8), | ||
| Expires: expirationDate | ||
| ? expirationDate.toLocaleDateString('en-US', { | ||
| weekday: 'short', | ||
| year: 'numeric', | ||
| month: 'short', | ||
| day: 'numeric', | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| }) | ||
| : 'Not set', |
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 missing walletAddress in owner display.
Line 77 calls .slice() on walletAddress?.toString() without guarding the result; if the wallet isn’t connected yet, this will throw and break render.
✅ Proposed fix
+ const ownerDisplay = walletAddress
+ ? `${walletAddress.toString().slice(0, 8)}...${walletAddress
+ .toString()
+ .slice(-8)}`
+ : domainInfo?.owner ?? 'N/A';
+
const orderSummary: Record<string, ReactNode> = {
'Listing Type': getListingTypeLabel(),
'Domain Name': domainName,
- 'Current Owner':
- walletAddress?.toString().slice(0, 8) +
- '...' +
- walletAddress?.toString().slice(-8),
+ 'Current Owner': ownerDisplay,
Expires: expirationDate
? expirationDate.toLocaleDateString('en-US', {🤖 Prompt for AI Agents
In `@src/components/modals/ListNameForSaleModal/panels/ConfirmListingPanel.tsx`
around lines 74 - 90, The 'Current Owner' entry in orderSummary in
ConfirmListingPanel uses walletAddress?.toString().slice(...) which can call
.slice on undefined and crash rendering; update the code that builds
orderSummary (or create a small helper like ownerDisplay) to check for a present
walletAddress/toString() result before slicing and provide a safe fallback
string (e.g., 'Not connected' or '—') when missing, ensuring the 'Current Owner'
value never calls .slice on undefined.
| // Check if there's a pending intent for this ANT | ||
| const antIntent = intents.find( | ||
| (intent: MarketplaceIntent) => intent.antProcessId === antId, | ||
| ); |
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.
Filter intents by pending status.
Without filtering, completed intents can be treated as interrupted workflows.
✅ Suggested fix
- const antIntent = intents.find(
- (intent: MarketplaceIntent) => intent.antProcessId === antId,
- );
+ const antIntent = intents.find(
+ (intent: MarketplaceIntent) =>
+ intent.antProcessId === antId && intent.status === 'pending',
+ );🤖 Prompt for AI Agents
In `@src/hooks/useInterruptedWorkflows.ts` around lines 68 - 71, The current
lookup for antIntent uses intents.find by antProcessId but doesn't ensure the
intent is still pending, causing completed intents to be treated as interrupted;
update the find predicate in useInterruptedWorkflows (where antIntent is
computed) to also check the intent's pending status (e.g., intent.status ===
'PENDING' or the project's equivalent flag on MarketplaceIntent) so only
active/pending intents are matched by antProcessId.
| queryKey: [ | ||
| 'marketplace-user-assets', | ||
| address, | ||
| marketplaceProcessId, | ||
| aoNetwork.ARIO, | ||
| ], | ||
| queryFn: () => { | ||
| if (!address) throw new Error('No address provided to fetch assets'); | ||
| return marketplaceContract | ||
| .getUserAssets({ address, arioProcessId }) | ||
| .catch((error) => { | ||
| console.error('Error fetching marketplace user assets', error); | ||
| throw error; | ||
| }); | ||
| }, | ||
| enabled: | ||
| !!marketplaceContract && | ||
| !!marketplaceProcessId && | ||
| !!address && | ||
| !!aoNetwork.ARIO, |
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:
find . -type f -name "useMarketplaceUserAssets.ts" 2>/dev/nullRepository: ar-io/arns-react
Length of output: 176
🏁 Script executed:
find . -type f -name "*marketplaceContract*" -o -name "*marketplace*" | grep -E "\.(ts|tsx)$" | head -20Repository: ar-io/arns-react
Length of output: 42
🏁 Script executed:
fd useMarketplaceUserAssetsRepository: ar-io/arns-react
Length of output: 97
🏁 Script executed:
cat -n src/hooks/useMarketplaceUserAssets.tsRepository: ar-io/arns-react
Length of output: 2218
🏁 Script executed:
rg "getUserAssets" -B 5 -A 10 src/hooks/useMarketplaceUserAssets.tsRepository: ar-io/arns-react
Length of output: 532
🏁 Script executed:
rg "arioProcessId" src/hooks/useMarketplaceUserAssets.ts -B 2 -A 2Repository: ar-io/arns-react
Length of output: 877
🏁 Script executed:
rg "queryKey:.*\[" src/hooks/ -A 4 | rg -v "arioProcessId" | head -30Repository: ar-io/arns-react
Length of output: 1523
🏁 Script executed:
rg "\.getUserAssets|\.getOrders|\.getOrder|\.getMarketplaceInfo" src/hooks/ -B 2 -A 5Repository: ar-io/arns-react
Length of output: 1296
🏁 Script executed:
rg "arioProcessId" src/hooks/useMarketplaceOrders.ts src/hooks/useMarketplaceOrder.tsx src/hooks/useMarketplaceInfo.ts -B 3 -A 3Repository: ar-io/arns-react
Length of output: 42
🏁 Script executed:
cat -n src/hooks/useMarketplaceOrders.ts | head -50Repository: ar-io/arns-react
Length of output: 1940
🏁 Script executed:
cat -n src/hooks/useMarketplaceOrder.tsx | head -50Repository: ar-io/arns-react
Length of output: 1872
🏁 Script executed:
cat -n src/hooks/useMarketplaceInfo.ts | head -50Repository: ar-io/arns-react
Length of output: 920
Include arioProcessId in the query key and enabled checks.
The query uses arioProcessId in getUserAssets() but it's missing from the queryKey and enabled conditions. If arioProcessId changes, React Query will serve stale cached results. Additionally, without an enabled guard, the query can execute with an undefined value.
Suggested fix
queryKey: [
'marketplace-user-assets',
address,
marketplaceProcessId,
+ arioProcessId,
aoNetwork.ARIO,
],
@@
enabled:
!!marketplaceContract &&
!!marketplaceProcessId &&
+ !!arioProcessId &&
!!address &&
!!aoNetwork.ARIO,📝 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.
| queryKey: [ | |
| 'marketplace-user-assets', | |
| address, | |
| marketplaceProcessId, | |
| aoNetwork.ARIO, | |
| ], | |
| queryFn: () => { | |
| if (!address) throw new Error('No address provided to fetch assets'); | |
| return marketplaceContract | |
| .getUserAssets({ address, arioProcessId }) | |
| .catch((error) => { | |
| console.error('Error fetching marketplace user assets', error); | |
| throw error; | |
| }); | |
| }, | |
| enabled: | |
| !!marketplaceContract && | |
| !!marketplaceProcessId && | |
| !!address && | |
| !!aoNetwork.ARIO, | |
| queryKey: [ | |
| 'marketplace-user-assets', | |
| address, | |
| marketplaceProcessId, | |
| arioProcessId, | |
| aoNetwork.ARIO, | |
| ], | |
| queryFn: () => { | |
| if (!address) throw new Error('No address provided to fetch assets'); | |
| return marketplaceContract | |
| .getUserAssets({ address, arioProcessId }) | |
| .catch((error) => { | |
| console.error('Error fetching marketplace user assets', error); | |
| throw error; | |
| }); | |
| }, | |
| enabled: | |
| !!marketplaceContract && | |
| !!marketplaceProcessId && | |
| !!arioProcessId && | |
| !!address && | |
| !!aoNetwork.RIO, |
🤖 Prompt for AI Agents
In `@src/hooks/useMarketplaceUserAssets.ts` around lines 20 - 39, The query uses
arioProcessId in marketplaceContract.getUserAssets(...) but omits it from the
React Query cache key and enabled guard; update the queryKey array (the
'marketplace-user-assets' key) to include arioProcessId and add arioProcessId to
the enabled condition so the hook only runs when marketplaceContract,
marketplaceProcessId, address, aoNetwork.ARIO, and arioProcessId are all truthy,
ensuring cache invalidation and preventing calls with an undefined
arioProcessId.
| if (!ant?.state?.Owner) return; | ||
|
|
||
| // Skip if ANT is owned by marketplace (those are handled by interrupted workflows) | ||
| if (ant.state.Owner === marketplaceProcessId) return; | ||
|
|
||
| // Check if there's a pending intent for this ANT with 'pending' status | ||
| const antIntent = intents.find( | ||
| (intent: MarketplaceIntent) => | ||
| intent.antProcessId === antId && intent.status === 'pending', | ||
| ); |
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.
Pending workflow detection should require user ownership.
The hook’s definition says the ANT must still be owned by the user, but the code only excludes marketplace ownership. This can surface workflows for domains no longer owned by the user.
✅ Suggested fix
- const [{ marketplaceProcessId }] = useGlobalState();
+ const [{ marketplaceProcessId }] = useGlobalState();
+ const userAddress = walletAddress.toString();
...
- if (!ant?.state?.Owner) return;
-
- // Skip if ANT is owned by marketplace (those are handled by interrupted workflows)
- if (ant.state.Owner === marketplaceProcessId) return;
+ if (!ant?.state?.Owner) return;
+ if (ant.state.Owner !== userAddress) return;🤖 Prompt for AI Agents
In `@src/hooks/usePendingWorkflows.ts` around lines 54 - 63, The code currently
only excludes marketplace ownership; update usePendingWorkflows to require the
ANT be owned by the current user by replacing the marketplace-only check with an
explicit equality check against the current process id (e.g., if
(ant.state.Owner !== processId) return;), so the hook returns early unless
ant.state.Owner equals the user's process id; keep the existing
marketplaceProcessId symbol in mind when locating the code and ensure
intents.find still filters by intent.antProcessId === antId && intent.status ===
'pending'.
| export const MARKETPLACE_PROCESS_ID = | ||
| import.meta.env.VITE_MARKETPLACE_PROCESS_ID || | ||
| 'ijCygKzV48PHx_TUdFN7NNJgbBUatQDL_h8FfqVzXfE'; |
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:
rg -n "VITE_MARKETPLACE_PROCESS_ID|MARKETPLACE_PROCESS_ID" -SRepository: ar-io/arns-react
Length of output: 478
🏁 Script executed:
find . -maxdepth 2 -type f \( -name ".env*" -o -name "*.config.*" \) | head -20Repository: ar-io/arns-react
Length of output: 142
🏁 Script executed:
git ls-files | grep -E "(\.env|config|README)" | head -30Repository: ar-io/arns-react
Length of output: 230
🏁 Script executed:
cat vite.config.tsRepository: ar-io/arns-react
Length of output: 1692
🏁 Script executed:
head -100 README.mdRepository: ar-io/arns-react
Length of output: 1902
🏁 Script executed:
sed -n '30,110p' src/state/contexts/GlobalState.tsxRepository: ar-io/arns-react
Length of output: 3055
🏁 Script executed:
find . -maxdepth 3 -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".github" \) | head -20Repository: ar-io/arns-react
Length of output: 208
🏁 Script executed:
cat .github/workflows/production.ymlRepository: ar-io/arns-react
Length of output: 4075
🏁 Script executed:
cat .github/workflows/staging_deploy.ymlRepository: ar-io/arns-react
Length of output: 3009
🏁 Script executed:
rg -i "ijCygKzV48PHx_TUdFN7NNJgbBUatQDL_h8FfqVzXfE" -B 3 -A 3Repository: ar-io/arns-react
Length of output: 439
🏁 Script executed:
rg -i "marketplace.*process.*id|process.*id.*marketplace" -B 2 -A 2 --type ts --type tsx | head -60Repository: ar-io/arns-react
Length of output: 87
🏁 Script executed:
rg -i "testnet|devnet|process.*id" src/utils/constants.ts -B 1 -A 1Repository: ar-io/arns-react
Length of output: 937
🏁 Script executed:
rg "PROCESS_ID" src/utils/constants.tsRepository: ar-io/arns-react
Length of output: 396
🏁 Script executed:
cat src/utils/constants.tsRepository: ar-io/arns-react
Length of output: 13753
Marketplace process ID lacks environment-specific configuration unlike other process IDs.
The hard-coded fallback 'ijCygKzV48PHx_TUdFN7NNJgbBUatQDL_h8FfqVzXfE' is used in all environments since VITE_MARKETPLACE_PROCESS_ID is never set in CI/CD (production.yml, staging_deploy.yml). This differs from ARIO_PROCESS_ID (overridable via CI/CD) and ANT_REGISTRY_PROCESS_ID (has testnet variant). Confirm this value is correct for mainnet and any other required environments, or add testnet/staging variants and environment-based configuration matching the pattern used for other process IDs.
🤖 Prompt for AI Agents
In `@src/utils/constants.ts` around lines 103 - 105, The MARKETPLACE_PROCESS_ID
constant currently falls back to a hard-coded mainnet ID and is not configurable
per environment; update MARKETPLACE_PROCESS_ID (in src/utils/constants.ts) to
follow the same environment-based pattern as ARIO_PROCESS_ID and
ANT_REGISTRY_PROCESS_ID by allowing environment overrides and adding explicit
variants (e.g., testnet/staging and mainnet) or ensure the fallback value is the
correct mainnet ID used in production; modify the code to read an env-specific
variable (VITE_MARKETPLACE_PROCESS_ID) and, if needed, add additional env keys
or conditional selection logic so CI/CD can supply the appropriate ID for
staging/testnet and production.
| export function isValidNetworkURL(url: string): boolean { | ||
| if (!url) return false; | ||
|
|
||
| try { | ||
| // Try to parse as URL to validate structure | ||
| const urlObj = new URL(url); | ||
|
|
||
| // Allow http and https protocols | ||
| if (!['http:', 'https:'].includes(urlObj.protocol)) { | ||
| return false; | ||
| } | ||
|
|
||
| // Validate hostname (can be domain, IP, or localhost) | ||
| const hostname = urlObj.hostname; | ||
|
|
||
| // Allow localhost | ||
| if (hostname === 'localhost') return true; | ||
|
|
||
| // Allow IP addresses | ||
| const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; | ||
| if (ipRegex.test(hostname)) { | ||
| // Validate IP ranges (0-255) | ||
| const parts = hostname.split('.'); | ||
| return parts.every((part) => { | ||
| const num = parseInt(part, 10); | ||
| return num >= 0 && num <= 255; | ||
| }); | ||
| } | ||
|
|
||
| // Allow domain names | ||
| const domainRegex = | ||
| /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; | ||
| return domainRegex.test(hostname); | ||
| } catch { | ||
| // If URL constructor fails, fall back to regex | ||
| return URL_REGEX.test(url); | ||
| } | ||
| } |
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:
# First, locate and examine the searchUtils.ts file
find . -name "searchUtils.ts" -type f | head -20Repository: ar-io/arns-react
Length of output: 98
🏁 Script executed:
# Examine the file and understand the URL_REGEX and isValidNetworkURL function
cat -n src/utils/searchUtils/searchUtils.ts | grep -A 50 "isValidNetworkURL"Repository: ar-io/arns-react
Length of output: 1835
🏁 Script executed:
# Search for URL_REGEX definition
cat -n src/utils/searchUtils/searchUtils.ts | head -300Repository: ar-io/arns-react
Length of output: 10567
🏁 Script executed:
# Find the constants file
find . -name "constants*" -type f | grep -E "\.(ts|js)$" | head -10Repository: ar-io/arns-react
Length of output: 84
🏁 Script executed:
# Search for URL_REGEX definition
rg "URL_REGEX\s*=" --type ts --type js -A 2 | head -30Repository: ar-io/arns-react
Length of output: 260
🏁 Script executed:
# Read the full URL_REGEX definition
cat -n src/utils/constants.ts | grep -A 10 "URL_REGEX"Repository: ar-io/arns-react
Length of output: 472
🏁 Script executed:
# Search for usages of isValidNetworkURL
rg "isValidNetworkURL" --type ts --type js -B 2 -A 2Repository: ar-io/arns-react
Length of output: 4233
🏁 Script executed:
# Test URL_REGEX behavior with a sample scheme-less input to confirm
node << 'EOF'
const URL_REGEX = new RegExp(
'^((https?|ftp)://)?' +
'((([a-zA-Z\\d]([a-zA-Z\\d-]*[a-zA-Z\\d])*)\\.)+[a-zA-Z]{2,}|' +
'((\\d{1,3}\\.){3}\\d{1,3})|' +
'localhost)' +
'(\\:\\d+)?(\\/[-a-zA-Z\\d%_.~+]*)*' +
'(\\?[;&a-zA-Z\\d%_.~+=-]*)?' +
'(\\#[-a-zA-Z\\d_]*)?$',
'i',
);
// Test cases
console.log('Test: "example.com" (scheme-less):', URL_REGEX.test('example.com'));
console.log('Test: "https://example.com":', URL_REGEX.test('https://example.com'));
console.log('Test: "http://localhost:3000":', URL_REGEX.test('http://localhost:3000'));
console.log('Test: "192.168.1.1":', URL_REGEX.test('192.168.1.1'));
// Test URL() constructor behavior
console.log('\nURL() constructor behavior:');
try {
new URL('example.com');
console.log('example.com: Valid');
} catch (e) {
console.log('example.com: Invalid -', e.message);
}
try {
new URL('https://example.com');
console.log('https://example.com: Valid');
} catch (e) {
console.log('https://example.com: Invalid -', e.message);
}
EOFRepository: ar-io/arns-react
Length of output: 286
Enforce http(s) scheme before try/catch to prevent scheme-less URLs in fallback.
The fallback to URL_REGEX accepts scheme-less inputs (e.g., example.com) because the regex has an optional scheme group ((https?|ftp)://)?. When new URL(url) throws for scheme-less input, the catch block still validates it as true, allowing invalid network URLs into settings like CU_URL, MU_URL, hyperbeamUrl, and turboPaymentUrl. Add a pre-check to enforce the scheme upfront:
💡 Suggested hardening (enforce http/https)
export function isValidNetworkURL(url: string): boolean {
if (!url) return false;
+ if (!/^https?:\/\//i.test(url)) return false;
try {
// Try to parse as URL to validate structure
const urlObj = new URL(url);🤖 Prompt for AI Agents
In `@src/utils/searchUtils/searchUtils.ts` around lines 204 - 241, The
isValidNetworkURL function allows scheme-less inputs because the catch fallback
uses URL_REGEX that permits optional schemes; add an explicit pre-check at the
top of isValidNetworkURL to reject any string that does not start with "http://"
or "https://" (e.g., using /^https?:\/\//i.test(url)) before attempting new
URL(url), so scheme-less inputs return false and the catch fallback cannot
validate them; update logic in isValidNetworkURL and keep the existing URL_REGEX
only for validating syntactically correct URLs that already include the http(s)
scheme.
Summary by CodeRabbit
New Features
Bug Fixes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.