Skip to content

Conversation

@atticusofsparta
Copy link
Contributor

@atticusofsparta atticusofsparta commented Jan 20, 2026

Summary by CodeRabbit

  • New Features

    • Added ArNS Marketplace with domain listings and search functionality.
    • Enabled listing domains for sale with fixed pricing options.
    • Implemented domain purchase flow with balance management.
    • Added marketplace ARIO deposit and withdrawal capabilities.
    • Introduced domain upgrade feature for marketplace compatibility.
    • Added workflow status tracking and interrupted workflow recovery.
  • Bug Fixes

    • Improved network URL validation in settings.
    • Enhanced wallet signing stability.
  • Chores

    • Streamlined testing infrastructure and dependencies.
    • Updated development tooling configuration.

✏️ Tip: You can customize this high-level summary in your review settings.

@atticusofsparta atticusofsparta requested a review from a team as a code owner January 20, 2026 16:18
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Workflow & Build Config
.github/workflows/build_and_test.yml, .github/workflows/production.yml, .github/workflows/staging_deploy.yml
Renamed jobs, standardized yarn syntax to explicit --frozen-lockfile flag, added concurrency policies, removed Playwright test job entirely
Test Infrastructure Removal
babel.config.cjs, jest.config.json, jest-setup.ts, playwright.config.ts, __mocks__/react-router-dom.ts, tests/playwright/*, tests/unit/*, tests/common/*
Deleted entire Jest/Babel/Playwright test configuration files and test suites including mobile specs, setup scripts, and fixtures
Package & Documentation
package.json, README.md, biome.json, vite.config.ts
Switched package manager references from yarn to pnpm; removed test scripts; upgraded ar-io SDK; added marketplace dependencies (permaweb-deploy, ethers, date-fns); reduced test globals in biome.json; removed @tests alias
Marketplace Pages
src/components/pages/Marketplace/Marketplace.tsx, src/components/pages/ViewListing/ViewListing.tsx, src/components/pages/index.ts, src/App.tsx
Added two new marketplace pages with full buy/sell flows, route registration, balances display, and listings table integration
Marketplace Modals
src/components/modals/ListNameForSaleModal/*, src/components/modals/ManageMarketplaceARIOModal/*, src/components/modals/ant-management/UpgradeDomainForMarketplaceModal/*, src/components/modals/ContinueWorkflowModal/*, src/components/modals/index.ts
New modal suite for listing domains (fixed/dutch/english pricing), managing marketplace ARIO deposits/withdrawals, upgrading domains for compatibility, and continuing interrupted workflows
Marketplace Components & Indicators
src/components/cards/MarketplaceOrderInfoCard/*, src/components/data-display/VerticalTimelineStepper.tsx, src/components/indicators/ARIOLoadingSpinner.tsx, src/components/indicators/InterruptedWorkflowIndicator/*
Added order info display card, timeline stepper for workflows, loading spinner, and interrupted workflow indicator with visual alerts
Marketplace Hooks
src/hooks/useMarketplaceOrder.tsx, src/hooks/useMarketplaceOrders.ts, src/hooks/useMarketplaceUserAssets.ts, src/hooks/useMarketplaceInfo.ts, src/hooks/useANTIntent.ts, src/hooks/useInterruptedWorkflows.ts, src/hooks/usePendingWorkflows.ts, src/hooks/index.ts
Comprehensive hook suite for fetching marketplace orders, user assets, intents, and detecting interrupted/pending workflows with caching and pagination support
Marketplace Data Tables
src/components/data-display/tables/MarketplaceListingsTable.tsx, src/components/data-display/tables/DomainsTable.tsx, src/components/data-display/tables/TableView.tsx
New marketplace listings table with search/sort/filtering; extended DomainsTable with marketplace ownership, workflow indicators, and modal triggers; updated empty-state styling
Global State & Actions
src/state/contexts/GlobalState.tsx, src/state/reducers/GlobalReducer.ts, src/state/actions/dispatchArNSUpdate.ts, src/state/contexts/ArNSState.tsx
Extended global state with marketplaceProcessId, minimumANTVersionForMarketplace, and marketplaceContract; updated dispatchArNSUpdate to fetch and merge marketplace-owned domains; added marketplace-related action types
UI Integration
src/components/cards/NavMenuCard/NavMenuCard.tsx, src/components/layout/Navbar/NotificationMenu/NotificationMenu.tsx, src/components/layout/Notifications/Notifications.tsx, src/components/pages/Manage/Manage.tsx, src/components/pages/Register/Checkout.tsx
Added wallet balance display and turbo credit effects; integrated interrupted workflow notifications and continue modals; propagated marketplaceProcessId through update flows
Utilities & Configuration
src/utils/constants.ts, src/utils/routes.tsx, src/utils/searchUtils/searchUtils.ts, src/utils/common/common.tsx, src/utils/sentry.ts, src/pages/Settings/settingsConfig.ts, src/pages/Settings/devtools/*
Added marketplace constants (process ID, min ANT version), registered marketplace route with icon, added network URL validation, enhanced expiry date formatting, updated Sentry browser tracing API, added marketplace settings and dev tools
Wallet Services
src/services/wallets/EthWalletConnector.ts, src/services/wallets/WanderWalletConnector.ts
Added data signing guard for empty/falsy input; added type casts for permission handling in Wander wallet
Removed Test Files
src/components/inputs/buttons/ConnectButton/ConnectButton.test.tsx, src/components/inputs/text/ValidationInput/ValidationInput.test.tsx, src/components/layout/Navbar/NavBarLink/NavBarLink.test.tsx, src/components/layout/Navbar/NavGroup/NavGroup.test.tsx, src/components/modals/LogoUploadModal/__tests__/LogoUploadModal.test.tsx, src/utils/imageUtils.test.ts, src/utils/searchUtils/searchUtils.test.ts
Deleted unit test files; no production code changes in these lines

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • dtfiedler
  • kunstmusik

Poem

🐰 Hoppy Marketplace Times!

A marketplace blooms where domains trade,
With listings and sales freshly made,
Modals dance, hooks fetch with care,
Workflows interrupted—now continued there! ✨
The ANT hops proudly, now marketplace-bound! 🏪

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies a specific feature area (marketplace) and the nature of the work (feedback/improvements), directly reflecting the substantial marketplace-related changes throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

@github-actions
Copy link

Permaweb Preview Deployed

Your preview is available at: https://arns-react-pr-916_arns.ar.io

Property Value
Transaction ID uIv2sKrQO1ymmvyKnAu8DFrYiyvWtL6FvtdTgngMtwU
ArNS Name arns
Undername arns-react-pr-916

This preview will be available permanently on Arweave via the AR.IO Network.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 of safeWanderApiExecutor.

Unlike connect() which wraps getPermissions() with safeWanderApiExecutor (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 showMenu dependency 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 dependencies marketplaceProcessId and antRegistryProcessId to useEffect dependency array.

Both marketplaceProcessId and antRegistryProcessId are extracted from global state, passed to dispatchArNSUpdate, 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('') returns false, which sets isValid=false on the SettingInput component. This disables the "Set" button (line 77 of SettingInput.tsx). The onPressEnter handler also blocks empty input with its own validation guard. While the onSet payload logic supports clearing via undefined, 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 on ReactNode.toString() and will never match.

notification.message is a React element, so .toString() returns "[object Object]", meaning the "Interrupted" checks never fire. Add an explicit kind (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, and marketplaceProcessId but 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: Add MU_URL to marketplace contract initialization for consistency.

The defaultMarketplaceContract is missing MU_URL in its connect() configuration, while defaultArIO and other AO contracts throughout the codebase include both CU_URL and MU_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/browser as it is redundant with @sentry/react.

@sentry/react is a React-specific wrapper that re-exports the @sentry/browser API plus React-specific helpers (ErrorBoundary, Profiler, etc.). For a React application, only @sentry/react is 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 repeated Object.entries calls 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 when value prop changes externally.

The initial state for hour12, minutes, and period is only set once during component mount. If value prop 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-black on 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 luaSourceId is 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 signingMessage intact; 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 any casts throughout the file work around a type mismatch between getPermissions() return type and PermissionType. A cleaner approach is to cast permissions once at the source:

const permissions = await this.safeWanderApiExecutor(
  this._wallet?.getPermissions,
) as PermissionType[] | undefined;

This allows removing the individual as any casts in the includes() 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 endTimestamp is in seconds, but the implementation treats it as milliseconds—it's compared directly against Date.now() (milliseconds) and passed to formatDate(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 returns string | 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: isExpired is true during grace period, which is counterintuitive.

The variable isExpired evaluates to true for any domain past its endTimestamp, including those still in grace period. While the logic works because isGracePeriod is 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 isActive for the green color check instead of endTimestamp > Date.now() at line 194.

.github/workflows/production.yml (1)

89-100: Inconsistent Node.js version specification.

The publish job uses a hardcoded node-version: 20.x while other jobs (lint_and_build_check, build_for_production) use node-version-file: '.nvmrc'. This could lead to version drift if .nvmrc is 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 to build_and_lint.yml for consistency.

package.json (1)

64-65: Consider moving permaweb-deploy to devDependencies.

permaweb-deploy is only used in npm scripts (publish:arweave, deploy:arweave) and is not a runtime dependency. It should be in devDependencies to reduce production bundle concerns.

Suggested change

Move permaweb-deploy from dependencies to devDependencies:

  "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 an aria-label (or aria-hidden if 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 adding tabIndex=0 and an aria-label to 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: marketplaceContract may be undefined from global state.

The buildMarketplaceOrderQuery function signature requires marketplaceContract to be ArNSMarketplaceRead | ArNSMarketplaceWrite, but when called from useMarketplaceOrder, the value comes from global state which may be undefined. While the enabled flag prevents the query from running when marketplaceContract is 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:

  1. Adding exponential backoff to reduce load on the marketplace contract
  2. 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 with as any.

The endTimestamp access uses as any to 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 of any type reduces type safety.

Both allOrders and result use any type, 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 getPaginatedOrders from 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 queryFn call. For large datasets, this could:

  1. Cause long loading times
  2. 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: totalCount reflects fetched count, not actual total.

The returned totalCount is allOrders.length, which is the number of items fetched, not necessarily the total available. Consider renaming to fetchedCount or documenting this behavior if the marketplace doesn't provide a true total count.

src/components/inputs/DateTimePicker/DateTimePicker.tsx (1)

10-10: Unused minDate prop.

The minDate prop 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: Unused antId prop.

The antId prop is declared in FixedPricePanelProps but 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 / 1000000 appears multiple times. The initial effect (Line 50) uses mARIOToken.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 marketPriceInARIO in 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_DEFAULTS is 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 whether getInfo() should be wallet-agnostic.

The query is disabled unless walletAddress is present and includes it in the key. If getInfo() is global, this blocks unauthenticated browsing and refetches on wallet changes. Consider removing walletAddress from enabled/queryKey if 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 from ordersData and 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.intents is 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 panelState is never set to 'success' in the component. The workflow completion is handled by the ProcessTransactionPanel with isComplete prop 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",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.

Comment on lines +182 to +195
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +184 to +195
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');
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +380 to +416
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);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +74 to +90
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',
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against 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.

Comment on lines +68 to +71
// Check if there's a pending intent for this ANT
const antIntent = intents.find(
(intent: MarketplaceIntent) => intent.antProcessId === antId,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +20 to +39
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "useMarketplaceUserAssets.ts" 2>/dev/null

Repository: ar-io/arns-react

Length of output: 176


🏁 Script executed:

find . -type f -name "*marketplaceContract*" -o -name "*marketplace*" | grep -E "\.(ts|tsx)$" | head -20

Repository: ar-io/arns-react

Length of output: 42


🏁 Script executed:

fd useMarketplaceUserAssets

Repository: ar-io/arns-react

Length of output: 97


🏁 Script executed:

cat -n src/hooks/useMarketplaceUserAssets.ts

Repository: ar-io/arns-react

Length of output: 2218


🏁 Script executed:

rg "getUserAssets" -B 5 -A 10 src/hooks/useMarketplaceUserAssets.ts

Repository: ar-io/arns-react

Length of output: 532


🏁 Script executed:

rg "arioProcessId" src/hooks/useMarketplaceUserAssets.ts -B 2 -A 2

Repository: ar-io/arns-react

Length of output: 877


🏁 Script executed:

rg "queryKey:.*\[" src/hooks/ -A 4 | rg -v "arioProcessId" | head -30

Repository: ar-io/arns-react

Length of output: 1523


🏁 Script executed:

rg "\.getUserAssets|\.getOrders|\.getOrder|\.getMarketplaceInfo" src/hooks/ -B 2 -A 5

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

Repository: ar-io/arns-react

Length of output: 42


🏁 Script executed:

cat -n src/hooks/useMarketplaceOrders.ts | head -50

Repository: ar-io/arns-react

Length of output: 1940


🏁 Script executed:

cat -n src/hooks/useMarketplaceOrder.tsx | head -50

Repository: ar-io/arns-react

Length of output: 1872


🏁 Script executed:

cat -n src/hooks/useMarketplaceInfo.ts | head -50

Repository: 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.

Suggested change
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.

Comment on lines +54 to +63
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',
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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'.

Comment on lines +103 to +105
export const MARKETPLACE_PROCESS_ID =
import.meta.env.VITE_MARKETPLACE_PROCESS_ID ||
'ijCygKzV48PHx_TUdFN7NNJgbBUatQDL_h8FfqVzXfE';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "VITE_MARKETPLACE_PROCESS_ID|MARKETPLACE_PROCESS_ID" -S

Repository: ar-io/arns-react

Length of output: 478


🏁 Script executed:

find . -maxdepth 2 -type f \( -name ".env*" -o -name "*.config.*" \) | head -20

Repository: ar-io/arns-react

Length of output: 142


🏁 Script executed:

git ls-files | grep -E "(\.env|config|README)" | head -30

Repository: ar-io/arns-react

Length of output: 230


🏁 Script executed:

cat vite.config.ts

Repository: ar-io/arns-react

Length of output: 1692


🏁 Script executed:

head -100 README.md

Repository: ar-io/arns-react

Length of output: 1902


🏁 Script executed:

sed -n '30,110p' src/state/contexts/GlobalState.tsx

Repository: ar-io/arns-react

Length of output: 3055


🏁 Script executed:

find . -maxdepth 3 -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".github" \) | head -20

Repository: ar-io/arns-react

Length of output: 208


🏁 Script executed:

cat .github/workflows/production.yml

Repository: ar-io/arns-react

Length of output: 4075


🏁 Script executed:

cat .github/workflows/staging_deploy.yml

Repository: ar-io/arns-react

Length of output: 3009


🏁 Script executed:

rg -i "ijCygKzV48PHx_TUdFN7NNJgbBUatQDL_h8FfqVzXfE" -B 3 -A 3

Repository: 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 -60

Repository: ar-io/arns-react

Length of output: 87


🏁 Script executed:

rg -i "testnet|devnet|process.*id" src/utils/constants.ts -B 1 -A 1

Repository: ar-io/arns-react

Length of output: 937


🏁 Script executed:

rg "PROCESS_ID" src/utils/constants.ts

Repository: ar-io/arns-react

Length of output: 396


🏁 Script executed:

cat src/utils/constants.ts

Repository: 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.

Comment on lines +204 to +241
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);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the searchUtils.ts file
find . -name "searchUtils.ts" -type f | head -20

Repository: 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 -300

Repository: ar-io/arns-react

Length of output: 10567


🏁 Script executed:

# Find the constants file
find . -name "constants*" -type f | grep -E "\.(ts|js)$" | head -10

Repository: 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 -30

Repository: 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 2

Repository: 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);
}
EOF

Repository: 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.

@atticusofsparta atticusofsparta merged commit 1a86a36 into develop Jan 26, 2026
3 checks passed
@atticusofsparta atticusofsparta deleted the PE-8875-marketplace-feature-feedback branch January 26, 2026 17:27
atticusofsparta added a commit that referenced this pull request Jan 26, 2026
…e-feedback"

This reverts commit 1a86a36, reversing
changes made to c28b78f.
@atticusofsparta atticusofsparta restored the PE-8875-marketplace-feature-feedback branch January 26, 2026 17:32
@atticusofsparta atticusofsparta deleted the PE-8875-marketplace-feature-feedback branch January 26, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants