Skip to content

Implement portal version 2#7

Open
Ehsan-saradar wants to merge 27 commits intomainfrom
feat/dev-portal-v2
Open

Implement portal version 2#7
Ehsan-saradar wants to merge 27 commits intomainfrom
feat/dev-portal-v2

Conversation

@Ehsan-saradar
Copy link
Contributor

@Ehsan-saradar Ehsan-saradar commented Feb 24, 2026

Summary by CodeRabbit

  • New Features

    • Dashboard, Connect flow, Proposal management, Project onboarding, Plugins, Members, Earnings, Plugin-specific earnings pages, currency selection modal, and a 500 error page; refreshed responsive top navigation and many new icons.
  • Bug Fixes & Improvements

    • Redesigned earnings table, improved date display and truncation components, responsive layout tweaks, and default theme set to light.
  • Chores

    • Environment and storage key updates, package/tooling updates, ignored file addition, and removal of legacy mock/editor pages.

@vercel
Copy link

vercel bot commented Feb 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
developer-portal Ready Ready Preview, Comment Feb 25, 2026 0:31am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds AppProvider/AppContext and App-level auth flows; replaces plugin client with portal and third-party clients; introduces many new pages, layouts, hooks, icons, utilities, and storage APIs; removes legacy plugin-edit/invite/new-plugin flows and mock data; updates config, env vars, dependencies, and styling tokens.

Changes

Cohort / File(s) Summary
Config & Env
\.coderabbit\.yaml, .env.example, .gitignore, eslint.config.js, knip.json, tsconfig.node.json, vercel.json, vite.config.ts
Adds CodeRabbit config; renames/adds env vars (VITE_DEVELOPER_PORTAL_URL, VITE_VULTISIG_SERVER), small .gitignore addition, ESLint ignore/rules changes, knip/tsconfig tweaks, removed Vite node polyfills.
Package manifest
package.json
Scripts updated and many dependency/devDependency bumps, additions (knip, vitest, lottie-react, decimal.js, jwt-decode, etc.) and removals (lodash-es, uuid, etc.).
Build / HTML / Types
index.html, src/vite-env.d.ts, src/assets/logo.json
SEO/meta tags added, new Lottie asset added; ImportMetaEnv updated to require new env vars.
API layer
src/api/client.ts, src/api/portal.ts, src/api/third-party/*, removed src/api/plugins.ts
Introduces token-managed apiClient, camel/snake payload transforms, centralized 401 handling; adds portal client and third-party HTTP/crypto helpers; removes legacy plugins API module.
Providers & Contexts
src/providers/App.tsx, src/providers/Antd.tsx, src/providers/Core.tsx, src/context/App.tsx, src/context/Core.tsx
New AppProvider with vault/auth flows and AppContext; CoreContext refocused to UI state (currency, baseValue, currentRoute); Antd provider styling extended.
Routing & App entry
src/App.tsx, src/Routes.tsx, src/utils/routes.ts, src/main.tsx
AppProvider wrapped into app; routes expanded (Dashboard, Connect, proposals, project pages, plugin earnings/members); ProtectedRoute and route current-route tracking added; routeTree updated.
Layouts
src/layouts/Auth.tsx, src/layouts/Default.tsx
Adds AuthLayout; refactors DefaultLayout to top navigation, responsive behavior, currency modal integration and simplified header.
Pages (added/removed/changed)
src/pages/*
Adds many pages (Connect, Dashboard, InternalError, PluginEarnings, PluginMembers, ProjectCategories, ProjectManagement, ProposalManagement, Proposals, etc.); removes AcceptInvite, NewPlugin, PluginEdit; major rewrites for Earnings, Plugins, NotFound.
Components
src/components/*
Adds CurrencyModal, DateView, MiddleTruncate; removes StatusModal.
Icons
src/icons/*
Adds ~30 new SVG icon components; consolidates/modifies several existing icons; removes some old icons.
Hooks
src/hooks/*
Adds useApp, useGoBack, useResizeObserver, useFilterParams; removes useExtension.
Storage
src/storage/*
Updates storage keys (vaultId → vaults), adds vaults and currency persistence helpers; removes token & vaultId modules.
Toolkits & UI primitives
src/toolkits/*
Button styling revised; new Divider component; Stack adds responsive breakpoints; minor Spin tweak.
Utilities & Types
src/utils/*, src/utils/types.ts
Adds chain/explorer metadata, currency constants, many helper functions (formatting, base64, explorer URLs), refactors extension utilities, switches API URLs to env vars, changes default theme to light, and reworks public types (Plugin/Proposal/Member/Vault/Earning/AuthToken). Removes eip712 helpers.
Styling & styled types
src/styled.d.ts, src/styles/_global.scss, src/utils/styled.ts
Inlines color tokens into DefaultTheme; removes some Ant sizing CSS; adds page min-width; HSLA comments added to theme tokens.
Data / Mocks
src/data/mockData.ts
Deletes mock data module and exported mock datasets.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through branches, nibbling lines of code,

Providers planted, new routes on the road,
Icons and pages in bright little stacks,
Vaults tucked in pockets and helpers with backs,
A joyful thump — the repo grew a new mode. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Implement portal version 2' accurately describes the major version upgrade and comprehensive refactoring evident throughout the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dev-portal-v2

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

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 (3)
src/styles/_global.scss (1)

1-6: ⚠️ Potential issue | 🟠 Major

Avoid global min-width and overflow-x: hidden on html.

This blocks small screens (mobile, split‑screen) and can hide legitimate overflow content. Consider scoping width constraints to a desktop-only layout container and handling small viewports with media queries instead of hiding overflow globally.

Suggested direction (scoped + responsive)
-html {
+html {
   font-family: "Brockmann", sans-serif;
   font-size: 14px;
   font-weight: 500;
-  min-width: 480px;
-  overflow-x: hidden;
 }
+
+/* Example: constrain only the app container */
+#root {
+  min-width: 480px;
+}
+
+@media (max-width: 479px) {
+  `#root` {
+    min-width: 100%;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/styles/_global.scss` around lines 1 - 6, Remove the global layout
constraints applied to the html selector (the min-width: 480px and overflow-x:
hidden) and instead apply them to a desktop-scoped container (e.g., add a
.app-container or .layout-wrapper) and enforce the width constraint via a
desktop-only media query; update the CSS by deleting the min-width and
overflow-x rules from the html block and adding equivalent rules to
.app-container with `@media` (min-width: 480px) { .app-container { min-width:
480px; overflow-x: hidden; } } so small viewports are not blocked and overflow
is handled only for desktop layouts.
src/Routes.tsx (1)

49-111: ⚠️ Potential issue | 🟠 Major

createBrowserRouter is recreated on every render — this resets router state.

createBrowserRouter is called directly inside the Routes component body without memoization. Every re-render creates a new router instance, causing React Router to remount the entire tree and lose navigation state, scroll position, and pending navigations. React Router warns against this.

Move the router creation outside the component, or at minimum wrap it in useMemo/useRef:

Proposed fix (minimal — useMemo)
 export const Routes = () => {
-  const router = createBrowserRouter([
+  const router = useMemo(() => createBrowserRouter([
     {
       path: routeTree.root.path,
       ...
     },
     ...
-  ]);
+  ]), []);

   return <RouterProvider router={router} />;
 };

However, ideally the router should be defined at module level or in a parent that doesn't re-render, since ProtectedRoute uses hooks (useApp) that can only work inside the router context — not at definition time. Verify whether the current approach triggers observable issues with navigation state loss.

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

In `@src/Routes.tsx` around lines 49 - 111, The Routes component recreates the
router on every render because createBrowserRouter is called inside Routes; move
router creation out of the component or memoize it so the router instance is
stable. Specifically, ensure the createBrowserRouter call that builds routes
(including children that reference ProtectedRoute, DefaultLayout, AuthLayout,
etc.) is created once (module-level) or wrapped in useMemo/useRef inside Routes
so RouterProvider receives a stable router; take care not to execute
component-hook-using code at module scope (ProtectedRoute must remain a
component reference only), so keep route elements as component JSX but ensure
the router object itself is reused.
src/utils/routes.ts (1)

1-55: ⚠️ Potential issue | 🔴 Critical

pluginCreate and pluginUpdate routes are defined in routeTree but not wired in Routes.tsx.

The routeTree object defines both routes (lines 23 and 32-35), but neither is included in the router configuration. Pages link to pluginUpdate (e.g., Plugins.tsx line 112 and PluginEarnings.tsx line 256) which will generate broken links. Navigating to /plugins/create or /plugins/:pluginId will hit the errorElement or render nothing.

Add route handlers for both paths to Routes.tsx:

  • pluginCreate (path: /plugins/create)
  • pluginUpdate (path: /plugins/:pluginId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/routes.ts` around lines 1 - 55, routeTree defines pluginCreate and
pluginUpdate but Routes.tsx never registers them, causing broken links; open
Routes.tsx and add route entries using routeTree.pluginCreate.path
("/plugins/create") and routeTree.pluginUpdate.path ("/plugins/:pluginId") to
the router configuration, importing and using the appropriate page components
(e.g., PluginCreatePage / PluginUpdatePage or whatever components render the
plugin creation and plugin detail/edit views in your codebase) so the routes are
handled instead of hitting the errorElement.
🟡 Minor comments (16)
index.html-17-23 (1)

17-23: ⚠️ Potential issue | 🟡 Minor

Align og:url with the canonical URL (trailing slash).
Right now the canonical uses a trailing slash but og:url doesn’t. For SEO consistency, keep them identical.

Suggested fix
-    <meta property="og:url" content="https://developer.vultisig.com" />
+    <meta property="og:url" content="https://developer.vultisig.com/" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` around lines 17 - 23, The meta tag values are inconsistent: the
<link rel="canonical"> uses a trailing slash but the meta property "og:url" does
not; update the meta property "og:url" value to include the trailing slash
(i.e., "https://developer.vultisig.com/") so it exactly matches the canonical
URL for SEO consistency.
src/toolkits/InputDigits.tsx-10-11 (1)

10-11: ⚠️ Potential issue | 🟡 Minor

Regex collapses only one dot per invocation — pasted values with 3+ dots are not fully sanitized

/(\..*)\./g is greedy: on each invocation it removes only the last dot in the matched span, leaving multiple dots in the result for pasted inputs with ≥ 3 dots.

Example traces:

  • "1.2.3.4.5" → one match ".2.3.4.""1.2.3.45" (3 dots remain)
  • "1.234.567.890" → one match ".234.567.""1.234.567890" (2 dots remain)

Per-keystroke input is unaffected (at most 2 dots can accumulate), but paste from formatted numbers (e.g. currency strings) would be mishandled.

🔧 Proposed fix — keep only the first decimal point
-          .replace(/(\..*)\./g, "$1");
+          .replace(/\.(?=.*\.)/g, "");

/\.(?=.*\.)/g removes every dot that has at least one more dot after it, keeping only the last (or equivalently, to keep the first dot, swap the lookahead direction).

Alternatively, split/join approach to keep the first dot:

-        const value = e.target.value
-          .replace(/[^0-9.]/g, "")
-          .replace(/(\..*)\./g, "$1");
+        const digits = e.target.value.replace(/[^0-9.]/g, "").split(".");
+        const value =
+          digits.length > 1
+            ? digits[0] + "." + digits.slice(1).join("")
+            : digits[0];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/toolkits/InputDigits.tsx` around lines 10 - 11, The current sanitization
in InputDigits.tsx uses .replace(/(\..*)\./g, "$1") which only collapses one dot
per invocation and fails on pasted values with 3+ dots; update the second
replace to remove every dot except the first by using a regex with lookahead
(e.g., replace the .replace(/(\..*)\./g, "$1") call with a pattern that strips
all dots that have another dot after them, such as /\.(?=.*\.)/g) so pasted
strings keep only the first decimal point (alternatively implement a split/join
that keeps the first dot).
src/toolkits/Spin.tsx-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Remove the stray BOM/hidden character from the import line.
Hidden characters can break linting or tooling in some environments.

🧹 Suggested fix (retype the import line)
-import { Spin as DefaultSpin, SpinProps } from "antd";
+import { Spin as DefaultSpin, SpinProps } from "antd";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/toolkits/Spin.tsx` at line 1, The import line for the Spin component
contains a stray BOM/hidden character; retype or replace the line that declares
"import { Spin as DefaultSpin, SpinProps } from \"antd\";" (the import of
DefaultSpin/SpinProps) to remove the invisible character so linters and tooling
stop failing—ensure the file begins with a normal ASCII "import" token and save.
src/utils/styled.ts-119-123 (1)

119-123: ⚠️ Potential issue | 🟡 Minor

HSLA annotations don't match the actual token values.

Lines 119, 123, and 160 have HSLA comments that don't align with the ColorToken constructor arguments:

  • Line 119: H=208 but comment says 209
  • Line 123: L=27 but comment says 24
  • Line 160: S=81 and L=13 but comment says 63% and 79%

These mismatches can mislead future design adjustments. Align the comments to match the actual values.

✏️ Suggested comment alignment
-  neutral300: new ColorToken(208, 24, 67), //hsla(209, 24%, 67%, 1)
+  neutral300: new ColorToken(208, 24, 67), //hsla(208, 24%, 67%, 1)
...
-  neutral700: new ColorToken(225, 7, 27), //hsla(225, 7%, 24%, 1)
+  neutral700: new ColorToken(225, 7, 27), //hsla(225, 7%, 27%, 1)
...
-    bgSuccess: new ColorToken(169, 81, 13), //hsla(169, 63%, 79%, 1)
+    bgSuccess: new ColorToken(169, 81, 13), //hsla(169, 81%, 13%, 1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/styled.ts` around lines 119 - 123, Update the HSLA inline comments
so they match the actual ColorToken constructor arguments: for each ColorToken
instance (e.g., neutral300, neutral700 and the ColorToken whose comment
currently reads "hsla(...63%, 79%)"), compute the HSLA values from the
constructor args (h, s, l) and replace the incorrect comment values so the
hsla(...) annotation reflects the exact hue, saturation% and lightness% used by
the ColorToken constructor; verify neutral300's hue is 208, neutral700's
lightness is 27, and fix the mismatched token comment that shows "63% / 79%" to
reflect S=81% and L=13% as per its constructor.
src/pages/ProjectManagement.tsx-60-103 (1)

60-103: ⚠️ Potential issue | 🟡 Minor

Consider enabling Enter-key submission for better form UX.

The button is outside the form and manually triggers form.submit() on click, which prevents users from submitting via Enter key. While this is Ant Design's recommended approach for external buttons, it bypasses standard form submission behavior.

To preserve Enter-key submission while keeping the button outside the form, use the native HTML form attribute instead:

Suggested adjustment
-          <Form
+          <Form
+            id="projectForm"
             autoComplete="off"
             form={form}
             layout="vertical"
             onFinish={handleFinish}
             requiredMark={false}
           >
-        <Stack as={Button} onClick={form.submit} $style={{ width: "300px" }}>
+        <Stack as={Button} htmlType="submit" form="projectForm" $style={{ width: "300px" }}>
           Continue
         </Stack>

Alternatively, move the button inside the form and remove the onClick handler to rely on native submission.

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

In `@src/pages/ProjectManagement.tsx` around lines 60 - 103, The external Continue
button prevents Enter-key submission; either move the button inside the Form so
native submission triggers on Enter (remove the manual form.submit handler) or
keep it outside but add the native HTML form association by setting the button’s
form attribute to the Form instance’s id and making the AntD <Form> have
matching id so pressing Enter triggers handleFinish; locate the Form component
(Form, form variable, onFinish={handleFinish}) and the external Stack acting as
the Button (Stack as={Button} onClick={form.submit}) and update accordingly.
src/pages/NotFound.tsx-1-31 (1)

1-31: ⚠️ Potential issue | 🟡 Minor

“Back Home” can navigate backward instead of home.

useGoBack ignores the provided path when location state exists, so the button may go back rather than to /. If the intent is always home, use useNavigate (or rename the button to “Go Back”).

✅ Option to always navigate home
-import { useGoBack } from "@/hooks/useGoBack";
+import { useNavigate } from "react-router-dom";
@@
-  const goBack = useGoBack();
+  const navigate = useNavigate();
@@
-            <Button onClick={() => goBack(routeTree.root.path)}>
+            <Button onClick={() => navigate(routeTree.root.path, { replace: true })}>
               Back Home
             </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/NotFound.tsx` around lines 1 - 31, The "Back Home" button uses
useGoBack (via useGoBack()) which can navigate backward instead of to root when
location state exists; update NotFoundPage to always navigate to home by
replacing useGoBack with React Router's useNavigate and call
navigate(routeTree.root.path) in the Button onClick (or alternatively rename the
Button to "Go Back" if you want back behavior to remain); ensure references to
useGoBack in NotFoundPage are removed and routeTree.root.path is used with
useNavigate so the button consistently lands on home.
src/hooks/useQueries.ts-22-25 (1)

22-25: ⚠️ Potential issue | 🟡 Minor

Avoid lowercasing IDs for case-sensitive chains.
Lowercasing id in the query key can collapse distinct case-sensitive token identifiers into the same cache entry. Normalize only for EVM chains or keep the raw id for non‑EVM chains.

💡 Suggested fix
-    const queryKey = ["tokens", chain.toLowerCase(), id.toLowerCase()];
+    const normalizedId = chain in evmChains ? id.toLowerCase() : id;
+    const queryKey = ["tokens", chain.toLowerCase(), normalizedId];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useQueries.ts` around lines 22 - 25, getTokenData currently
lowercases id when building queryKey (const queryKey = ["tokens",
chain.toLowerCase(), id.toLowerCase()]) which can collapse distinct
case‑sensitive token ids; update getTokenData to only normalize id for EVM
chains (or use raw id for non‑EVM) by checking Chain (e.g., if chain is in the
EVM list then .toLowerCase() the id, otherwise use id as‑is) and ensure the same
logic is used when creating queryKey and when calling queryClient.refetchQueries
so cache keys remain consistent.
.env.example-1-2 (1)

1-2: ⚠️ Potential issue | 🟡 Minor

Add a trailing newline to satisfy dotenv-linter.

Static analysis flagged a missing ending blank line.

🧹 Proposed fix
 VITE_DEVELOPER_PORTAL_URL=http://localhost:8080
 VITE_VULTISIG_SERVER=http://localhost:3000
+
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 1 - 2, Add a trailing newline character at the end
of the .env.example file so the last line
("VITE_VULTISIG_SERVER=http://localhost:3000") ends with a newline; this
satisfies dotenv-linter's requirement for a final blank line and prevents the
linter from reporting a missing ending newline.
src/toolkits/Divider.tsx-38-49 (1)

38-49: ⚠️ Potential issue | 🟡 Minor

Swap left/right placement logic for divider lines.

left currently hides the right line and shows the left; right does the opposite. Swap the conditions to match expected placement.

🛠️ Proposed fix
       $after={{
         backgroundColor,
-        content: placement !== "left" ? "" : "none",
+        content: placement !== "right" ? "" : "none",
         height,
         width,
       }}
       $before={{
         backgroundColor,
-        content: placement !== "right" ? "" : "none",
+        content: placement !== "left" ? "" : "none",
         height,
         width,
       }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/toolkits/Divider.tsx` around lines 38 - 49, The placement conditions for
the divider pseudo-elements are inverted: in Divider.tsx update the $after and
$before content checks so the right line is hidden when placement === "right"
and the left line is hidden when placement === "left" — specifically in the
component where $after and $before props are set, change the $after content
condition to check placement !== "right" (so it shows unless placement is
"right") and change the $before content condition to check placement !== "left"
(so it shows unless placement is "left"); keep the rest of the styling
(backgroundColor, height, width) unchanged.
src/pages/PluginEarnings.tsx-116-144 (1)

116-144: ⚠️ Potential issue | 🟡 Minor

Fix dataIndex to match the actual Transaction field.

The column's dataIndex: "statusOnchain" references a field that doesn't exist in the Transaction type. The Transaction object only has a status field. Change dataIndex to "status" to align with the field the render function correctly accesses.

🔧 Fix
-      dataIndex: "statusOnchain",
-      key: "statusOnchain",
+      dataIndex: "status",
+      key: "status",
       title: "Status",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/PluginEarnings.tsx` around lines 116 - 144, The column config uses
dataIndex: "statusOnchain" which doesn't exist on Transaction; update the
column's dataIndex to "status" so it matches the Transaction field accessed in
the render callback (and also update the column key from "statusOnchain" to
"status" for consistency with the dataIndex and to avoid mismatches in the
column definition inside PluginEarnings.tsx).
src/pages/Dashboard.tsx-46-54 (1)

46-54: ⚠️ Potential issue | 🟡 Minor

Revenue and user metrics are hardcoded.

"$2.3k" and "2.8k" are static strings, not derived from any data source. If these are placeholders, consider adding a TODO comment or fetching real data. As-is, the dashboard displays misleading numbers.

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

In `@src/pages/Dashboard.tsx` around lines 46 - 54, The items array in
Dashboard.tsx currently uses hardcoded strings ("$2.3k", "2.8k") for revenue and
users, which should be replaced with real data or explicitly marked as
placeholders: update the items definition (where DollarIcon and PeopleCopyIcon
are used) to read dynamic values (e.g., use props/state/fetched variables like
revenue and totalUsers) or, if temporary, add a clear TODO comment next to those
entries indicating they are placeholders; ensure the Total Plugins value
continues to use plugins.length and that any new variables are defined/fetched
earlier in the component (or passed in as props) before building items.
src/pages/ProposalManagement.tsx-393-395 (1)

393-395: ⚠️ Potential issue | 🟡 Minor

Email field has a URL placeholder instead of an email example.

The placeholder reads "https://your-plugin.example.com" — this is copied from the server endpoint field. It should be an email address.

Proposed fix
-                  <Input placeholder="https://your-plugin.example.com" />
+                  <Input placeholder="you@example.com" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/ProposalManagement.tsx` around lines 393 - 395, The email Input
inside the Form.Item in ProposalManagement.tsx uses the wrong placeholder (a
URL); update the placeholder for that Input component to a realistic email
example (e.g., "name@example.com") so the Input's placeholder matches the
intended email field. Locate the Input element rendered inside the Form.Item for
the email field and replace the URL placeholder string with an email example.
src/pages/Earnings.tsx-201-219 (1)

201-219: ⚠️ Potential issue | 🟡 Minor

Stats are hard-coded

Values like “$2,3k” and “1.7K” will drift from real data and can mislead users. Consider deriving them from earnings or clearly marking them as placeholders.

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

In `@src/pages/Earnings.tsx` around lines 201 - 219, The stats array in the
Earnings component currently uses hard-coded values ("$2,3k", "1.7K") which can
become stale; update the stats construction (the stats constant) to compute
label values from the existing earnings data (use the earnings variable/prop or
the data source used elsewhere in this file)—for example derive totalRevenue,
revenueGrowth, and totalTransactions from earnings summary fields and format
them consistently, or if real data is not yet available mark each value clearly
as a placeholder (e.g. append "—placeholder") so consumers know they are not
real; keep the same color and icon fields (CoinsAddIcon, LineChartOneIcon,
NewspaperIcon) and ensure any formatting utilities used for currency/abbrev are
reused or centralized.
src/pages/Earnings.tsx-122-125 (1)

122-125: ⚠️ Potential issue | 🟡 Minor

Status column dataIndex/key mismatch

dataIndex: "statusOnchain" doesn’t exist on Transaction, while the renderer uses status. This breaks built-in sorting/filtering and makes the column inconsistent.

🔧 Suggested fix
-      dataIndex: "statusOnchain",
-      key: "statusOnchain",
+      dataIndex: "status",
+      key: "status",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` around lines 122 - 125, The column config uses
dataIndex and key "statusOnchain" which doesn't exist on Transaction while the
cell renderer expects "status"; update the column definition in the Earnings
table (the column object with title "Status") to use dataIndex: "status" and
key: "status" so built-in sorting/filtering and rendering align with the
Transaction type, or alternatively adjust the Transaction model to expose a
"statusOnchain" field if that name is preferred—ensure the renderer, dataIndex,
and key all match the same property name.
src/pages/Earnings.tsx-64-80 (1)

64-80: ⚠️ Potential issue | 🟡 Minor

Amount formatting mixes fiat and token units

You multiply by baseValue (fiat conversion) but format with feeAsset.decimals and append feeAsset.symbol, producing values like “$123.0000 ETH” and excessive precision. Please align the unit: either show fiat (2–4 decimals, no token symbol) or show token amount (no fiat conversion).

🧭 One possible fiat-only formatting option
-            currency,
-            feeAsset.decimals,
-          )} ${feeAsset.symbol}`}</Stack>
+            currency,
+            2,
+          )}`}</Stack>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` around lines 64 - 80, The current render in
Earnings.tsx mixes fiat and token units by multiplying amount by baseValue but
formatting with feeAsset.decimals and appending feeAsset.symbol; decide on
fiat-only: keep the multiplication by baseValue, stop using feeAsset.decimals
for formatting, call toValueFormat with a fiat-appropriate precision (e.g., 2–4
decimals) and currency, and remove the appended feeAsset.symbol; alternatively,
if you want token amounts, remove the baseValue multiplication and format using
feeAsset.decimals and append feeAsset.symbol — update the render (the anonymous
render for dataIndex "amount") accordingly.
src/pages/Earnings.tsx-168-178 (1)

168-178: ⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" when opening external links in a new tab

External explorer links should include rel="noopener noreferrer" to prevent window.opener access from the opened page.

Suggested fix
                 to={explorerUrl}
                 target="_blank"
+                rel="noopener noreferrer"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` around lines 168 - 178, The external explorer link
uses target="_blank" on the HStack rendered as Link (the element with props
to={explorerUrl} and target="_blank"); add rel="noopener noreferrer" to that
HStack/Link element to prevent window.opener access when opening in a new tab
(ensure the rel prop is passed through the Link component so the final anchor
has rel="noopener noreferrer").
🧹 Nitpick comments (15)
src/icons/BuildingsIcon.tsx (1)

3-17: LGTM — consider defaulting aria-hidden="true" for decorative use

The component is well-structured: correct typing, proper {...props} spread after defaults so callers can override, and stroke="currentColor" / width="1em" follow idiomatic icon patterns.

One optional improvement: purely decorative icons should be hidden from screen readers. Without a default aria-hidden="true", every call site must remember to pass the prop. Spreading it before {...props} keeps it overridable:

♿ Optional: default aria-hidden for decorative icon usage
 export const BuildingsIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
   <svg
+    aria-hidden="true"
     fill="none"
     height="1em"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/icons/BuildingsIcon.tsx` around lines 3 - 17, Add a default
aria-hidden="true" to the BuildingsIcon SVG element so the decorative icon is
hidden from screen readers, keeping the existing {...props} spread after the
attributes so callers can still override it; update the SVG element inside the
BuildingsIcon component to include aria-hidden="true" alongside the current
attributes (fill, height, stroke, etc.) while leaving the component name and
prop typing unchanged.
src/icons/LoaderIcon.tsx (1)

3-17: Consider defaulting aria-hidden="true" for this decorative icon.

As a loader glyph with no text content or <title>, the SVG will be announced as an unlabelled image by screen readers when aria-hidden is not explicitly set by the consumer. Callers can still override it via {...props}, but a safe default prevents accidental inaccessible states.

♿ Proposed change
-export const LoaderIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
+export const LoaderIcon: FC<SVGProps<SVGSVGElement>> = ({ "aria-hidden": ariaHidden = true, ...props }) => (
   <svg
+    aria-hidden={ariaHidden}
     fill="none"
     height="1em"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/icons/LoaderIcon.tsx` around lines 3 - 17, The LoaderIcon SVG should
default to aria-hidden="true" to avoid being announced by screen readers; update
the <svg> in the LoaderIcon component to include aria-hidden="true" and ensure
the props spread ({...props}) remains so callers can override if needed (place
the explicit aria-hidden attribute before the {...props} spread on the <svg>
element).
src/assets/logo.json (2)

1-1: Consider externalizing the embedded PNG to reduce bundle weight.

The assets entry has "e":1 with the full image stored as a data:image/png;base64,… URI (~270×275 px). Base64 encoding adds ~33% overhead over the raw binary, and the image is parsed/decoded on every page that imports this JSON. Two lighter alternatives:

  • External reference: Set "e":0, "u":"<path-to-assets>/", "p":"logo-image.png". The image is fetched and cached separately by the browser.
  • dotLottie format (.lottie): Bundles both animation JSON and image in a zip container, avoiding base64 entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/assets/logo.json` at line 1, The assets array currently embeds a large
PNG as a base64 data URI (asset object with "id":"0", "e":1 and
"p":"data:image/png;base64,..."), which increases bundle size; change that asset
to reference an external file or a .lottie bundle instead: set "e":0, provide a
"u" base path and set "p" to the filename (e.g. "logo-image.png") so the browser
can fetch/cache the raw PNG, or convert the animation into a .lottie package and
replace the embedded image with the packaged asset and appropriate refId updates
(update the asset object with id "0" and any refId usages in layers to match).

1-1: Single-line JSON makes git diffs unreadable.

The entire animation is serialised onto one line, so any future edit produces an unintelligible diff. If this file is ever hand-edited or re-exported, consider pretty-printing it (e.g., jq . logo.json > logo.formatted.json) and committing the formatted version. A .prettierignore or similar can be updated to exclude it from auto-formatting if needed.

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

In `@src/assets/logo.json` at line 1, The logo.json file is minified into a single
line which makes git diffs unreadable; reformat it into a pretty-printed JSON
(e.g., using jq . or a JSON formatter) and commit the formatted file replacing
the current src/assets/logo.json, and optionally add src/assets/logo.json to
.prettierignore or update project formatting rules so exporters don't re-minify
it in the future; locate the file by its name "logo.json" (contains top-level
keys like "layers" and "assets" and version "v":"5.7.0") and ensure the
committed file remains human-readable.
src/toolkits/InputDigits.tsx (1)

1-1: Remove the UTF-8 BOM character

The file starts with a byte-order mark (). While TypeScript parsers tolerate it, it can cause subtle issues in some toolchains (bundlers, diff tools, string comparisons of imported paths). Save the file without BOM.

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

In `@src/toolkits/InputDigits.tsx` at line 1, The file contains a UTF-8 BOM at the
start (before the import statement "import { Input, InputProps } from \"antd\"")
which can break some toolchains; remove the BOM by re-saving the
src/toolkits/InputDigits.tsx file without a byte‑order mark (e.g., use your
editor's "Save without BOM" or run a small script to strip the leading U+FEFF)
so the import line and rest of the file start with the ASCII characters only.
src/utils/styled.ts (1)

177-177: Consider removing the commented-out export.

Leaving a commented public type can turn into dead code drift; either remove it or add a brief rationale if it’s intentionally parked for later.

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

In `@src/utils/styled.ts` at line 177, Remove the commented-out public type export
to avoid dead code drift: delete the line "//export type ThemeColorKeys = keyof
DefaultTheme;" from src/utils/styled.ts, or if you intend to keep it, replace it
with a short rationale comment above the symbol (e.g., "// kept intentionally
for future use: maps theme keys to DefaultTheme") or re-enable it by
uncommenting "export type ThemeColorKeys = keyof DefaultTheme;" so the intent is
explicit; reference the ThemeColorKeys and DefaultTheme identifiers when making
the change.
src/hooks/useResizeObserver.ts (1)

8-25: Rebind the observer if the ref target can change.

Right now the effect depends only on the ref object, so if the same ref is attached to a different element (conditional render), the observer stays on the old node and size updates stop. Consider keying the effect to the current element.

🔧 Suggested update
-  }, [ref]);
+  }, [ref.current]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useResizeObserver.ts` around lines 8 - 25, The effect in
useLayoutEffect registers a ResizeObserver for ref.current but only depends on
the ref object, so if ref.current changes the old observer remains attached;
update the effect to key off the current element (e.g., capture const element =
ref.current and include element in the dependency array) and ensure the cleanup
disconnects the observer for the previous element before observing the new one;
keep the existing updateSize and setSize logic and the ResizeObserver
creation/observer.observe(element) but make sure observer.disconnect() runs for
the previous observer when element changes or on unmount.
src/pages/Plugins.tsx (2)

20-275: Significant code duplication between Plugins.tsx and Proposals.tsx.

The stats section (grid of 3 stat cards), header/subtitle layout, StateProps shape, and fetchData pattern are nearly identical across both pages. Consider extracting shared components:

  • A PageHeader component for the title + subtitle + action button pattern.
  • A StatsGrid component for the 3-column stat cards.
  • A shared useFetchPluginsAndProposals hook for the data-fetching logic.

This would reduce the maintenance burden as these pages evolve.

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

In `@src/pages/Plugins.tsx` around lines 20 - 275, The PluginsPage duplicates
header, stats grid, state shape and fetch logic found in Proposals.tsx; extract
a reusable PageHeader component (title, subtitle, action Button usage), a
StatsGrid component (consumes stats array and renders the 3 stat cards), and
move the state/fetch logic into a hook useFetchPluginsAndProposals that returns
{loading, plugins, proposals, refresh}; refactor PluginsPage to replace
StateProps, the fetchData useEffectEvent, the stats array and header JSX with
imports of PageHeader, StatsGrid and the new hook (useFetchPluginsAndProposals)
so both PluginsPage and Proposals page can reuse them.

159-166: Sequential API calls — use Promise.all for parallel fetching.

Same issue flagged in Proposals.tsx. These independent requests should be parallelized:

Proposed fix
   const fetchData = useEffectEvent(async () => {
     setState((prev) => ({ ...prev, loading: true }));

-    const plugins = await getPlugins();
-    const proposals = await getProposals();
+    const [plugins, proposals] = await Promise.all([
+      getPlugins(),
+      getProposals(),
+    ]);

     setState((prev) => ({ ...prev, loading: false, plugins, proposals }));
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Plugins.tsx` around lines 159 - 166, The effect handler fetchData
currently awaits getPlugins() and then getProposals() sequentially; change it to
run both requests in parallel (e.g., Promise.all) inside fetchData so plugins
and proposals are fetched concurrently, set loading true before the parallel
call and false after both results are received, and update state with both
results at once (keep the same state keys); reference the fetchData
useEffectEvent and the getPlugins/getProposals calls when making the change.
src/pages/Proposals.tsx (2)

187-201: Remove commented-out code.

This block is dead code. If the Transactions feature is planned, track it in an issue rather than leaving commented-out JSX in the codebase.

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

In `@src/pages/Proposals.tsx` around lines 187 - 201, Remove the commented-out JSX
block for the Transactions shortcut (the Tooltip/HStack/NewspaperIcon link that
references routeTree.pluginEarnings and pluginId) from the Proposals component
so no dead/commented code remains; if the feature is intended for future work,
open/associate an issue instead of leaving the Tooltip/HStack/NewspaperIcon
block commented out.

207-214: Sequential await calls — consider parallel fetching.

getPlugins() and getProposals() are independent requests executed sequentially. Using Promise.all would cut the loading time roughly in half:

Proposed fix
   const fetchData = useEffectEvent(async () => {
     setState((prev) => ({ ...prev, loading: true }));

-    const plugins = await getPlugins();
-    const proposals = await getProposals();
+    const [plugins, proposals] = await Promise.all([
+      getPlugins(),
+      getProposals(),
+    ]);

     setState((prev) => ({ ...prev, loading: false, plugins, proposals }));
   });

The same pattern applies to Plugins.tsx (Lines 159-166) and Dashboard.tsx (if it fetches multiple resources in the future).

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

In `@src/pages/Proposals.tsx` around lines 207 - 214, fetchData currently calls
getPlugins() and getProposals() sequentially, which slows loading; change
fetchData (the async passed to useEffectEvent) to run them in parallel using
Promise.all (await Promise.all([getPlugins(), getProposals()])), then
destructure the results and call setState once to update loading, plugins, and
proposals; ensure you preserve existing error handling and loading toggles
around the parallel call in fetchData.
src/Routes.tsx (1)

26-41: Potential naming collision: component SetCurrentRoute and helper setCurrentRoute.

The component and the helper function differ only in casing. While JavaScript distinguishes them, this can confuse readers and tools (e.g., auto-import). Consider renaming the helper — e.g., withCurrentRoute or wrapCurrentRoute — to make the intent clearer at a glance.

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

In `@src/Routes.tsx` around lines 26 - 41, Rename the helper function
setCurrentRoute to a clearer distinct name (e.g., withCurrentRoute or
wrapCurrentRoute) to avoid collision with the SetCurrentRoute component; update
the function declaration for setCurrentRoute and all its call sites to the new
name, and keep the SetCurrentRoute component unchanged (symbols: SetCurrentRoute
component, previously named setCurrentRoute helper) so imports/auto-imports and
tooling no longer confuse the two.
src/pages/ProposalManagement.tsx (1)

310-324: Add validateDebounce to debounce async pluginId validation.

The custom validator calls validatePluginId(value) on every keystroke, triggering network requests during user input. Ant Design v6.3.0 supports the validateDebounce prop on Form.Item to delay validation:

Example using validateDebounce
                  hasFeedback
+                 validateDebounce={500}
                >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/ProposalManagement.tsx` around lines 310 - 324, The inline async
validator that calls validatePluginId on every keystroke should be debounced by
adding Ant Design's validateDebounce prop to the Form.Item that contains this
validator; update the Form.Item wrapping the validator (the one that defines the
async validator calling validatePluginId) to include validateDebounce={300} (or
another suitable ms) so validatePluginId is only invoked after the user pauses
typing, leaving the existing async validator logic intact.
src/utils/functions.ts (1)

207-222: Remove the commented-out toKebabCase block

Dead commented code adds noise; consider deleting it and relying on git history if it’s needed later.

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

In `@src/utils/functions.ts` around lines 207 - 222, Remove the dead commented-out
implementation of toKebabCase in this file: delete the entire commented block
that references toKebabCase, toKebab, isObject, and isArray so the file contains
only active code (rely on git history to restore if needed); ensure no other
references or duplicated implementations remain elsewhere that would be affected
by removing this comment.
src/pages/Earnings.tsx (1)

321-321: Wire up the existing loading state

state.loading is set but unused; Table supports a loading prop to show progress and avoid blank flashes.

♻️ Suggested fix
-      <Table<Transaction> columns={columns} dataSource={earnings} rowKey="id" />
+      <Table<Transaction>
+        columns={columns}
+        dataSource={earnings}
+        rowKey="id"
+        loading={state.loading}
+      />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` at line 321, The Table is not using the existing
loading state so the UI can flash; pass the component's loading flag into the
Table by setting its loading prop to the existing state.loading (i.e., update
the Table<Transaction> usage that currently references columns,
dataSource={earnings}, rowKey="id" to also include loading={state.loading}) so
the Table shows a spinner while earnings are being fetched.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df4047c and fa16f12.

⛔ Files ignored due to path filters (45)
  • package-lock.json is excluded by !**/package-lock.json
  • public/images/auth.jpg is excluded by !**/*.jpg
  • public/images/avatar.png is excluded by !**/*.png
  • public/images/failure-banner.jpg is excluded by !**/*.jpg
  • public/images/metatag.png is excluded by !**/*.png
  • public/images/not-support.jpg is excluded by !**/*.jpg
  • public/images/success-banner.jpg is excluded by !**/*.jpg
  • public/images/vulti.svg is excluded by !**/*.svg
  • public/tokens/akash.svg is excluded by !**/*.svg
  • public/tokens/arbitrum.svg is excluded by !**/*.svg
  • public/tokens/avalanche.svg is excluded by !**/*.svg
  • public/tokens/base.svg is excluded by !**/*.svg
  • public/tokens/bitcoin-cash.svg is excluded by !**/*.svg
  • public/tokens/bitcoin.svg is excluded by !**/*.svg
  • public/tokens/blast.svg is excluded by !**/*.svg
  • public/tokens/bsc.svg is excluded by !**/*.svg
  • public/tokens/cardano.svg is excluded by !**/*.svg
  • public/tokens/cosmos.svg is excluded by !**/*.svg
  • public/tokens/cronoschain.svg is excluded by !**/*.svg
  • public/tokens/dash.svg is excluded by !**/*.svg
  • public/tokens/dogecoin.svg is excluded by !**/*.svg
  • public/tokens/dydx.svg is excluded by !**/*.svg
  • public/tokens/ethereum.svg is excluded by !**/*.svg
  • public/tokens/hyperliquid.svg is excluded by !**/*.svg
  • public/tokens/kuji.svg is excluded by !**/*.svg
  • public/tokens/litecoin.svg is excluded by !**/*.svg
  • public/tokens/mantle.svg is excluded by !**/*.svg
  • public/tokens/mayachain.svg is excluded by !**/*.svg
  • public/tokens/noble.svg is excluded by !**/*.svg
  • public/tokens/optimism.svg is excluded by !**/*.svg
  • public/tokens/osmosis.svg is excluded by !**/*.svg
  • public/tokens/polkadot.svg is excluded by !**/*.svg
  • public/tokens/polygon.svg is excluded by !**/*.svg
  • public/tokens/ripple.svg is excluded by !**/*.svg
  • public/tokens/sei.svg is excluded by !**/*.svg
  • public/tokens/solana.svg is excluded by !**/*.svg
  • public/tokens/sui.svg is excluded by !**/*.svg
  • public/tokens/terra.svg is excluded by !**/*.svg
  • public/tokens/terraclassic.svg is excluded by !**/*.svg
  • public/tokens/thorchain.svg is excluded by !**/*.svg
  • public/tokens/ton.svg is excluded by !**/*.svg
  • public/tokens/tron.svg is excluded by !**/*.svg
  • public/tokens/zcash.svg is excluded by !**/*.svg
  • public/tokens/zksync.svg is excluded by !**/*.svg
  • public/wallet-core.wasm is excluded by !**/*.wasm
📒 Files selected for processing (118)
  • .coderabbit.yaml
  • .env.example
  • .gitignore
  • eslint.config.js
  • index.html
  • knip.json
  • package.json
  • src/App.tsx
  • src/Routes.tsx
  • src/api/plugins.ts
  • src/api/portal.ts
  • src/api/third-party/client.ts
  • src/api/third-party/crypto.ts
  • src/assets/logo.json
  • src/components/CurrencyModal.tsx
  • src/components/DateView.tsx
  • src/components/MiddleTruncate.tsx
  • src/components/StatusModal.tsx
  • src/context/App.tsx
  • src/context/Core.tsx
  • src/data/mockData.ts
  • src/hooks/useAntd.ts
  • src/hooks/useApp.ts
  • src/hooks/useExtension.tsx
  • src/hooks/useFilterParams.ts
  • src/hooks/useGoBack.ts
  • src/hooks/useQueries.ts
  • src/hooks/useResizeObserver.ts
  • src/icons/AnalyticsIcon.tsx
  • src/icons/ArrowBoxLeftIcon.tsx
  • src/icons/ArrowBoxRightIcon.tsx
  • src/icons/ArrowLeftIcon.tsx
  • src/icons/BoxIcon.tsx
  • src/icons/BrainIcon.tsx
  • src/icons/BuildingsIcon.tsx
  • src/icons/ChartFiveIcon.tsx
  • src/icons/ChartSixIcon.tsx
  • src/icons/CheckmarkIcon.tsx
  • src/icons/CoinsAddIcon.tsx
  • src/icons/CrossLargeIcon.tsx
  • src/icons/CurrencyDollarIcon.tsx
  • src/icons/CuteRobotIcon.tsx
  • src/icons/DollarIcon.tsx
  • src/icons/DotGridVerticalIcon.tsx
  • src/icons/EditIcon.tsx
  • src/icons/EmailTwoIcon.tsx
  • src/icons/FortuneTellerBallIcon.tsx
  • src/icons/ImagesFiveIcon.tsx
  • src/icons/InboxEmptyIcon.tsx
  • src/icons/LineChartOneIcon.tsx
  • src/icons/LiveFullIcon.tsx
  • src/icons/LoaderIcon.tsx
  • src/icons/MacbookIcon.tsx
  • src/icons/NewspaperIcon.tsx
  • src/icons/PencilLineIcon.tsx
  • src/icons/PeopleAddIcon.tsx
  • src/icons/PeopleAddedIcon.tsx
  • src/icons/PeopleCopyIcon.tsx
  • src/icons/PluginIcon.tsx
  • src/icons/PlusLargeIcon.tsx
  • src/icons/PlusSmallIcon.tsx
  • src/icons/PuzzleIcon.tsx
  • src/icons/SquareArrowOutTopLeftIcon.tsx
  • src/icons/SquareGridCircleIcon.tsx
  • src/icons/StarIcon.tsx
  • src/icons/ToolboxIcon.tsx
  • src/icons/TrashCanIcon.tsx
  • src/icons/WalletIcon.tsx
  • src/icons/ZapIcon.tsx
  • src/layouts/Auth.tsx
  • src/layouts/Default.tsx
  • src/main.tsx
  • src/pages/AcceptInvite.tsx
  • src/pages/Connect.tsx
  • src/pages/Dashboard.tsx
  • src/pages/Earnings.tsx
  • src/pages/InternalError.tsx
  • src/pages/NewPlugin.tsx
  • src/pages/NotFound.tsx
  • src/pages/PluginEarnings.tsx
  • src/pages/PluginEdit.tsx
  • src/pages/PluginMembers.tsx
  • src/pages/Plugins.tsx
  • src/pages/ProjectCategories.tsx
  • src/pages/ProjectManagement.tsx
  • src/pages/ProposalManagement.tsx
  • src/pages/Proposals.tsx
  • src/providers/Antd.tsx
  • src/providers/App.tsx
  • src/storage/constants.ts
  • src/storage/currency.ts
  • src/storage/state/del.ts
  • src/storage/theme.tsx
  • src/storage/token.ts
  • src/storage/vaultId.ts
  • src/storage/vaults.ts
  • src/styled.d.ts
  • src/styles/_global.scss
  • src/toolkits/Button.tsx
  • src/toolkits/Divider.tsx
  • src/toolkits/InputDigits.tsx
  • src/toolkits/Rate.tsx
  • src/toolkits/Spin.tsx
  • src/toolkits/Stack.ts
  • src/utils/chain.ts
  • src/utils/constants.ts
  • src/utils/currency.ts
  • src/utils/eip712.ts
  • src/utils/extension.ts
  • src/utils/functions.ts
  • src/utils/routes.ts
  • src/utils/styled.ts
  • src/utils/theme.ts
  • src/utils/types.ts
  • src/vite-env.d.ts
  • tsconfig.node.json
  • vercel.json
  • vite.config.ts
💤 Files with no reviewable changes (16)
  • src/icons/BoxIcon.tsx
  • src/icons/PluginIcon.tsx
  • src/icons/ZapIcon.tsx
  • src/pages/AcceptInvite.tsx
  • src/utils/eip712.ts
  • src/hooks/useExtension.tsx
  • src/storage/state/del.ts
  • src/icons/MacbookIcon.tsx
  • src/pages/NewPlugin.tsx
  • src/storage/token.ts
  • src/icons/EditIcon.tsx
  • src/data/mockData.ts
  • src/components/StatusModal.tsx
  • src/storage/vaultId.ts
  • src/api/plugins.ts
  • src/pages/PluginEdit.tsx

Copy link

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

♻️ Duplicate comments (2)
src/pages/PluginMembers.tsx (2)

80-95: ⚠️ Potential issue | 🟠 Major

Delete action is not wired, and current visibility condition looks inverted.

At Line 82 and Line 83-94, the control is rendered as clickable UI but has no onClick, so the Action column cannot perform deletion.

Suggested patch
-          {member.role === "admin" && (
+          {member.role !== "admin" && (
             <HStack
-              as="span"
+              as="button"
+              type="button"
+              onClick={() => handleDeleteMember(member.publicKey)}
+              aria-label="Remove member"
               $style={{
                 backgroundColor: colors.bgTertiary.toHex(),
                 borderRadius: "50%",
                 cursor: "pointer",
                 padding: "12px",
               }}
               $hover={{ color: colors.error.toHex() }}
             >
               <TrashCanIcon fontSize={16} />
             </HStack>
           )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/PluginMembers.tsx` around lines 80 - 95, The Action column
currently renders a clickable TrashCanIcon inside the render function but it has
no onClick and the visibility check is inverted; update the condition in the
render block (where member.role is checked) to only show the delete control for
non-admins (e.g., member.role !== "admin") and wire an onClick handler that
calls the component's delete routine (e.g., handleDeleteMember or deleteMember)
with the member identifier (member.id) to perform deletion; ensure the HStack
wrapper (and TrashCanIcon) includes an accessible label/role and prevents
unwanted event propagation if needed.

167-179: ⚠️ Potential issue | 🔴 Critical

form.submit() has no effect without an onFinish handler.

At Line 167 and Line 179, submit is triggered but no onFinish callback exists, so invite/update logic is never executed.

Suggested patch
+  const handleInvite = (values: Pick<Member, "role">) => {
+    // invoke existing invite/update flow here
+  };
...
-        <Form form={form} layout="vertical" requiredMark={false}>
+        <Form
+          form={form}
+          layout="vertical"
+          requiredMark={false}
+          onFinish={handleInvite}
+        >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/PluginMembers.tsx` around lines 167 - 179, The Form has no onFinish
handler so form.submit() (called by the Button) does nothing; add an onFinish
prop to the <Form form={form} ...> that calls the submission logic (e.g., a new
or existing handler like handleMemberSubmit) which performs invite vs update
based on the member variable, and wire that handler to dispatch the
create-invite or save-member flow; ensure the handler receives form values and
closes the modal (setState(... member: undefined) / goBack()) or shows errors as
appropriate so the Button's form.submit() triggers the intended invite/update
behavior.
🧹 Nitpick comments (6)
src/api/portal.ts (3)

23-32: Remove commented-out dead code.

The delMember function is entirely commented out. If it's not needed now, remove it; it can be recovered from version control if needed later.

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

In `@src/api/portal.ts` around lines 23 - 32, Remove the dead commented-out
delMember function block from src/api/portal.ts: delete the entire commented
code that defines delMember and its internal apiClient.del call (the lines
referencing delMember, pluginId, address, and
`/plugins/${pluginId}/team/${encodeURIComponent(address)}`); if you need it
later recover from VCS instead of keeping commented code.

17-21: Same pattern: delAuthToken declares Promise<void> but returns the API result.

Same as createProposal — use await instead of return to match the declared void return type.

🔧 Proposed fix
 export const delAuthToken = async (token: string): Promise<void> => {
   const { token_id } = jwtDecode<{ token_id: string }>(token);
 
-  return apiClient.del(`/auth/tokens/${token_id}`);
+  await apiClient.del(`/auth/tokens/${token_id}`);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/portal.ts` around lines 17 - 21, delAuthToken currently declares
Promise<void> but returns the apiClient.del result; change it to await the call
and not return its value so the function truly returns void. In function
delAuthToken decode the token_id via jwtDecode as before, then call await
apiClient.del(`/auth/tokens/${token_id}`) and let the function complete without
returning the API response (same pattern used to fix createProposal).

13-15: Minor: createProposal returns the API response but declares Promise<void>.

The function returns the result of apiClient.post(...) (which yields parsed response data) while declaring Promise<void>. TypeScript permits this (the value is silently discarded by callers), but using await would make the intent explicit and align the implementation with the declared type.

🔧 Proposed fix
 export const createProposal = async (proposal: Proposal): Promise<void> => {
-  return apiClient.post("/plugin-proposals", toSnakeCase(proposal));
+  await apiClient.post("/plugin-proposals", toSnakeCase(proposal));
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/portal.ts` around lines 13 - 15, The createProposal function
currently returns the result of apiClient.post(...) while declaring
Promise<void>; change it to await the call and not return the response so the
implementation matches the declared Promise<void>. Concretely, in createProposal
replace "return apiClient.post('/plugin-proposals', toSnakeCase(proposal));"
with "await apiClient.post('/plugin-proposals', toSnakeCase(proposal));" (and
optionally an explicit "return;"), keeping the function signature as
createProposal(proposal: Proposal): Promise<void>.
src/utils/functions.ts (2)

207-222: Remove commented-out toKebabCase function.

Dead code; recoverable from version control.

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

In `@src/utils/functions.ts` around lines 207 - 222, Remove the dead,
commented-out toKebabCase implementation: delete the entire commented block for
toKebabCase (which references toKebab, isObject, and isArray) so the file
contains no unused commented code; rely on VCS to recover if needed and ensure
no other references to the removed comment remain.

54-141: getExplorerUrl is highly repetitive — consider a data-driven approach.

Nearly all chains use the identical ${baseUrl}/address/${value} and ${baseUrl}/tx/${value} patterns. Only ~5 chains deviate (Polkadot, Ripple, Ton, TerraClassic, etc.). A default-with-overrides map would cut this from ~180 lines to ~20, improve maintainability, and make it trivial to add new chains.

♻️ Sketch of a data-driven alternative
const pathOverrides: Partial<Record<Chain, { address?: string; tx?: string }>> = {
  [chains.Polkadot]:      { address: "account", tx: "extrinsic" },
  [chains.Ripple]:        { address: "account", tx: "transaction" },
  [chains.Ton]:           { address: "",        tx: "transaction" },
  [chains.TerraClassic]:  { address: "classic/address", tx: "tx" },
  [chains.BitcoinCash]:   { tx: "transaction" },
  [chains.Cardano]:       { tx: "transaction" },
  [chains.Dash]:          { tx: "transaction" },
  [chains.Dogecoin]:      { tx: "transaction" },
  [chains.Litecoin]:      { tx: "transaction" },
  [chains.Tron]:          { tx: "transaction" },
  // ...other deviations
};

export const getExplorerUrl = (chain: Chain, entity: "address" | "tx", value: string): string => {
  const baseUrl = explorerBaseUrl[chain];
  const defaults = { address: "address", tx: "tx" };
  const segment = pathOverrides[chain]?.[entity] ?? defaults[entity];
  // Ton address is a special case: no path segment
  if (chain === chains.Ton && entity === "address") return `${baseUrl}/${value}`;
  return `${baseUrl}/${segment}/${value}`;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/functions.ts` around lines 54 - 141, getExplorerUrl is overly
repetitive; replace the nested match tables with a data-driven approach: create
a pathOverrides map (e.g., pathOverrides: Partial<Record<Chain, { address?:
string; tx?: string }>>) that lists only the chains that deviate from the
defaults, keep defaults {address: "address", tx: "tx"}, read
explorerBaseUrl[chain], compute segment = pathOverrides[chain]?.[entity] ??
defaults[entity], handle the special-case Ton address (no segment) and
TerraClassic/custom segments, then return `${baseUrl}/${segment}/${value}` (or
`${baseUrl}/${value}` for Ton address) from getExplorerUrl to condense and
simplify the logic.
src/providers/Core.tsx (1)

35-39: setCurrency and setTheme are not memoized, unlike setCurrentRoute.

setCurrentRoute is wrapped in useCallback (Line 31), but setCurrency and setTheme are plain functions recreated every render. Since they're passed through context, this causes all consumers to re-render even when the values haven't changed. Consider wrapping them in useCallback for consistency with setCurrentRoute.

Also applies to: 41-45

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

In `@src/providers/Core.tsx` around lines 35 - 39, Wrap setCurrency and setTheme
in useCallback like setCurrentRoute to prevent consumers from re-rendering; keep
the same logic (call setCurrencyStorage/setThemeStorage when fromStorage is
false and call setState(prev => ({ ...prev, currency })) or theme) and include
any external helpers (e.g., setCurrencyStorage, setThemeStorage) in the
useCallback dependency arrays so the callbacks update if those helpers change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/client.ts`:
- Line 17: The expiry check currently uses "return exp < dayjs().unix();" which
treats tokens with exp === now as valid; change that comparison to "exp <=
dayjs().unix()" so boundary-expired JWTs are treated as expired, and (optional)
read dayjs().unix() into a local const (e.g., const now = dayjs().unix()) to
avoid calling it twice when used elsewhere in the same function that performs
the expiry check.

In `@src/pages/PluginMembers.tsx`:
- Line 171: Replace the invalid Ant Design Modal prop usage in the PluginMembers
component: change the JSX using mask={{ closable: false }} to use
maskClosable={false} instead (i.e., update the Modal invocation where mask is
set to an object to use the maskClosable boolean prop so clicking the overlay
does not close the modal).

In `@src/providers/Core.tsx`:
- Around line 55-59: The effect using getBaseValue(currency) can set a stale
baseValue when currency changes rapidly; update the useEffect around
getBaseValue in Core.tsx to guard against out-of-order resolutions by tracking a
request token or AbortController: create a local identifier (e.g., requestId) or
an AbortController before calling getBaseValue, capture it in the closure, and
in the cleanup function mark the token as invalid (or call controller.abort());
then only call setState((prev) => ({ ...prev, baseValue })) inside the then
handler if the token is still valid (or the fetch was not aborted). Ensure you
reference the existing getBaseValue call and setState update in the effect and
add the cleanup return to the same useEffect.

In `@src/utils/extension.ts`:
- Around line 53-57: The catch block currently awaits disconnect() which can
throw and mask the original error from window.vultisig.getVault(); change the
catch to call disconnect() fire-and-forget (e.g., void disconnect()) so the
original error from window.vultisig.getVault() always propagates, or
alternatively wrap disconnect() in its own try/catch inside the catch block to
swallow/log any disconnect errors; update the code paths that call disconnect()
(the disconnect function itself and the catch in the function that calls
window.vultisig.getVault()) to reflect this change.
- Around line 24-27: Remove the unsupported params object from the
eth_requestAccounts call: update the call to window.vultisig.ethereum.request
used for account retrieval (the existing invocation that passes params: [{
preselectFastVault: true }]) so it requests "eth_requestAccounts" with no
params, letting the Vultisig UI handle vault selection.

In `@src/utils/functions.ts`:
- Around line 170-179: The function parseBase64DataUrl can yield base64 ===
undefined when dataUrl lacks a comma; update it to safely split and default
missing parts to empty strings: use const parts = dataUrl.split(","); const
prefix = parts[0] || ""; const base64 = parts[1] || ""; then derive mime with
prefix.match(...) as before and return { mime, base64 } so the return type {
mime: string; base64: string } is always satisfied (no runtime undefined).

---

Duplicate comments:
In `@src/pages/PluginMembers.tsx`:
- Around line 80-95: The Action column currently renders a clickable
TrashCanIcon inside the render function but it has no onClick and the visibility
check is inverted; update the condition in the render block (where member.role
is checked) to only show the delete control for non-admins (e.g., member.role
!== "admin") and wire an onClick handler that calls the component's delete
routine (e.g., handleDeleteMember or deleteMember) with the member identifier
(member.id) to perform deletion; ensure the HStack wrapper (and TrashCanIcon)
includes an accessible label/role and prevents unwanted event propagation if
needed.
- Around line 167-179: The Form has no onFinish handler so form.submit() (called
by the Button) does nothing; add an onFinish prop to the <Form form={form} ...>
that calls the submission logic (e.g., a new or existing handler like
handleMemberSubmit) which performs invite vs update based on the member
variable, and wire that handler to dispatch the create-invite or save-member
flow; ensure the handler receives form values and closes the modal (setState(...
member: undefined) / goBack()) or shows errors as appropriate so the Button's
form.submit() triggers the intended invite/update behavior.

---

Nitpick comments:
In `@src/api/portal.ts`:
- Around line 23-32: Remove the dead commented-out delMember function block from
src/api/portal.ts: delete the entire commented code that defines delMember and
its internal apiClient.del call (the lines referencing delMember, pluginId,
address, and `/plugins/${pluginId}/team/${encodeURIComponent(address)}`); if you
need it later recover from VCS instead of keeping commented code.
- Around line 17-21: delAuthToken currently declares Promise<void> but returns
the apiClient.del result; change it to await the call and not return its value
so the function truly returns void. In function delAuthToken decode the token_id
via jwtDecode as before, then call await
apiClient.del(`/auth/tokens/${token_id}`) and let the function complete without
returning the API response (same pattern used to fix createProposal).
- Around line 13-15: The createProposal function currently returns the result of
apiClient.post(...) while declaring Promise<void>; change it to await the call
and not return the response so the implementation matches the declared
Promise<void>. Concretely, in createProposal replace "return
apiClient.post('/plugin-proposals', toSnakeCase(proposal));" with "await
apiClient.post('/plugin-proposals', toSnakeCase(proposal));" (and optionally an
explicit "return;"), keeping the function signature as createProposal(proposal:
Proposal): Promise<void>.

In `@src/providers/Core.tsx`:
- Around line 35-39: Wrap setCurrency and setTheme in useCallback like
setCurrentRoute to prevent consumers from re-rendering; keep the same logic
(call setCurrencyStorage/setThemeStorage when fromStorage is false and call
setState(prev => ({ ...prev, currency })) or theme) and include any external
helpers (e.g., setCurrencyStorage, setThemeStorage) in the useCallback
dependency arrays so the callbacks update if those helpers change.

In `@src/utils/functions.ts`:
- Around line 207-222: Remove the dead, commented-out toKebabCase
implementation: delete the entire commented block for toKebabCase (which
references toKebab, isObject, and isArray) so the file contains no unused
commented code; rely on VCS to recover if needed and ensure no other references
to the removed comment remain.
- Around line 54-141: getExplorerUrl is overly repetitive; replace the nested
match tables with a data-driven approach: create a pathOverrides map (e.g.,
pathOverrides: Partial<Record<Chain, { address?: string; tx?: string }>>) that
lists only the chains that deviate from the defaults, keep defaults {address:
"address", tx: "tx"}, read explorerBaseUrl[chain], compute segment =
pathOverrides[chain]?.[entity] ?? defaults[entity], handle the special-case Ton
address (no segment) and TerraClassic/custom segments, then return
`${baseUrl}/${segment}/${value}` (or `${baseUrl}/${value}` for Ton address) from
getExplorerUrl to condense and simplify the logic.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa16f12 and e5a6ddf.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • package.json
  • src/api/client.ts
  • src/api/portal.ts
  • src/api/third-party/crypto.ts
  • src/pages/Dashboard.tsx
  • src/pages/PluginMembers.tsx
  • src/pages/Plugins.tsx
  • src/pages/Proposals.tsx
  • src/providers/Core.tsx
  • src/toolkits/Stack.ts
  • src/utils/chain.ts
  • src/utils/constants.ts
  • src/utils/extension.ts
  • src/utils/functions.ts
  • src/utils/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api/third-party/crypto.ts

Copy link

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

♻️ Duplicate comments (2)
src/providers/Antd.tsx (1)

116-203: ⚠️ Potential issue | 🔴 Critical

createStyles callback still uses unsupported cssVar context (Line 116), causing invalid style references.

This was already flagged in prior review comments and is still present. cssVar is referenced across Lines 119, 138, 158, 161-163, 167-169, 177, and 181; this should be replaced with supported token values (or CSS vars sourced through Ant Design config).

Proposed fix
-const useStyles = createStyles(({ css, cssVar, prefixCls }) => ({
+const useStyles = createStyles(({ css, token, prefixCls }) => ({
   modal: css`
     .${prefixCls}-modal-close {
-      background-color: ${cssVar.colorBgContainerDisabled};
+      background-color: ${token.colorBgContainerDisabled};
       inset-inline-end: 24px;
       top: 18px;
     }
@@
     .${prefixCls}-modal-header {
-      padding-right: ${cssVar.controlHeight};
+      padding-right: ${token.controlHeight}px;
     }
@@
     .${prefixCls}-table-tbody > tr > td {
-      border-top: 1px solid ${cssVar.colorBorder};
+      border-top: 1px solid ${token.colorBorder};
 
       &:first-child {
-        border-inline-start: 1px solid ${cssVar.colorBorder};
-        border-start-start-radius: ${cssVar.borderRadius};
-        border-end-start-radius: ${cssVar.borderRadius};
+        border-inline-start: 1px solid ${token.colorBorder};
+        border-start-start-radius: ${token.borderRadius}px;
+        border-end-start-radius: ${token.borderRadius}px;
       }
 
       &:last-child {
-        border-inline-end: 1px solid ${cssVar.colorBorder};
-        border-start-end-radius: ${cssVar.borderRadius};
-        border-end-end-radius: ${cssVar.borderRadius};
+        border-inline-end: 1px solid ${token.colorBorder};
+        border-start-end-radius: ${token.borderRadius}px;
+        border-end-end-radius: ${token.borderRadius}px;
       }
     }
@@
       &:first-child {
-        border-end-start-radius: ${cssVar.borderRadius};
+        border-end-start-radius: ${token.borderRadius}px;
       }
 
       &:last-child {
-        border-end-end-radius: ${cssVar.borderRadius};
+        border-end-end-radius: ${token.borderRadius}px;
       }
     }
   `,
#!/bin/bash
# Verify unsupported usage is still present in repository code.
# Expected: callback destructures cssVar and cssVar.* usages exist in this file.
rg -n '"antd-style"\s*:' package.json
rg -nP 'createStyles\(\(\{[^}]*\bcssVar\b' src/providers/Antd.tsx
rg -n '\bcssVar\.' src/providers/Antd.tsx
In antd-style v4.1.0, what is the supported createStyles callback context signature, and is cssVar officially supported there?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/Antd.tsx` around lines 116 - 203, The createStyles callback in
useStyles still destructures and uses the unsupported cssVar (see createStyles
callback and useStyles symbol) — replace all cssVar.* references with the
supported token values (e.g., token.colorBgContainerDisabled, token.colorBorder,
token.borderRadius, token.controlHeight) or obtain CSS vars via Ant Design theme
config; update the createStyles parameter to destructure token instead of cssVar
and adjust every occurrence inside modal, table, and upload blocks accordingly
so styles reference token.<insert nothing>
src/pages/Earnings.tsx (1)

65-73: ⚠️ Potential issue | 🟠 Major

“From” column is rendering asset contract address instead of sender address.

The column title says “From”, but the row value is feeAsset.addr instead of fromAddress.

🔧 Proposed fix
-      dataIndex: "feeAsset",
-      key: "feeAsset",
+      dataIndex: "fromAddress",
+      key: "fromAddress",
       title: "From",
-      render: (_, { feeAsset }) => {
+      render: (_, { fromAddress }) => {
         return (
           <HStack $style={{ justifyContent: "center" }}>
             <MiddleTruncate $style={{ width: "140px" }}>
-              {feeAsset.addr}
+              {fromAddress}
             </MiddleTruncate>
           </HStack>
         );
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` around lines 65 - 73, The "From" column is showing
feeAsset.addr but should show the sender's address; update the column's render
function (currently the render for dataIndex/key "feeAsset") to use the record's
fromAddress instead of feeAsset.addr — either change the render signature to (_,
{ fromAddress }) => ... and render fromAddress inside MiddleTruncate, or
reference record.fromAddress (e.g., render: (__, record) => record.fromAddress)
while keeping the HStack and MiddleTruncate components.
🧹 Nitpick comments (6)
src/hooks/useFilterParams.ts (1)

7-16: Redundant double Object.fromEntries/Object.entries round-trip.

Object.fromEntries(searchParams) already yields a Record<string, string> with string values (URLSearchParams entries are always strings, never null or undefined). Converting immediately back with Object.entries(...).map(...) and the ?? "" fallback is a no-op.

♻️ Proposed simplification
  const filters = useMemo(
-   () =>
-     Object.fromEntries(
-       Object.entries(Object.fromEntries(searchParams)).map(([key, value]) => [
-         key,
-         value ?? "",
-       ])
-     ) as T,
+   () => Object.fromEntries(searchParams) as T,
    [searchParams]
  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useFilterParams.ts` around lines 7 - 16, The filters memo currently
does a redundant round-trip
Object.fromEntries(...)->Object.entries(...).map(...) to coerce values, which is
unnecessary because URLSearchParams entries are already strings; simplify the
useMemo by directly using Object.fromEntries(searchParams) and cast to T (remove
the mapping and the nullish fallback), keeping the same dependency on
searchParams so update the filters definition inside useMemo (referencing
filters, useMemo and searchParams) to return Object.fromEntries(searchParams) as
T.
src/api/portal.ts (1)

24-33: Delete the commented-out delMember block.

If this API is intentionally removed, keeping it commented in-place adds noise and confusion.

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

In `@src/api/portal.ts` around lines 24 - 33, Remove the commented-out delMember
function block (the commented lines defining delMember and its call to
apiClient.del) from src/api/portal.ts; locate the commented symbol "delMember"
and the apiClient.del invocation and delete the entire commented section to
avoid dead/noisy code.
src/utils/functions.ts (2)

207-223: Remove commented-out implementation block.

Keeping inactive code inline makes this utility module harder to scan and maintain.

🧹 Cleanup
-// export const toKebabCase = <T>(obj: T): T => {
-//   if (isObject(obj)) {
-//     const result: Record<string, unknown> = {};
-//
-//     Object.keys(obj).forEach((key) => {
-//       const kebabKey = toKebab(key);
-//       result[kebabKey] = toKebabCase((obj as Record<string, unknown>)[key]);
-//     });
-//
-//     return result as T;
-//   } else if (isArray(obj)) {
-//     return obj.map((item) => toKebabCase(item)) as T;
-//   }
-//
-//   return obj;
-// };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/functions.ts` around lines 207 - 223, Remove the commented-out
toKebabCase implementation block to tidy the module: locate the commented
section referencing toKebabCase (and related helpers like isObject, isArray,
toKebab) and delete the entire commented code so only active utilities remain;
if you want to preserve the implementation for history, rely on VCS rather than
leaving commented code in src/utils/functions.ts.

54-141: Reduce explorer URL duplication to prevent mapping drift.

address and tx mappings duplicate almost all chain entries, which makes future chain updates error-prone.

♻️ Refactor sketch
+type ExplorerEntity = "address" | "tx";
+type PathResolver = (value: string) => string;
+
+const explorerPathByEntity: Record<ExplorerEntity, Record<Chain, PathResolver>> = {
+  address: {
+    [chains.Akash]: (v) => `/address/${v}`,
+    // ... chain-specific exceptions only
+  },
+  tx: {
+    [chains.Akash]: (v) => `/tx/${v}`,
+    // ... chain-specific exceptions only
+  },
+};
+
 export const getExplorerUrl = (
   chain: Chain,
   entity: "address" | "tx",
   value: string,
 ): string => {
   const baseUrl = explorerBaseUrl[chain];
-
-  return match(entity, {
-    address: () => ...,
-    tx: () => ...,
-  });
+  return `${baseUrl}${explorerPathByEntity[entity][chain](value)}`;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/functions.ts` around lines 54 - 141, The getExplorerUrl function
duplicates nearly identical per-chain branches for "address" and "tx";
consolidate by computing a default path (e.g., "/address" for entity==="address"
or "/tx" for entity==="tx") using explorerBaseUrl[chain] and then apply a small
override map for chains with nonstandard routes (e.g., Polkadot -> "/account"
(address) and "/extrinsic" (tx), Ripple -> "/account" (address) and
"/transaction" (tx), TerraClassic -> "/classic/address", Ton -> "/{value}"
(address) and "/transaction" (tx),
BitcoinCash/Cardano/Dash/Dogecoin/Litecoin/Tron -> "/transaction" for tx, etc.
Update getExplorerUrl to build the URL by joining baseUrl + (overridePath ??
defaultPath) and keep the match(entity, ...) wrapper but replace the long
per-chain lists with the centralized default + exception map to avoid
duplication and mapping drift.
src/pages/Earnings.tsx (1)

204-207: Cancel the debounced filter handler on unmount.

Without cleanup, pending debounced updates can fire after the component lifecycle moves on.

♻️ Proposed fix
   const debouncedHandleFilter = useMemo(
     () => debounce(setFilters, 500),
     [setFilters],
   );
+
+  useEffect(() => {
+    return () => {
+      debouncedHandleFilter.cancel();
+    };
+  }, [debouncedHandleFilter]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` around lines 204 - 207, The debouncedHandleFilter
created via useMemo (debounce(setFilters, 500)) needs to be cancelled on unmount
to avoid firing after the component is gone; add a useEffect that depends on
debouncedHandleFilter and returns a cleanup function which calls
debouncedHandleFilter.cancel(), ensuring any pending debounced calls are cleared
when the component unmounts or when debouncedHandleFilter changes.
src/pages/PluginEarnings.tsx (1)

186-188: Fetch plugin and earnings concurrently to reduce wait time.

These calls are independent and can be awaited together.

♻️ Proposed fix
-    const plugin = await getPlugin(pluginId);
-    const earnings = await getEarnings({ pluginId });
+    const [plugin, earnings] = await Promise.all([
+      getPlugin(pluginId),
+      getEarnings({ pluginId }),
+    ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/PluginEarnings.tsx` around lines 186 - 188, The sequential awaits
for getPlugin(pluginId) and getEarnings({ pluginId }) slow this page; run them
concurrently using Promise.all and destructuring instead: call
Promise.all([getPlugin(pluginId), getEarnings({ pluginId })]) and assign the
results to plugin and earnings respectively (ensure you keep the same variable
names and handle any errors the surrounding function already handles). This
change should be made where getPlugin and getEarnings are currently awaited so
both requests execute in parallel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/portal.ts`:
- Around line 18-22: The code is interpolating raw dynamic path segments (e.g.,
token_id) into endpoint URLs which can break for reserved characters; update
delAuthToken to encode the token_id (use encodeURIComponent on the decoded
token_id) before constructing the path, and apply the same change to other
affected functions in this file (the endpoints in the 55-79 range that
interpolate pluginId or similar path params) so every dynamic segment is passed
through encodeURIComponent prior to calling apiClient.get/post/del/etc.

In `@src/hooks/useFilterParams.ts`:
- Around line 18-28: setFilters currently overwrites the entire query string
because it calls setSearchParams(sanitized); change it to merge sanitized into
the existing search params instead: read the current params (via the
searchParams value you get from the hook or
Object.fromEntries(searchParams.entries())), create a merged object by spreading
current params and then spreading sanitized (so sanitized keys override), then
pass that merged object to setSearchParams; keep the existing sanitization step
(sanitized) so keys with undefined/null/"" are omitted. Ensure you update the
logic inside setFilters (and continue to use the sanitized variable) and call
setSearchParams with the merged params rather than sanitized alone.

In `@src/pages/Earnings.tsx`:
- Around line 44-49: The component tracks loading in state but never uses it in
the UI; destructure loading from state (const { earnings, plugins, loading } =
state) and pass it into the Table component(s) that render earnings (e.g., the
<Table> instances referenced near earnings rendering and the second occurrence
around the later block) so the table shows a loading indicator; ensure the Table
prop name matches your table implementation (e.g., loading or isLoading) and
keep existing setState calls that flip loading during fetches intact.

In `@src/pages/PluginEarnings.tsx`:
- Around line 237-240: The image rendering in the PluginEarnings component uses
plugin.logoUrl but lacks accessible alt text; update the JSX for the img element
(the element using as="img" and src={plugin.logoUrl}) to include an alt
prop—preferably alt={plugin.name || plugin.title || `Logo for ${plugin.id ||
'plugin'}`} and, if the image is purely decorative, use alt="" and
role="presentation"/aria-hidden accordingly—so screen readers receive
appropriate descriptive or decorative semantics.
- Around line 46-53: The "From" column currently renders feeAsset.addr (see the
column definition with dataIndex: "feeAsset", key: "feeAsset", title: "From",
render: (_, { feeAsset }) => ...) which shows the fee asset address instead of
the transaction sender; update the render to use the sender field (e.g., render:
(_, { sender }) => <MiddleTruncate ...>{sender.addr}</MiddleTruncate>) or adjust
dataIndex/key to "sender" so the column displays the actual sender address
(ensure you reference sender.addr and keep MiddleTruncate styling).

---

Duplicate comments:
In `@src/pages/Earnings.tsx`:
- Around line 65-73: The "From" column is showing feeAsset.addr but should show
the sender's address; update the column's render function (currently the render
for dataIndex/key "feeAsset") to use the record's fromAddress instead of
feeAsset.addr — either change the render signature to (_, { fromAddress }) =>
... and render fromAddress inside MiddleTruncate, or reference
record.fromAddress (e.g., render: (__, record) => record.fromAddress) while
keeping the HStack and MiddleTruncate components.

In `@src/providers/Antd.tsx`:
- Around line 116-203: The createStyles callback in useStyles still destructures
and uses the unsupported cssVar (see createStyles callback and useStyles symbol)
— replace all cssVar.* references with the supported token values (e.g.,
token.colorBgContainerDisabled, token.colorBorder, token.borderRadius,
token.controlHeight) or obtain CSS vars via Ant Design theme config; update the
createStyles parameter to destructure token instead of cssVar and adjust every
occurrence inside modal, table, and upload blocks accordingly so styles
reference token.<insert nothing>

---

Nitpick comments:
In `@src/api/portal.ts`:
- Around line 24-33: Remove the commented-out delMember function block (the
commented lines defining delMember and its call to apiClient.del) from
src/api/portal.ts; locate the commented symbol "delMember" and the apiClient.del
invocation and delete the entire commented section to avoid dead/noisy code.

In `@src/hooks/useFilterParams.ts`:
- Around line 7-16: The filters memo currently does a redundant round-trip
Object.fromEntries(...)->Object.entries(...).map(...) to coerce values, which is
unnecessary because URLSearchParams entries are already strings; simplify the
useMemo by directly using Object.fromEntries(searchParams) and cast to T (remove
the mapping and the nullish fallback), keeping the same dependency on
searchParams so update the filters definition inside useMemo (referencing
filters, useMemo and searchParams) to return Object.fromEntries(searchParams) as
T.

In `@src/pages/Earnings.tsx`:
- Around line 204-207: The debouncedHandleFilter created via useMemo
(debounce(setFilters, 500)) needs to be cancelled on unmount to avoid firing
after the component is gone; add a useEffect that depends on
debouncedHandleFilter and returns a cleanup function which calls
debouncedHandleFilter.cancel(), ensuring any pending debounced calls are cleared
when the component unmounts or when debouncedHandleFilter changes.

In `@src/pages/PluginEarnings.tsx`:
- Around line 186-188: The sequential awaits for getPlugin(pluginId) and
getEarnings({ pluginId }) slow this page; run them concurrently using
Promise.all and destructuring instead: call Promise.all([getPlugin(pluginId),
getEarnings({ pluginId })]) and assign the results to plugin and earnings
respectively (ensure you keep the same variable names and handle any errors the
surrounding function already handles). This change should be made where
getPlugin and getEarnings are currently awaited so both requests execute in
parallel.

In `@src/utils/functions.ts`:
- Around line 207-223: Remove the commented-out toKebabCase implementation block
to tidy the module: locate the commented section referencing toKebabCase (and
related helpers like isObject, isArray, toKebab) and delete the entire commented
code so only active utilities remain; if you want to preserve the implementation
for history, rely on VCS rather than leaving commented code in
src/utils/functions.ts.
- Around line 54-141: The getExplorerUrl function duplicates nearly identical
per-chain branches for "address" and "tx"; consolidate by computing a default
path (e.g., "/address" for entity==="address" or "/tx" for entity==="tx") using
explorerBaseUrl[chain] and then apply a small override map for chains with
nonstandard routes (e.g., Polkadot -> "/account" (address) and "/extrinsic"
(tx), Ripple -> "/account" (address) and "/transaction" (tx), TerraClassic ->
"/classic/address", Ton -> "/{value}" (address) and "/transaction" (tx),
BitcoinCash/Cardano/Dash/Dogecoin/Litecoin/Tron -> "/transaction" for tx, etc.
Update getExplorerUrl to build the URL by joining baseUrl + (overridePath ??
defaultPath) and keep the match(entity, ...) wrapper but replace the long
per-chain lists with the centralized default + exception map to avoid
duplication and mapping drift.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e5a6ddf and 9ff7cee.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • package.json
  • src/api/portal.ts
  • src/hooks/useFilterParams.ts
  • src/pages/Earnings.tsx
  • src/pages/PluginEarnings.tsx
  • src/providers/Antd.tsx
  • src/utils/constants.ts
  • src/utils/functions.ts
  • src/utils/types.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/pages/Earnings.tsx (1)

63-76: ⚠️ Potential issue | 🟠 Major

"From" column still renders the token contract address (feeAsset.addr) instead of the sender (fromAddress).

The prior review already flagged this. Earning.fromAddress is the transaction sender; feeAsset.addr is the token contract address. The column label "From" implies the sender.

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

In `@src/pages/Earnings.tsx` around lines 63 - 76, The "From" column currently
renders feeAsset.addr; change the render to use the transaction sender field by
returning fromAddress instead of feeAsset.addr—update the column config where
dataIndex/key "feeAsset" and the render function (render: (_, { feeAsset }) =>
{...}) to reference the Earning property fromAddress (e.g., render: (_, {
fromAddress }) => ...) and display that value (inside the same
HStack/MiddleTruncate) so the "From" column shows the sender, not the token
contract address.
🧹 Nitpick comments (2)
src/pages/Earnings.tsx (2)

181-196: Prefer a native <a> element for external explorer links.

getExplorerUrl returns an absolute external URL; react-router-dom's Link is designed for internal SPA routes. Using it for external links with target="_blank" works but is non-idiomatic and bypasses the router correctly only by accident.

♻️ Proposed refactor
-            <HStack
-              as={Link}
-              to={explorerUrl}
-              target="_blank"
-              $style={{
-                backgroundColor: colors.bgTertiary.toHex(),
-                borderRadius: "50%",
-                padding: "12px",
-              }}
-              $hover={{ color: colors.info.toHex() }}
-            >
+            <HStack
+              as="a"
+              href={explorerUrl}
+              target="_blank"
+              rel="noopener noreferrer"
+              $style={{
+                backgroundColor: colors.bgTertiary.toHex(),
+                borderRadius: "50%",
+                padding: "12px",
+              }}
+              $hover={{ color: colors.info.toHex() }}
+            >

Note: rel="noopener noreferrer" should be added when opening external links in a new tab to prevent the opened page from accessing window.opener.

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

In `@src/pages/Earnings.tsx` around lines 181 - 196, The external explorer link
currently uses react-router-dom's Link in the Earnings component return block
(the HStack wrapping SquareArrowOutTopLeftIcon and the call site that uses
getExplorerUrl), which is intended for internal routing; replace that Link usage
with a native anchor element (<a>) that uses the absolute URL returned by
getExplorerUrl, preserve the HStack styling and target="_blank", and add
rel="noopener noreferrer" to the anchor to secure the new tab; ensure Tooltip
and the surrounding HStack semantics remain unchanged so styling and hover
behavior are preserved.

204-207: Cancel the debounced function on unmount to avoid stale updates.

If the component unmounts before the 500 ms timer fires, setFilters (which calls setSearchParams) still executes, potentially triggering an unintended navigation.

♻️ Proposed fix
  const debouncedHandleFilter = useMemo(
    () => debounce(setFilters, 500),
    [setFilters],
  );
+
+  useEffect(() => {
+    return () => {
+      debouncedHandleFilter.cancel();
+    };
+  }, [debouncedHandleFilter]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` around lines 204 - 207, The debounced function
created by useMemo (debouncedHandleFilter) can fire after the component unmounts
causing stale setFilters calls; add a cleanup effect that cancels the debounce
on unmount by calling debouncedHandleFilter.cancel() (or the equivalent cancel
method provided by your debounce implementation) in a useEffect whose dependency
is debouncedHandleFilter so the timer is cleared when the component unmounts or
the debounced function changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/Earnings.tsx`:
- Around line 237-256: The stats array currently contains hardcoded placeholder
strings; replace those with computed values derived from the earnings array:
compute total revenue by summing earnings.map(e => e.amount) and format it with
toValueFormat (use "$" + toValueFormat(totalRevenue)); set Total Transactions to
earnings.length (format as a short string if needed, e.g., toValueFormat or
`${earnings.length}`) and compute Revenue Growth as a percent change between two
periods (e.g., split earnings by date or by recent vs. prior period: (recentSum
- priorSum)/priorSum * 100) and format with a "+"/ "-" prefix and "%" (fallback
to "0%" or "—" if priorSum is 0 or insufficient data). Update the stats array
definition to reference these computed variables instead of the hardcoded
"$2,3k", "+32%", and "1.7K", and ensure you use toValueFormat for numeric
formatting and correct English decimal separators.
- Around line 213-221: fetchEarnings is calling form.setFieldsValue(filters) on
every filters change (it's defined with useEffectEvent), causing the form to be
overwritten even when the user edits it; remove form.setFieldsValue from
fetchEarnings and instead initialize the form once on mount by adding a separate
useEffect (or useEffectOnce) that calls form.setFieldsValue(filters) when the
component mounts to sync URL params into the form, while leaving fetchEarnings
to only set loading, call getEarnings(filters), and update state. Ensure you
reference fetchEarnings, form.setFieldsValue, filters, and getEarnings when
applying this change.
- Around line 136-169: The column uses dataIndex: "statusOnchain" which doesn't
exist on the Earning type (the field is status), so update the column definition
(the object with key "statusOnchain" and render callback in Earnings.tsx) to use
dataIndex: "status" so Ant Design receives the correct field for
sorting/filtering; keep the render function that destructures { status }
unchanged and ensure key/title remain appropriate after the dataIndex rename.

---

Duplicate comments:
In `@src/pages/Earnings.tsx`:
- Around line 63-76: The "From" column currently renders feeAsset.addr; change
the render to use the transaction sender field by returning fromAddress instead
of feeAsset.addr—update the column config where dataIndex/key "feeAsset" and the
render function (render: (_, { feeAsset }) => {...}) to reference the Earning
property fromAddress (e.g., render: (_, { fromAddress }) => ...) and display
that value (inside the same HStack/MiddleTruncate) so the "From" column shows
the sender, not the token contract address.

---

Nitpick comments:
In `@src/pages/Earnings.tsx`:
- Around line 181-196: The external explorer link currently uses
react-router-dom's Link in the Earnings component return block (the HStack
wrapping SquareArrowOutTopLeftIcon and the call site that uses getExplorerUrl),
which is intended for internal routing; replace that Link usage with a native
anchor element (<a>) that uses the absolute URL returned by getExplorerUrl,
preserve the HStack styling and target="_blank", and add rel="noopener
noreferrer" to the anchor to secure the new tab; ensure Tooltip and the
surrounding HStack semantics remain unchanged so styling and hover behavior are
preserved.
- Around line 204-207: The debounced function created by useMemo
(debouncedHandleFilter) can fire after the component unmounts causing stale
setFilters calls; add a cleanup effect that cancels the debounce on unmount by
calling debouncedHandleFilter.cancel() (or the equivalent cancel method provided
by your debounce implementation) in a useEffect whose dependency is
debouncedHandleFilter so the timer is cleared when the component unmounts or the
debounced function changes.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ff7cee and 77e3a44.

📒 Files selected for processing (2)
  • src/pages/Earnings.tsx
  • src/pages/PluginEarnings.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/PluginEarnings.tsx

Copy link

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

♻️ Duplicate comments (6)
src/providers/Core.tsx (1)

36-42: ⚠️ Potential issue | 🟠 Major

Prevent stale baseValue writes from out-of-order async responses.

Line 68 triggers a new request on each currency change, but an older request can still resolve later and overwrite state at Line 41.

Proposed fix
-  const fetchBaseValue = useEffectEvent(async () => {
-    setState((prev) => ({ ...prev, baseValue: undefined }));
-
-    const baseValue = await getBaseValue(currency);
-
-    setState((prev) => ({ ...prev, baseValue }));
-  });
-
   useEffect(() => {
-    fetchBaseValue();
+    let cancelled = false;
+
+    setState((prev) => ({ ...prev, baseValue: undefined }));
+    getBaseValue(currency).then((baseValue) => {
+      if (!cancelled) {
+        setState((prev) => ({ ...prev, baseValue }));
+      }
+    });
+
+    return () => {
+      cancelled = true;
+    };
   }, [currency]);

Also applies to: 68-70

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

In `@src/providers/Core.tsx` around lines 36 - 42, The async fetchBaseValue
invoked on currency changes can write stale baseValue when out-of-order
responses arrive; fix by tagging each request with a monotonic id (e.g.,
requestId stored in a ref) or using an AbortController if getBaseValue supports
cancellation: increment the id before calling getBaseValue, capture the id in
the async closure, and only call setState to update baseValue when the captured
id equals the current ref id (or when the AbortController signal has not been
aborted). Apply this change inside fetchBaseValue (referencing fetchBaseValue,
getBaseValue, setState, baseValue, currency) so older responses cannot overwrite
newer state.
src/pages/Earnings.tsx (2)

66-78: "From" column renders fee asset contract address, not the sender.

The column is labeled "From" but displays feeAsset.addr (the token contract address). The Earning type has a fromAddress field that represents the actual sender.

🔧 Proposed fix
     {
       align: "center",
-      dataIndex: "feeAsset",
-      key: "feeAsset",
+      dataIndex: "fromAddress",
+      key: "fromAddress",
       title: "From",
-      render: (_, { feeAsset }) => {
+      render: (_, { fromAddress }) => {
         return (
           <HStack $style={{ justifyContent: "center" }}>
             <MiddleTruncate $style={{ width: "140px" }}>
-              {feeAsset.addr}
+              {fromAddress}
             </MiddleTruncate>
           </HStack>
         );
       },
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Earnings.tsx` around lines 66 - 78, The "From" column currently
renders the token contract address via feeAsset.addr; update the column render
to show the actual sender by using the Earning.fromAddress field instead. Locate
the column definition in Earnings.tsx (the column with title "From",
dataIndex/key "feeAsset") and change the render callback to read from the row's
fromAddress (e.g., render: (_, { fromAddress }) => ...) and display that value
inside the existing HStack/MiddleTruncate UI so the column reflects the sender,
not the fee asset contract.

215-223: form.setFieldsValue(filters) called on every filter change, not just mount.

Since fetchEarnings runs on every filters change, form.setFieldsValue is redundant for user-initiated filter changes and can cause form value flickering.

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

In `@src/pages/Earnings.tsx` around lines 215 - 223, The call to
form.setFieldsValue(filters) inside fetchEarnings (the async created by
useEffectEvent) causes the form to be updated on every filters change and
produces flicker; remove that call from fetchEarnings and instead set the form
once on mount or initialization (e.g., in a separate useEffect with empty deps
or via the form's initialValues) so only the initial filters populate the form;
reference fetchEarnings, form.setFieldsValue, useEffectEvent and filters to
locate and adjust the code.
src/api/portal.ts (1)

19-23: Encode dynamic path segments with encodeURIComponent.

token_id (from JWT decode) and pluginId in various endpoints are interpolated raw into URL paths. If these contain reserved URI characters, the request will be malformed.

This applies to lines 22, 61, 69, 77, and 83.

🔧 Proposed fix (representative example)
-  return apiClient.del(`/auth/tokens/${token_id}`);
+  return apiClient.del(`/auth/tokens/${encodeURIComponent(token_id)}`);
-  return apiClient.get<Plugin>(`/plugins/${pluginId}`);
+  return apiClient.get<Plugin>(`/plugins/${encodeURIComponent(pluginId)}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/portal.ts` around lines 19 - 23, The code interpolates dynamic path
segments raw into URLs (e.g., token_id in delAuthToken and pluginId in other
portal API helpers), which can break requests if they contain reserved URI
characters; fix by wrapping every dynamic segment used inside template paths
with encodeURIComponent — specifically update delAuthToken to use
encodeURIComponent(token_id) and update all portal API functions that
interpolate pluginId (and any other decoded IDs) to use
encodeURIComponent(pluginId) when building the path strings so all requests are
properly encoded.
src/pages/PluginEarnings.tsx (2)

58-68: "From" column renders fee asset address, not sender.

Same issue as in Earnings.tsx — the column labeled "From" displays feeAsset.addr instead of fromAddress.

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

In `@src/pages/PluginEarnings.tsx` around lines 58 - 68, The "From" column is
rendering feeAsset.addr incorrectly; update the column definition (dataIndex
"feeAsset", key "feeAsset", title "From", render function) to use the
transaction's fromAddress instead of feeAsset.addr — either change dataIndex/key
to "fromAddress" or adjust the render callback signature to extract and return
fromAddress (e.g., render: (_, { fromAddress }) => <MiddleTruncate
...>{fromAddress}</MiddleTruncate>), keeping the existing MiddleTruncate
styling.

272-277: Missing alt attribute on plugin logo image.

Accessibility concern — the <img> has no alt text for screen readers.

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

In `@src/pages/PluginEarnings.tsx` around lines 272 - 277, The image rendered with
Stack as="img" when plugin.logoUrl exists is missing an alt attribute; update
the JSX in PluginEarnings (where plugin.logoUrl and the Stack with as="img" are
used) to include an appropriate alt prop (e.g., alt={plugin.name ?? 'Plugin
logo'}), or set alt="" if the image is purely decorative, so screen readers get
correct accessibility info.
🧹 Nitpick comments (8)
src/providers/Core.tsx (1)

48-52: Skip no-op setter updates to avoid redundant renders and fetches.

If the same currency/theme is set again, state still updates. For currency, that can cause extra effect work downstream.

Proposed refactor
-  const setCurrency = (currency: Currency, fromStorage?: boolean) => {
+  const setCurrency = (currency: Currency, fromStorage?: boolean) => {
+    if (currency === state.currency) return;
     if (!fromStorage) setCurrencyStorage(currency);
 
     setState((prev) => ({ ...prev, currency }));
   };
 
   const setTheme = (theme: Theme, fromStorage?: boolean) => {
+    if (theme === state.theme) return;
     if (!fromStorage) setThemeStorage(theme);
 
     setState((prev) => ({ ...prev, theme }));
   };

Also applies to: 54-58

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

In `@src/providers/Core.tsx` around lines 48 - 52, The setCurrency function
currently updates storage and state even when the new currency equals the
current one; add an early no-op guard in setCurrency that compares the incoming
currency with the current state.currency and returns immediately (skipping
setCurrencyStorage and setState) when identical to avoid redundant
renders/fetches; apply the same pattern to setTheme (and its storage helper
setThemeStorage) so both setters bail out on no-op updates.
src/utils/functions.ts (3)

219-234: Remove commented-out toKebabCase code.

Dead/commented-out code adds noise. If it's needed later it can be recovered from version control.

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

In `@src/utils/functions.ts` around lines 219 - 234, Remove the commented-out dead
code for the toKebabCase helper: delete the entire commented block that
references toKebabCase, toKebab, isObject, and isArray in src/utils/functions.ts
so the file contains only active code; if this functionality is needed later it
can be restored from VCS.

208-217: Verify toDecimalFormat formula intent — baseValue acts as a price multiplier.

The formula value * baseValue / 10^decimals implies baseValue is a price/exchange-rate, not a "base" in the mathematical sense. The naming could mislead maintainers into thinking it's a radix or base unit. Consider renaming to price or exchangeRate for clarity, or at minimum adding a JSDoc comment.

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

In `@src/utils/functions.ts` around lines 208 - 217, The toDecimalFormat
function's parameter baseValue is misleading because the formula value *
baseValue / 10^decimals treats it as a price/exchange-rate; update the signature
or docs to clarify intent: either rename baseValue to price or exchangeRate in
the function declaration and all call sites (toDecimalFormat(value, price,
decimals)), or add a JSDoc above toDecimalFormat explaining that baseValue is a
price/exchange-rate multiplier used to convert units (value * baseValue /
10^decimals), and update parameter name in the JSDoc to match; ensure
tests/types and any imports/usages are updated accordingly.

55-142: getExplorerUrl is highly repetitive — consider a data-driven approach.

Nearly all chains use the same pattern (/address/${value} and /tx/${value}), with only a handful of exceptions (Polkadot, Ripple, Ton, BitcoinCash, etc.). A lookup table of overrides would cut ~160 lines to ~20 and make adding new chains trivial.

♻️ Sketch of data-driven approach
+const addressPath: Partial<Record<Chain, string>> = {
+  [chains.Polkadot]: "account",
+  [chains.Ripple]: "account",
+  [chains.Ton]: "",           // no prefix segment
+  [chains.TerraClassic]: "classic/address",
+};
+
+const txPath: Partial<Record<Chain, string>> = {
+  [chains.BitcoinCash]: "transaction",
+  [chains.Cardano]: "transaction",
+  [chains.Dash]: "transaction",
+  [chains.Dogecoin]: "transaction",
+  [chains.Litecoin]: "transaction",
+  [chains.Polkadot]: "extrinsic",
+  [chains.Ripple]: "transaction",
+  [chains.Ton]: "transaction",
+  [chains.Tron]: "transaction",
+};
+
+export const getExplorerUrl = (
+  chain: Chain,
+  entity: "address" | "tx",
+  value: string,
+): string => {
+  const baseUrl = explorerBaseUrl[chain];
+  if (entity === "address") {
+    const seg = addressPath[chain] ?? "address";
+    return seg ? `${baseUrl}/${seg}/${value}` : `${baseUrl}/${value}`;
+  }
+  const seg = txPath[chain] ?? "tx";
+  return `${baseUrl}/${seg}/${value}`;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/functions.ts` around lines 55 - 142, The getExplorerUrl function
repeats the same string patterns for most chains; replace the large match blocks
in getExplorerUrl with a data-driven approach: compute a default path for entity
("address" -> "/address/${value}", "tx" -> "/tx/${value}") using
explorerBaseUrl[chain], then apply small override lookup maps (e.g.,
addressOverrides and txOverrides keyed by chains.Polkadot, chains.Ripple,
chains.Ton, chains.TerraClassic, chains.BitcoinCash, chains.Cardano,
chains.Dash, chains.Dogecoin, chains.Litecoin, chains.Tron, etc.) to return the
special path when present; update getExplorerUrl to first check
overrideMap[entity][chain] and fall back to defaultPath, keeping references to
getExplorerUrl, explorerBaseUrl and the chains enum to locate where to change
code.
src/utils/types.ts (1)

66-83: Consider separating form-submission fields from the API response type.

Proposal mixes API response fields with form-only fields (logo, media, thumbnail). This can lead to confusion at call sites — e.g., toSnakeCase(proposal) in createProposal will serialize these extra fields to the API. Consider a ProposalForm type that extends Proposal with the form-only fields, keeping Proposal as the pure API shape.

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

In `@src/utils/types.ts` around lines 66 - 83, The Proposal type mixes
API-returned fields with form-only fields (logo, media, thumbnail), causing form
values to be serialized and sent to the API (e.g., via toSnakeCase in
createProposal); split responsibilities by creating a new ProposalForm type that
extends the API Proposal with the form-only fields and update usages: keep
Proposal as the pure API shape, change form handlers and createProposal input to
accept ProposalForm (or convert ProposalForm -> Proposal before calling
toSnakeCase/createProposal), and update any type annotations referencing
Proposal where they should be the form type.
src/pages/Dashboard.tsx (1)

37-49: Sequential awaits for independent API calls — use Promise.all.

getEarningSummary() and getPlugins() are independent; fetching them in parallel would improve load time.

♻️ Proposed improvement
   const fetchData = useEffectEvent(async () => {
     setState((prev) => ({ ...prev, loading: true }));

-    const { totalEarnings } = await getEarningSummary();
-    const plugins = await getPlugins();
+    const [{ totalEarnings }, plugins] = await Promise.all([
+      getEarningSummary(),
+      getPlugins(),
+    ]);

     setState((prev) => ({
       ...prev,
       loading: false,
       plugins,
       totalEarnings,
     }));
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Dashboard.tsx` around lines 37 - 49, The fetchData effect currently
awaits getEarningSummary() then getPlugins() sequentially; update fetchData (the
async handler created with useEffectEvent) to run them in parallel using
Promise.all([getEarningSummary(), getPlugins()]) and destructure the results
into totalEarnings and plugins, then call setState to clear loading and set
plugins/totalEarnings; ensure loading is set true before the Promise.all and
false after, and handle/rethrow errors as currently done.
src/api/portal.ts (1)

25-34: Remove commented-out delMember function.

Dead code — recoverable from version control if needed later.

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

In `@src/api/portal.ts` around lines 25 - 34, Remove the dead, commented-out
delMember function block (the commented export const delMember ...) from the
codebase; delete the entire commented block so no dead code remains, ensure
surrounding code formatting is preserved (no leftover blank lines or dangling
commas), and run lint/tests to confirm nothing else depended on that commented
declaration.
src/pages/Earnings.tsx (1)

37-49: Sequential awaits in fetchData — consider Promise.all for parallel fetching.

getEarningSummary() and getPlugins() are independent and can be fetched concurrently.

♻️ Proposed improvement
   const fetchData = useEffectEvent(async () => {
     setState((prev) => ({ ...prev, loading: true }));

-    const { totalEarnings } = await getEarningSummary();
-    const plugins = await getPlugins();
+    const [{ totalEarnings, totalTransactions }, plugins] = await Promise.all([
+      getEarningSummary(),
+      getPlugins(),
+    ]);

     setState((prev) => ({
       ...prev,
       loading: false,
       plugins,
       totalEarnings,
+      totalTransactions,
     }));
   });

Also applies to: 225-235

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

In `@src/pages/Earnings.tsx` around lines 37 - 49, In fetchData (used by
EarningsPage), getEarningSummary() and getPlugins() are being awaited
sequentially; change to run them in parallel with Promise.all (e.g.,
Promise.all([getEarningSummary(), getPlugins()])) and then destructure results
to update state in one setState call, preserving the loading flag and merging
the EarningSummary fields into state; ensure error handling still catches
failures and sets loading false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/portal.ts`:
- Around line 36-48: getAuthToken currently maps the single backend token to
both accessToken and refreshToken; update getAuthToken and/or the AuthToken type
so this isn't misleading: either (A) make AuthToken.refreshToken optional (or
nullable) and return { accessToken: token, refreshToken: undefined } from
getAuthToken, or (B) keep the existing shape but add an explicit comment in
getAuthToken stating the backend only returns a single token and both fields are
intentionally the same for now; locate the getAuthToken function and the
AuthToken type to apply the chosen change.

In `@src/pages/Dashboard.tsx`:
- Around line 341-394: The plugin card currently renders a hardcoded "Live"
badge and "Pricing: Free" inside the HStack/Stack blocks; update the rendering
to use the plugin's actual properties (e.g., plugin.status and plugin.pricing or
plugin.pricingModel) instead of fixed strings: conditionally render the badge
text and color (use colors.success when plugin.status === 'live', otherwise use
a neutral/warning color) in the HStack/Stack that currently shows "Live", and
replace the static "Pricing: Free" text with the plugin.pricing value or a
computed label (e.g., "Free", "Paid", or formatted price) using the same Stack
elements so cards reflect each plugin's real status and pricing.
- Around line 286-296: The plugin logo <img> rendered via the Stack component
lacks an alt attribute; update the conditional that renders the logo (the
Boolean(logoUrl) block) to pass an alt prop to the Stack element (e.g. use the
plugin's displayName or name like plugin.displayName || plugin.name as the alt
text, or alt="" if the image is purely decorative) so screen readers get
appropriate text; make this change where logoUrl is used in the Dashboard
component.

In `@src/pages/Earnings.tsx`:
- Around line 206-209: The debounced handler created as debouncedHandleFilter
via useMemo (debounce(setFilters, 500)) needs to be cancelled when the component
unmounts to avoid calling setFilters after unmount; update the component to call
debouncedHandleFilter.cancel() in a cleanup function (e.g., return () =>
debouncedHandleFilter.cancel()) inside a useEffect that depends on
debouncedHandleFilter (or include the cancel in the existing effect that creates
it), ensuring debounce from debounce(...) is properly cleaned up.

In `@src/pages/PluginEarnings.tsx`:
- Around line 196-199: debouncedHandleFilter created via useMemo with
debounce(setFilters, 500) is not cancelled on unmount; update the component to
ensure the debounced function is cancelled in cleanup by keeping a reference to
the debounced function (debouncedHandleFilter) and calling its cancel method in
a useEffect cleanup (or create it inside useEffect and return cancel) so that
debouncedHandleFilter.cancel() is invoked when the component unmounts to avoid
stale callbacks.
- Around line 233-254: The stats array in PluginEarnings.tsx currently hardcodes
"Total Revenue" and "Active Users" as "0" and uses earnings.length for
"Transactions"; update it to compute real values: call getEarningSummary() (same
helper used in Earnings.tsx) to obtain totalRevenue and activeUsers, and replace
the "Total Revenue" and "Active Users" value fields with those computed values;
for "Transactions" filter the earnings array to the last 30 days (compare each
earning's date to Date.now() - 30 days) and set the value to the filtered length
so the label "Last 30 days" matches the data.
- Around line 41-44: StateProps is missing the loading boolean that
fetchEarnings/fetchPlugin set on state, so loading is effectively untyped and
not passed to the <Table>; update the StateProps type to include loading:
boolean, ensure the component (PluginEarnings) destructures loading from state
where earnings/plugin are read, and pass that loading value into the <Table>
component so the table shows the loading indicator while
fetchEarnings/fetchPlugin are in progress.

---

Duplicate comments:
In `@src/api/portal.ts`:
- Around line 19-23: The code interpolates dynamic path segments raw into URLs
(e.g., token_id in delAuthToken and pluginId in other portal API helpers), which
can break requests if they contain reserved URI characters; fix by wrapping
every dynamic segment used inside template paths with encodeURIComponent —
specifically update delAuthToken to use encodeURIComponent(token_id) and update
all portal API functions that interpolate pluginId (and any other decoded IDs)
to use encodeURIComponent(pluginId) when building the path strings so all
requests are properly encoded.

In `@src/pages/Earnings.tsx`:
- Around line 66-78: The "From" column currently renders the token contract
address via feeAsset.addr; update the column render to show the actual sender by
using the Earning.fromAddress field instead. Locate the column definition in
Earnings.tsx (the column with title "From", dataIndex/key "feeAsset") and change
the render callback to read from the row's fromAddress (e.g., render: (_, {
fromAddress }) => ...) and display that value inside the existing
HStack/MiddleTruncate UI so the column reflects the sender, not the fee asset
contract.
- Around line 215-223: The call to form.setFieldsValue(filters) inside
fetchEarnings (the async created by useEffectEvent) causes the form to be
updated on every filters change and produces flicker; remove that call from
fetchEarnings and instead set the form once on mount or initialization (e.g., in
a separate useEffect with empty deps or via the form's initialValues) so only
the initial filters populate the form; reference fetchEarnings,
form.setFieldsValue, useEffectEvent and filters to locate and adjust the code.

In `@src/pages/PluginEarnings.tsx`:
- Around line 58-68: The "From" column is rendering feeAsset.addr incorrectly;
update the column definition (dataIndex "feeAsset", key "feeAsset", title
"From", render function) to use the transaction's fromAddress instead of
feeAsset.addr — either change dataIndex/key to "fromAddress" or adjust the
render callback signature to extract and return fromAddress (e.g., render: (_, {
fromAddress }) => <MiddleTruncate ...>{fromAddress}</MiddleTruncate>), keeping
the existing MiddleTruncate styling.
- Around line 272-277: The image rendered with Stack as="img" when
plugin.logoUrl exists is missing an alt attribute; update the JSX in
PluginEarnings (where plugin.logoUrl and the Stack with as="img" are used) to
include an appropriate alt prop (e.g., alt={plugin.name ?? 'Plugin logo'}), or
set alt="" if the image is purely decorative, so screen readers get correct
accessibility info.

In `@src/providers/Core.tsx`:
- Around line 36-42: The async fetchBaseValue invoked on currency changes can
write stale baseValue when out-of-order responses arrive; fix by tagging each
request with a monotonic id (e.g., requestId stored in a ref) or using an
AbortController if getBaseValue supports cancellation: increment the id before
calling getBaseValue, capture the id in the async closure, and only call
setState to update baseValue when the captured id equals the current ref id (or
when the AbortController signal has not been aborted). Apply this change inside
fetchBaseValue (referencing fetchBaseValue, getBaseValue, setState, baseValue,
currency) so older responses cannot overwrite newer state.

---

Nitpick comments:
In `@src/api/portal.ts`:
- Around line 25-34: Remove the dead, commented-out delMember function block
(the commented export const delMember ...) from the codebase; delete the entire
commented block so no dead code remains, ensure surrounding code formatting is
preserved (no leftover blank lines or dangling commas), and run lint/tests to
confirm nothing else depended on that commented declaration.

In `@src/pages/Dashboard.tsx`:
- Around line 37-49: The fetchData effect currently awaits getEarningSummary()
then getPlugins() sequentially; update fetchData (the async handler created with
useEffectEvent) to run them in parallel using Promise.all([getEarningSummary(),
getPlugins()]) and destructure the results into totalEarnings and plugins, then
call setState to clear loading and set plugins/totalEarnings; ensure loading is
set true before the Promise.all and false after, and handle/rethrow errors as
currently done.

In `@src/pages/Earnings.tsx`:
- Around line 37-49: In fetchData (used by EarningsPage), getEarningSummary()
and getPlugins() are being awaited sequentially; change to run them in parallel
with Promise.all (e.g., Promise.all([getEarningSummary(), getPlugins()])) and
then destructure results to update state in one setState call, preserving the
loading flag and merging the EarningSummary fields into state; ensure error
handling still catches failures and sets loading false.

In `@src/providers/Core.tsx`:
- Around line 48-52: The setCurrency function currently updates storage and
state even when the new currency equals the current one; add an early no-op
guard in setCurrency that compares the incoming currency with the current
state.currency and returns immediately (skipping setCurrencyStorage and
setState) when identical to avoid redundant renders/fetches; apply the same
pattern to setTheme (and its storage helper setThemeStorage) so both setters
bail out on no-op updates.

In `@src/utils/functions.ts`:
- Around line 219-234: Remove the commented-out dead code for the toKebabCase
helper: delete the entire commented block that references toKebabCase, toKebab,
isObject, and isArray in src/utils/functions.ts so the file contains only active
code; if this functionality is needed later it can be restored from VCS.
- Around line 208-217: The toDecimalFormat function's parameter baseValue is
misleading because the formula value * baseValue / 10^decimals treats it as a
price/exchange-rate; update the signature or docs to clarify intent: either
rename baseValue to price or exchangeRate in the function declaration and all
call sites (toDecimalFormat(value, price, decimals)), or add a JSDoc above
toDecimalFormat explaining that baseValue is a price/exchange-rate multiplier
used to convert units (value * baseValue / 10^decimals), and update parameter
name in the JSDoc to match; ensure tests/types and any imports/usages are
updated accordingly.
- Around line 55-142: The getExplorerUrl function repeats the same string
patterns for most chains; replace the large match blocks in getExplorerUrl with
a data-driven approach: compute a default path for entity ("address" ->
"/address/${value}", "tx" -> "/tx/${value}") using explorerBaseUrl[chain], then
apply small override lookup maps (e.g., addressOverrides and txOverrides keyed
by chains.Polkadot, chains.Ripple, chains.Ton, chains.TerraClassic,
chains.BitcoinCash, chains.Cardano, chains.Dash, chains.Dogecoin,
chains.Litecoin, chains.Tron, etc.) to return the special path when present;
update getExplorerUrl to first check overrideMap[entity][chain] and fall back to
defaultPath, keeping references to getExplorerUrl, explorerBaseUrl and the
chains enum to locate where to change code.

In `@src/utils/types.ts`:
- Around line 66-83: The Proposal type mixes API-returned fields with form-only
fields (logo, media, thumbnail), causing form values to be serialized and sent
to the API (e.g., via toSnakeCase in createProposal); split responsibilities by
creating a new ProposalForm type that extends the API Proposal with the
form-only fields and update usages: keep Proposal as the pure API shape, change
form handlers and createProposal input to accept ProposalForm (or convert
ProposalForm -> Proposal before calling toSnakeCase/createProposal), and update
any type annotations referencing Proposal where they should be the form type.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77e3a44 and 1a014a0.

📒 Files selected for processing (10)
  • src/api/portal.ts
  • src/context/Core.tsx
  • src/pages/Dashboard.tsx
  • src/pages/Earnings.tsx
  • src/pages/PluginEarnings.tsx
  • src/pages/ProjectManagement.tsx
  • src/pages/ProposalManagement.tsx
  • src/providers/Core.tsx
  • src/utils/functions.ts
  • src/utils/types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/pages/ProjectManagement.tsx
  • src/pages/ProposalManagement.tsx

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.

1 participant